CMS configuration
The CMS is added to a site with the cmsPlugin() plugin. To set it up for the first time, see Getting started. This page covers the options you're most likely to need.
Plugin options
Only firebaseConfig is required. Common options include:
-
id: namespaces the site's content in Firestore, so several sites can share one Firebase project. Defaults todefault. -
name: the site name shown in the CMS. -
firebaseConfig: the web app config from the Firebase console. -
gapi: a Google API key and OAuth client id, for the Google Drive and Sheets features. -
ai: the models available to Root AI. -
isUserAuthorized: a custom check for whether a user can access the CMS.
Checks, Notifications and integrations and Customizing the CMS cover the rest, and CMSPluginOptions in the API reference lists every option.
// @/root.config.ts
import {defineConfig} from '@blinkk/root';
import {cmsPlugin} from '@blinkk/root-cms/plugin';
export default defineConfig({
plugins: [
cmsPlugin({
id: 'my-site',
name: 'My Site',
firebaseConfig: {
apiKey: '...',
authDomain: 'my-project.firebaseapp.com',
projectId: 'my-project',
storageBucket: 'my-project.appspot.com',
},
}),
],
});
Setting up Firebase by hand
root-cms setup does all of this for you (see Getting started). To set up a project by hand instead:
-
Create a project in the Firebase console, or add Firebase to an existing Google Cloud project.
-
Create a Firestore database in Native mode.
-
Under Authentication, enable the Google sign-in provider. If your site will serve on a custom domain, add it to the authorized domains.
-
Under Project settings, register a web app and copy its
firebaseConfigvalues intocmsPlugin():
// @/root.config.ts
cmsPlugin({
id: 'starter',
name: 'Starter',
// From Project settings > Your apps in the Firebase console.
firebaseConfig: {
apiKey: '...',
authDomain: 'my-project.firebaseapp.com',
projectId: 'my-project',
storageBucket: 'my-project.appspot.com',
},
}),
Before you deploy, also replace sessionCookieSecret with a long random value. The server reads and writes Firestore with application default credentials; for local development, sign in with the gcloud CLI:
gcloud auth login
gcloud auth application-default login
Finally, apply the security rules and add yourself as an admin, as described below.
Security rules and roles
Content access is controlled by Firestore security rules and a list of roles per site. The easiest way to set both up is with init-firebase, which applies the rules and makes you an ADMIN:
pnpm exec root-cms init-firebase --admin=you@example.com
To apply the rules by hand, paste them into the Rules tab of the Firestore page in the Firebase console:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /Projects/{project} {
allow write:
if isSignedIn() && userIsAdmin();
allow read:
if isSignedIn() && userCanRead();
match /{collection}/{document=**} {
allow write:
if isSignedIn() && userCanPublish();
allow read:
if isSignedIn() && userCanRead();
}
match /Collections/{collectionId}/Drafts/{document=**} {
allow write:
if isSignedIn() && userCanEdit();
}
function isSignedIn() {
return request.auth != null;
}
function getRoles() {
return get(/databases/$(database)/documents/Projects/$(project)).data.roles;
}
function userCanRead() {
let roles = getRoles();
let email = request.auth.token.email;
let domain = '*@' + email.split('@')[1];
return (roles[email] in ['ADMIN', 'EDITOR', 'CONTRIBUTOR', 'VIEWER']) || (roles[domain] in ['ADMIN', 'EDITOR', 'CONTRIBUTOR', 'VIEWER']);
}
function userCanPublish() {
let roles = getRoles();
let email = request.auth.token.email;
let domain = '*@' + email.split('@')[1];
return (roles[email] in ['ADMIN', 'EDITOR']) || (roles[domain] in ['ADMIN', 'EDITOR']);
}
function userCanEdit() {
let roles = getRoles();
let email = request.auth.token.email;
let domain = '*@' + email.split('@')[1];
return (roles[email] in ['ADMIN', 'EDITOR', 'CONTRIBUTOR']) || (roles[domain] in ['ADMIN', 'EDITOR', 'CONTRIBUTOR']);
}
function userIsAdmin() {
let roles = getRoles();
let email = request.auth.token.email;
let domain = '*@' + email.split('@')[1];
return (roles[email] == 'ADMIN') || (roles[domain] == 'ADMIN');
}
}
}
}
Then, in Firestore, create a doc at Projects/<id> (where <id> is the plugin's id) with a roles map from your email to ADMIN. After that, manage users from the CMS's settings page.
The roles are:
-
ADMIN: everything, including managing users and settings.
-
EDITOR: edit and publish content.
-
CONTRIBUTOR: edit drafts, but not publish.
-
VIEWER: read-only access.
A role can be granted to a whole domain with an entry like *@example.com. Anyone with a verified email on that domain gets the role, so use it sparingly, never with a free email provider, and prefer per-email grants for ADMIN and EDITOR.
Google Drive and Sheets
Some CMS features, like importing from Google Sheets and Drive, use Google APIs on the editor's behalf. To enable them:
-
In the Google Cloud console, enable the Google Sheets API and Google Drive API.
-
Create an OAuth client id (web application) and an API key.
-
Pass them to the plugin as
gapi: {clientId, apiKey}, e.g. from environment variables.
Root AI
Root AI lets editors chat with, draft and translate content in the CMS. Enable it with the ai option, listing the models editors can choose from and their API keys. See the v3 migration guide for an example.
Custom AI tools and skills
Available as of version 2.4.1.
Root AI can call your own tools alongside the built-in ones for reading and editing docs. Add them to ai.tools. The chat runs in the browser, but a tool's execute function always runs on the server, so it can use API keys and server-only code. The input is checked against inputSchema (a zod schema, or jsonSchema() from ai) before execute runs, and whatever it returns is sent back to the model.
Skills are reusable instructions an editor runs by typing / and the skill's name in the chat box, like /launch-checklist Pages/home. The model gets the skill's instructions plus whatever was typed after the name.
// @/root.config.ts
import {cmsPlugin, defineAiSkill, defineAiTool} from '@blinkk/root-cms/plugin';
import {z} from 'zod';
cmsPlugin({
ai: {
models: [
// ...
],
tools: [
defineAiTool({
name: 'inventory_lookup',
description: 'Looks up the stock level for a product SKU.',
inputSchema: z.object({sku: z.string()}),
execute: async ({sku}, ctx) => {
return await fetchStock(sku, {user: ctx.user.email});
},
}),
],
skills: [
defineAiSkill({
name: 'launch-checklist',
description: 'Reviews a page against the launch checklist.',
instructions: 'Read the page with doc_get, then check that ...',
}),
],
},
});
Set write: true on a tool that changes anything. Write tools aren't offered in the chat's read-only mode, and they always wait for the editor to approve each call, even in auto-apply mode. By default, read tools are available to every role and write tools to admins, editors and contributors; set roles to restrict a tool further. Tool names can't reuse the name of a built-in tool such as doc_get.
Other plugins can contribute tools and skills too, by setting rootAi: {tools, skills} on the plugin object (see RootAiPlugin in the API reference).
Scheduled jobs
Scheduled publishing, version history and a few other features rely on a job that calls the /cms/api/cron.run endpoint every few minutes.
Firebase: if your site is deployed to Cloud Functions for Firebase, export the cron function from @blinkk/root-cms/functions:
// @/index.ts
import {server} from '@blinkk/root/functions';
import {cron} from '@blinkk/root-cms/functions';
export const www = {
server: server({mode: 'production'}),
cron: cron(),
};
Other hosting: on App Engine, Cloud Run or elsewhere, create a Cloud Scheduler job that calls /cms/api/cron.run on your site every few minutes.
Credentials in production
The server accesses Firestore with application default credentials. On Google Cloud (App Engine, Cloud Run, Cloud Functions), the runtime's service account is used, so give it access to Firestore. Elsewhere, set GOOGLE_APPLICATION_CREDENTIALS to a service account key file.