Skip to main content
Reference

@blinkk/root-cms

The Root.js CMS: the plugin, schemas, RootCMSClient and rich text rendering. This reference is generated from the package's TypeScript types and doc comments.

@blinkk/root-cms

import {…} from '@blinkk/root-cms';

Actioninterface

interface Action<T = any>
Members (5)
action: string;

The name of the action.

by?: string;

The user's email that performed the action (or "system").

timestamp: Timestamp;

Timestamp when the action occurred.

metadata?: T;

Metadata for the action.

links?: {
    label: string;
    url: string;
    target?: string;
}[];

Optional list of quick links to display in the UI.

applyProposalfunction

function applyProposal(cmsClient: RootCMSClient, proposal: Proposal, options?: ApplyProposalOptions): Promise<ApplyProposalResult>;

Resolves and (unless dryRun) applies every change in a proposal.

ApplyProposalOptionsinterface

interface ApplyProposalOptions
Members (4)
dryRun?: boolean;

Resolve and validate everything, but write nothing.

verifyBefore?: boolean;

Also check each op's recorded before against the value currently in the database and fail on drift. Off by default: before is documentation for the human reviewing the proposal, not a precondition.

modifiedBy?: string;

Attributed as the author of the writes.

skipValidation?: boolean;

Skip schema validation of the resulting doc fields.

ApplyProposalResulttype

type ApplyProposalResult = {
    ok: true;
    dryRun: boolean;
    changes: ResolvedChange[];
} | {
    ok: false;
    errors: ProposalApplyError[];
};

ArrayObjectinterface

interface ArrayObject
Members (1)
_array: string[];

BatchRequestclass

class BatchRequest
Members (6)
cmsClient: RootCMSClient;
addDoc(docId: string): void;

Adds a doc to the batch request.

addDataSource(dataSourceId: string): void;

Adds a data source to the batch request.

addQuery(queryId: string, collectionId: string, queryOptions?: BatchRequestQueryOptions): void;

Adds a collection-based query to the batch request.

addTranslations(translationsId: string): void;

Adds a translations doc to the request.

fetch(): Promise<BatchResponse>;

Fetches data from the DB.

BatchRequestOptionsinterface

interface BatchRequestOptions
Members (3)
mode: 'draft' | 'published';
translate?: boolean;

Whether to automatically fetch translations for the docs retrieved in the request (including docs returned by queries).

locales?: string[];

Locales to fetch translations for. Each locale is expanded through its fallback chain (per the i18n.fallbacks config). Defaults to the locales configured in i18n.locales.

BatchRequestQueryinterface

interface BatchRequestQuery
Members (3)
queryId: string;
collectionId: string;
queryOptions?: BatchRequestQueryOptions;

BatchRequestQueryOptionsinterface

interface BatchRequestQueryOptions
Members (5)
offset?: number;
limit?: number;
orderBy?: string;
orderByDirection?: 'asc' | 'desc';
query?: (query: Query) => Query;

BatchResponseclass

class BatchResponse
Members (5)
docs: Record<string, Doc>;
queries: Record<string, Doc[]>;
dataSources: Record<string, DataSourceData>;
translations: Record<string, Record<string, TranslationsLocaleDoc>>;

Translations locale docs retrieved in the request, keyed by translations id then by locale. The ids are in precedence order: generic translations (e.g. "common") first, doc-specific translations last.

getTranslations(locale: string | string[]): LocaleTranslations;

Returns a map of translations for a given locale or locale fallbacks.

The input is either a single locale (e.g. "de"), which is expanded through the fallback chain configured in i18n.fallbacks, or an array of locales representing an explicit fallback chain, e.g. ["en-CA", "en-GB", "en"].

The returned value is a flat map of source string to translated string, e.g.: {"<source>": "<translation>"}

buildTranslationsDbPathfunction

function buildTranslationsDbPath(options: TranslationsDbPathOptions): string;

buildTranslationsLocaleDocDbPathfunction

function buildTranslationsLocaleDocDbPath(options: TranslationsLocaleDocDbPathOptions): string;

chunkArrayfunction

function chunkArray<T>(items: T[], chunkSize: number): T[][];

Splits an array into chunks of (up to) a given size.

compareSortKeysfunction

function compareSortKeys(a: string, b: string): number;

Compares two sort keys by code point (the same ordering Firestore uses for string fields). Intentionally not localeCompare(), whose locale-aware collation can disagree with Firestore's byte ordering.

createRoutefunction

function createRoute(options: CreateRouteOptions): Route;

Utility for creating Root filesystem routes that are connected to a CMS doc.

CreateRouteOptionsinterface

interface CreateRouteOptions
Members (14)
collection: string;

Collection mapped to the route.

slugParam?: string;

Route param name used for the slug. Used for dynamic routes, e.g. [...slug].tsx. Defaults to "slug".

slugFormat?: string;

Format pattern for slugs that use multiple param values to form the slug, e.g. for experiments you might have something like:

Route: routes/ex/[experimentId]/[...page].tsx Doc ID: ExperimentPages/1234--about--foo URL path: /ex/1234/about/foo/

To grab the correct doc, use {slugFormat: '[experimentId]/[page]'}.

slug?: string;

Slug to use for the route. Used for non-dynamic, single-document routes.

fetchData?: (context: RouteContext) => Record<string, Promise<any>>;

Callback function that returns a map of Promises that contain fetched data. Once the promise is resolved, the values are injected into page's props for rendering.

notFoundHook?: (req: Request, res: Response) => void | Promise<void>;

Hook that's called when the doc is not found. If not provided, the default 404 handler will be called.

preRenderHook?: (props: any, context: RouteContext) => any | Promise<any>;

Hook for amending any props values before being passed to the page component.

setResponseHeaders?: (req: Request, res: Response) => void;

Hook for setting any response headers.

translations?: (context: RouteContext) => {
    tags?: string[];
};

Translations configuration.

disableCacheControl?: boolean;

Sets Cache-Control header to private.

ssg?: boolean;

Enables SSG mode for sites that serve on SCS or other static servers.

ssgMode?: DocMode;

Overrides the "mode" (draft vs published) for SSG builds. Primarily intended for testing prior to launches.

previewOnly?: boolean;

Whether the route should only be available with ?preview=true.

defaultLocale?: string;

Overrides the default locale for the route.

CronScheduleTypetype

type CronScheduleType = 'interval' | 'daily' | 'weekly' | 'custom';

Data source sync schedule type.

- interval: sync every N minutes/hours/days (uses interval + unit). - daily / weekly / custom: sync on a specific cron schedule (uses expression + timezone). daily and weekly are UI presets that are stored as standard cron expressions; custom allows an arbitrary expression.

CronUnittype

type CronUnit = 'minutes' | 'hours' | 'days';

DataSourceinterface

interface DataSource
Members (15)
id: string;
description?: string;
type: 'http' | 'gsheet';
url: string;
dataFormat?: GsheetDataFormat;

Currently only used by gsheet. map returns the sheet as an array of objects keyed by the header row, grid returns the sheet as an array of arrays of strings (including the header row).

httpOptions?: {
    method: HttpMethod;
    headers?: Record<string, string>;
    body?: string;
};

Options for HTTP requests.

cron?: DataSourceCron;
createdAt: Timestamp;
createdBy: string;
syncedAt?: Timestamp;
syncedBy?: string;
publishedAt?: Timestamp;
publishedBy?: string;
archivedAt?: Timestamp;
archivedBy?: string;

DataSourceCroninterface

interface DataSourceCron
Members (7)
enabled: boolean;
schedule?: CronScheduleType;

Scheduling mode. Defaults to interval when unset (for backwards compatibility with data sources created before specific schedules were supported).

interval?: number;

Interval value, used when schedule is interval.

unit?: CronUnit;

Interval unit, used when schedule is interval.

expression?: string;

Standard 5-field cron expression, used when schedule is daily, weekly, or custom, e.g. 0 19 * * * for "every day at 7pm".

timezone?: string;

IANA timezone used to evaluate expression, e.g. America/New_York. Defaults to UTC when unset.

autoPublish?: boolean;

DataSourceDatainterface

interface DataSourceData<T = any>
Members (3)
dataSource: DataSource;
data: T;
headers?: string[];

Optional list of column headers (for gsheet sources).

DataSourceGridRowinterface

interface DataSourceGridRow

A single row of grid data as stored in Firestore. Rows are wrapped in a map because Firestore doesn't support arrays nested directly within arrays.

Members (1)
cells: string[];

DataSourceModetype

type DataSourceMode = 'draft' | 'published';

DependencyGraphclass

class DependencyGraph

An in-memory snapshot of the dependency graph for a single doc mode, providing forward (dependencies) and reverse (dependents) lookups.

Members (5)
readonly mode: DocMode;
readonly lastRun: number | null;

Millis timestamp of the last graph update, or null if never built.

readonly edges: Record<string, string[]>;

Map of doc id to the sorted list of doc ids it references.

getDependencies(docIds: string | string[], options?: GetDependenciesOptions): string[];

Returns the doc ids referenced by the given doc(s), i.e. the additional docs that need to be fetched when fetching the given docs. Resolves transitively by default (references of referenced docs are included, recursively; cycles are handled). The input doc ids themselves are excluded from the result.

getDependents(docIds: string | string[], options?: GetDependenciesOptions): string[];

Returns the doc ids that reference the given doc(s). Useful for finding which docs are affected when a doc changes (e.g. for cache invalidation). Resolves transitively by default.

DependencyGraphBatchOptype

type DependencyGraphBatchOp = (batch: WriteBatch) => void;

A single buffered write produced for a caller-managed Firestore batch.

DependencyGraphModeStatusinterface

interface DependencyGraphModeStatus
Members (2)
docsWithRefs: number;

Number of docs with at least one outgoing reference.

refs: number;

Total number of outgoing reference edges.

DependencyGraphRebuildResultinterface

interface DependencyGraphRebuildResult
Members (5)
forced: boolean;

True if a full (force) rebuild was performed.

skipped: boolean;

True if the rebuild short-circuited (no doc changes were found).

refDocCounts: Record<DocMode, number>;
refCounts: Record<DocMode, number>;
durationMs: number;

DependencyGraphServiceclass

class DependencyGraphService

Service for building, updating, and reading the persisted dependency graph.

Members (11)
isEnabled(): boolean;

Returns true if the dependency graph feature is enabled.

isCollectionTracked(collectionId: string): boolean;

Returns true if the given collection passes the include/exclude filter.

listCollectionIds(): Promise<string[]>;

Lists collection ids by globbing <rootDir>/collections/*.schema.ts, filtered by the include/exclude config.

hasChangesSince(meta: {
    lastRun: Timestamp;
    docCounts: Record<string, number>;
}): Promise<boolean>;

Returns true when any doc changed (created, updated, published, or deleted) since lastRun. Uses limit(1) probes for modifications and count() aggregations (compared against the doc counts captured at lastRun) for deletions, so the no-change case stays cheap.

runCronUpdate(options?: {
    minIntervalMs?: number;
}): Promise<DependencyGraphRebuildResult | null>;

Cron entrypoint: incrementally updates the graph when the feature is enabled, the min interval has elapsed, and any doc changed since the last run. Returns null when the update was skipped.

buildPublishBatchOps(docs: Array<{
    collection: string;
    slug: string;
    fields?: any;
}>): Promise<DependencyGraphBatchOp[]>;

Builds the writes that sync the published-mode edges for docs being published, so new references are queryable immediately (without waiting for the next cron tick). Callers weave the returned ops into the same batch as the publish writes. Returns an empty list when the feature is disabled, no tracked docs are affected, or the graph has never been built.

buildUnpublishBatchOps(docs: Array<{
    collection: string;
    slug: string;
}>): Promise<DependencyGraphBatchOp[]>;

Builds the writes that remove the published-mode edges for docs being unpublished. See buildPublishBatchOps().

syncPublishedDocs(docIds: string[]): Promise<{
    synced: number;
}>;

Re-reads the given docs from the Published collection and syncs their published-mode edges: existing docs get their refs re-extracted, missing docs (unpublished or deleted) get their edges removed. Used by the /cms/api/dependency_graph.sync_published endpoint, which the CMS UI calls after client-side publishes. Idempotent — the graph is synced to whatever is currently in the database.

rebuildGraph(opts?: {
    force?: boolean;
}): Promise<DependencyGraphRebuildResult>;

Orchestrates a graph rebuild (incremental by default; full when force: true).

getGraph(mode: DocMode): Promise<DependencyGraph>;

Returns the dependency graph for the given mode, loading it from Firestore if necessary. Loaded graphs are cached in-process and re-validated against _meta.lastRun on every call, so repeated calls only cost a single meta doc read until the graph changes.

getStatus(): Promise<DependencyGraphStatus>;

Lightweight metadata read for status endpoints.

DependencyGraphStatusinterface

interface DependencyGraphStatus
Members (4)
enabled: boolean;

Whether the feature is enabled via the cmsPlugin config.

lastRun: number | null;

Millis timestamp of the last graph update, or null if never built.

draft: DependencyGraphModeStatus;
published: DependencyGraphModeStatus;

Docinterface

interface Doc<Fields = any>
Members (5)
id: string;

The id of the doc, e.g. "Pages/foo-bar".

collection: string;

The collection id of the doc, e.g. "Pages".

slug: string;

The slug of the doc, e.g. "foo-bar".

sys: {
    createdAt: number;
    createdBy: string;
    modifiedAt: number;
    modifiedBy: string;
    firstPublishedAt?: number;
    firstPublishedBy?: string;
    publishedAt?: number;
    publishedBy?: string;
    publishingLocked: {
        lockedAt: string;
        lockedBy: string;
        reason: string;
        until?: Timestamp;
    };
    locales?: string[];
    assets?: string[];
    sortKey?: string;
};
fields: Fields;

DocCreateChangeinterface

interface DocCreateChange

Creation of a brand new doc with a complete set of fields.

Members (4)
kind: 'doc.create';
docId: string;
note?: string;
after: Record<string, any>;

DocDuplicateChangeinterface

interface DocDuplicateChange

Duplication of an existing doc to a new slug.

Members (4)
kind: 'doc.duplicate';
fromDocId: string;
toDocId: string;
note?: string;

DocEditChangeinterface

interface DocEditChange

Edits to an existing doc's draft fields.

Members (4)
kind: 'doc.edit';
docId: string;
note?: string;

Free-form rationale shown to the reviewer.

ops: ProposalOp[];

DocModetype

type DocMode = 'draft' | 'published';

extractRefDocIdsfunction

function extractRefDocIds(fieldsData: any): string[];

Extracts the doc ids referenced by a doc's fields data. Accepts the raw (marshaled) data as stored in Firestore or unmarshaled data — both plain arrays and _array objects are traversed. Returns a sorted, de-duped list.

generateKeyAfterfunction

function generateKeyAfter(a: string | null): string;

Returns a key that sorts after a (e.g. after the current max key to append an item to the end of the list). Pass null when the list has no keys yet.

generateKeyBetweenfunction

function generateKeyBetween(a: string | null, b: string | null): string;

Returns a key that sorts strictly between a and b (by code point). Pass a = null for "before everything" and b = null for "after everything"; generateKeyBetween(null, null) returns the initial key. Throws if a >= b or if either key is malformed.

generateNKeysBetweenfunction

function generateNKeysBetween(a: string | null, b: string | null, n: number): string[];

Returns n distinct keys in sorted order, all strictly between a and b (same semantics as generateKeyBetween). Used for bulk-assigning positions, e.g. when initializing the custom order of an existing collection.

getCmsPluginfunction

function getCmsPlugin(rootConfig: RootConfig): CMSPlugin;

GetCountOptionsinterface

interface GetCountOptions
Members (2)
mode: DocMode;
query?: (query: Query) => Query;

GetDependenciesOptionsinterface

interface GetDependenciesOptions
Members (1)
transitive?: boolean;

Whether to resolve dependencies transitively (i.e. also include the references of referenced docs, recursively). Defaults to true.

getDocfunction

function getDoc<T>(rootConfig: RootConfig, collectionId: string, slug: string, options: {
    mode: 'draft' | 'published';
}): Promise<T | null>;

Deprecated. Use RootCMSClient.getDoc() instead.

Retrieves a doc from Root.js CMS.

GetDocOptionsinterface

interface GetDocOptions
Members (1)
mode: DocMode;

Mode, either "draft" or "published".

getFirstQueryParamfunction

function getFirstQueryParam(req: Request, key: string): string | null;

Returns the first query param value in a given request.

For example, for a URL like /?foo=bar&foo=baz, calling getFirstQueryParam(req, 'foo') would return "bar".

getModefunction

function getMode(req: Request): Promise<DocMode>;

Returns the CMS document mode associated with the request.

getProposalDocIdsfunction

function getProposalDocIds(proposal: Proposal): string[];

Returns every doc id a proposal touches, in document order, deduped.

getProposedNewDocIdsfunction

function getProposedNewDocIds(proposal: Proposal): string[];

Returns the doc ids a proposal creates (via doc.create/doc.duplicate).

getReferenceDocIdfunction

function getReferenceDocId(value: any): string | null;

Returns the normalized doc id (<collection>/<slug>) when a value has the shape saved by the CMS reference fields ({id, collection, slug} with a consistent id), or null otherwise. Requiring the id to agree with the collection/slug pair avoids false positives on arbitrary user data that happens to use the same keys.

GsheetDataFormattype

type GsheetDataFormat = 'map' | 'grid';

Format used to store data synced from a csv-style data source.

- map: an array of objects keyed by the sheet's header row, e.g. [{header1: 'foo', header2: 'bar'}]. - grid: an array of arrays of strings, including the header row, e.g. [['header1', 'header2'], ['foo', 'bar']].

hashPasswordfunction

function hashPassword(password: string, options?: HashPasswordOptions): Promise<PasswordHash>;

Hashes a password with a freshly generated random salt.

const stored = await hashPassword('hunter2');
// => {algorithm: 'pbkdf2-sha256', iterations: 600000, salt: '...', hash: '...'}

HashPasswordOptionsinterface

interface HashPasswordOptions
Members (1)
iterations?: number;

The number of PBKDF2 iterations. Defaults to DEFAULT_ITERATIONS. Higher values are slower to compute and slower to brute force.

HttpMethodtype

type HttpMethod = 'GET' | 'POST';

ImportTranslationsFromV1Resultinterface

interface ImportTranslationsFromV1Result

Stats returned by importTranslationsFromV1().

Members (2)
ids: string[];

Translations doc ids that were created or updated.

stats: {
    numStrings: number;
    numDocs: number;
};

isCollectionTrackedfunction

function isCollectionTracked(filters: ResolvedFilters, collectionId: string): boolean;

isPasswordHashfunction

function isPasswordHash(value: unknown): value is PasswordHash;

Returns whether a value has the shape of a PasswordHash.

isRichTextDatafunction

function isRichTextData(data: any): boolean;

Returns true if the data is a rich text data object.

ListActionsOptionsinterface

interface ListActionsOptions
Members (3)
action?: string;

Filter by a specific action. Defaults to all actions.

by?: string;

Filter by a specific user. Defaults to all users.

limit?: number;

Max number of actions to return. Defaults to 100.

listDocsfunction

function listDocs<T>(rootConfig: RootConfig, collectionId: string, options: {
    mode: 'draft' | 'published';
    offset?: number;
    limit?: number;
    orderBy?: string;
    orderByDirection?: 'asc' | 'desc';
    query?: (query: Query) => Query;
}): Promise<{
    docs: T[];
}>;

Deprecated. Use RootCMSClient.listDocs() instead.

Lists docs from a Root.js CMS collection.

ListDocsOptionsinterface

interface ListDocsOptions
Members (7)
mode: DocMode;
offset?: number;
limit?: number;
orderBy?: string;

DB field path to order results by, e.g. sys.createdAt. Defaults to slug. The default is skipped when a query fn is provided, since the query fn may supply its own ordering.

orderByDirection?: 'asc' | 'desc';
query?: (query: Query) => Query;
raw?: boolean;

Whether to fetch the "raw" version of the doc (for use in conjunction with setRawDoc()).

loadTranslationsfunction

function loadTranslations(rootConfig: RootConfig, options?: LoadTranslationsOptions): Promise<TranslationsMap>;

Deprecated. Use `RootCMSClient.loadTranslations()`.

loadTranslationsForLocalefunction

function loadTranslationsForLocale(rootConfig: RootConfig, locale: string, options?: LoadTranslationsOptions): Promise<LocaleTranslations>;

Deprecated. Use `RootCMSClient.loadTranslationsForLocale()`.

LoadTranslationsOptionsinterface

interface LoadTranslationsOptions
Members (1)
tags?: string[];

Localetype

type Locale = string;

LocaleFallbacksI18nConfiginterface

interface LocaleFallbacksI18nConfig

Utilities for resolving the locale fallback chain used by translations.

The i18n.fallbacks config in root.config.ts maps a locale to an ordered list of fallback locales. When a translation is missing for a locale, each fallback locale is checked (in order) before falling back to i18n.defaultLocale and finally the source string. For example:

i18n: {
  locales: ['en', 'en-GB', 'en-CA'],
  fallbacks: {
    'en-CA': ['en-GB'],
  },
}

With the config above, resolveLocaleFallbacks(i18n, 'en-CA') returns ['en-CA', 'en-GB', 'en']. Fallbacks are resolved recursively (breadth first), and all matching is case-insensitive.

Members (3)
locales?: string[];
defaultLocale?: string;
fallbacks?: Record<string, string[]>;

LocaleTranslationsinterface

interface LocaleTranslations

marshalArrayvariable

const marshalArray: typeof toArrayObject;

marshalDatafunction

function marshalData(data: any): any;

Walks the data tree and converts any array of objects into "array objects" for storage in firestore.

migrateV1TranslationsIfNeededfunction

function migrateV1TranslationsIfNeeded(cmsClient: RootCMSClient, options?: MigrateV1TranslationsOptions): Promise<MigrateV1TranslationsResult>;

Migrates v1 translations to the v2 TranslationsManager if the migration hasn't already run for this project. Safe to call on every dev server boot and build: after the first successful run, this costs a single Firestore read.

MigrateV1TranslationsOptionsinterface

interface MigrateV1TranslationsOptions
Members (1)
trigger?: 'build' | 'dev';

What triggered the migration (used for logging/state).

MigrateV1TranslationsResultinterface

interface MigrateV1TranslationsResult
Members (2)
status: TranslationsMigrationStatus;
skipped: boolean;

True if the migration was skipped, either because it already completed or because another process holds the running lock.

MultiLocaleTranslationsMapinterface

interface MultiLocaleTranslationsMap

A translations map containing translations for multiple locales.

Example:

{
  "one": {"es": "uno", "fr": "un"},
  "two": {"es": "dos", "fr": "deux"}
}

normalizeDatafunction

function normalizeData(data: any): any;

Deprecated. Use `unmarshalData()` instead.

numDocsfunction

function numDocs(rootConfig: RootConfig, collectionId: string, options: {
    mode: 'draft' | 'published';
    query?: (query: Query) => Query;
}): Promise<number>;

Deprecated. Use RootCMSClient.getDocsCount() instead.

Returns the number of docs in a Root.js CMS collection.

parseDocIdfunction

function parseDocId(docId: string): {
    collection: string;
    slug: string;
};

Parses a docId (e.g. Pages/foo) and returns the collection and slug.

parseProposalfunction

function parseProposal(yamlText: string): ParseProposalResult;

Parses a YAML change proposal.

Syntax errors are reported by the YAML parser with real line numbers; structural errors come from the schema and are mapped back onto source lines by walking the parsed document.

ParseProposalResulttype

type ParseProposalResult = {
    ok: true;
    proposal: Proposal;
} | {
    ok: false;
    errors: ProposalParseError[];
};

PasswordHashinterface

interface PasswordHash

The value stored in the db for a password field. The salt and the derived hash are stored base64-encoded alongside the parameters used to compute them, so the algorithm or its cost can change without invalidating existing hashes.

Members (4)
algorithm: PasswordHashAlgorithm;

The key derivation algorithm used, e.g. pbkdf2-sha256.

iterations: number;

The number of PBKDF2 iterations used to derive the hash.

salt: string;

The random salt used to derive the hash, base64-encoded.

hash: string;

The derived hash, base64-encoded.

PasswordHashAlgorithmtype

type PasswordHashAlgorithm = 'pbkdf2-sha256';

The hashing algorithms supported by hashPassword.

Proposalinterface

interface Proposal

A parsed change proposal.

Members (8)
version: number;
id: string;

Stable id for the proposal, e.g. "2026-08-20-hero-refresh".

title?: string;
project?: string;

CMS project id the proposal targets. Advisory only.

generated?: string;

ISO-8601 timestamp of when the proposal was generated.

author?: string;
summary?: string;
changes: ProposalChange[];

PROPOSAL_CHANGE_KINDSvariable

const PROPOSAL_CHANGE_KINDS: readonly [
    'doc.edit',
    'doc.create',
    'doc.duplicate',
    'release.create',
    'release.update',
    'translations'
];

Change kinds a proposal can describe.

PROPOSAL_VERSIONvariable

const PROPOSAL_VERSION = 1;

The only proposal format version understood by this module.

ProposalApplyErrorinterface

interface ProposalApplyError

A problem that prevented a change from being applied.

Members (7)
changeIndex: number;

Zero-based index of the change in proposal.changes.

kind: string;
target: string;

The target of the change, e.g. a doc id or release id.

message: string;
path?: string;

Dotted field path, when the failure is tied to one.

expected?: string;
received?: any;

ProposalChangetype

type ProposalChange = DocEditChange | DocCreateChange | DocDuplicateChange | ReleaseChange | TranslationsChange;

ProposalChangeKindtype

type ProposalChangeKind = (typeof PROPOSAL_CHANGE_KINDS)[number];

ProposalOpinterface

interface ProposalOp

A single edit operation within a change. Mirrors DocEditOperation from core/ai-tools.ts, with after in place of value and an extra before that exists purely so a human reviewer can see what is being replaced.

Members (5)
op: 'set' | 'insert_item' | 'remove_item';
path: string;

Dotted path within the target object (no leading "fields." prefix).

index?: number;

Zero-based array index. Required for "remove_item", optional for "insert_item".

after?: any;

The value to write ("set") or insert ("insert_item").

before?: any;

The value as it stood when the proposal was written. Review-only: the applier ignores it unless verifyBefore is explicitly enabled.

ProposalOverlayclass

class ProposalOverlay

Indexes a change proposal so it can be applied cheaply to docs and translations as they are read.

Only the change kinds that affect rendering are handled: doc edits, doc creation, and translations. Release changes affect publishing rather than content, so they are ignored here and only take effect when the proposal is applied.

Members (9)
readonly proposal: Proposal;
applyToDoc(doc: Doc): Doc;

Applies any edit ops for doc.id to an unmarshaled doc.

hasNewDocsInCollection(collectionId: string): boolean;

True if the proposal creates any doc in collectionId.

countNewDocsInCollection(collectionId: string): number;

Number of docs the proposal creates in collectionId.

newDocsInCollection(collectionId: string, existingIds: Set<string>): Doc[];

Builds Doc objects for the docs the proposal creates in collectionId, skipping any that already exist in the db.

buildNewDoc(docId: string): Doc | null;

Synthesizes a Doc for a doc id the proposal creates, or null if the proposal does not create it. Duplicated docs are not resolved here (the source doc is not available synchronously), so they come back with empty fields.

newDocIds(): string[];

Every doc id the proposal creates, in proposal order.

allTranslations(): Record<string, Record<string, string>> | null;

All proposed translations, flattened to {source: {locale: string}}.

localeDocStrings(translationsId: string, locale: string): Record<string, {
    source: string;
    translation: string;
}> | null;

Returns proposed strings for one translations id and locale, shaped like the strings hash map stored on a translations locale doc, or null when the proposal has nothing for it.

ProposalParseErrorinterface

interface ProposalParseError

A structural or syntactic problem found while parsing a proposal.

Members (5)
line?: number;

1-based line number in the source YAML, when known.

path: string;

Dotted path to the offending value, e.g. "changes.0.ops.1.path".

message: string;
expected?: string;
received?: any;

publishScheduledDocsfunction

function publishScheduledDocs(rootConfig: RootConfig): Promise<any[]>;

Deprecated. Use RootCMSClient.publishScheduledDocs() instead.

Publishes scheduled docs.

redirectWithQueryfunction

function redirectWithQuery(req: Request, res: Response, redirectCode: number, redirectPath: string): void;

Issues an HTTP redirect, preserving any query params from the original req.

Releaseinterface

interface Release
Members (10)
id: string;
description?: string;
docIds?: string[];
dataSourceIds?: string[];
createdAt?: Timestamp;
createdBy?: string;
scheduledAt?: Timestamp;
scheduledBy?: string;
publishedAt?: Timestamp;
publishedBy?: string;

ReleaseChangeinterface

interface ReleaseChange

Creation or modification of a release. A release is just an object with description / docIds / dataSourceIds, so it uses the same op vocabulary as a doc.

Members (4)
kind: 'release.create' | 'release.update';
releaseId: string;
note?: string;
ops: ProposalOp[];

requireCmsLoginfunction

function requireCmsLogin(options?: RequireLoginOptions): RequestMiddleware;

Express middleware that enforces Root CMS login on a custom route, without requiring the ?preview=true query param.

The Root CMS plugin's auth middleware runs before any user middleware and populates req.user on every request that carries a valid session cookie (see core/plugin.ts). This guard simply requires that req.user to exist: authenticated requests fall through to the next handler, while unauthenticated ones are redirected to the CMS login page (or answered with a 401 for API / XHR requests).

It must be used in a project that installs the root-cms plugin, since that plugin is what resolves and attaches req.user.

Usage in root.config.ts — protect everything under /admin:

import {requireCmsLogin} from '@blinkk/root-cms';

export default defineConfig({ server: { middlewares: [requireCmsLogin({match: '/admin'})], }, });

Or, from a plugin with direct server access, scope it with express:

server.use('/admin', requireCmsLogin());

RequireLoginOptionsinterface

interface RequireLoginOptions
Members (2)
match?: string | string[] | ((req: Request) => boolean);

Restricts the guard to matching request paths. Accepts a path prefix (e.g. /admin), a list of prefixes, or a predicate (req) => boolean. When omitted, login is enforced for every request the middleware sees.

This is useful when adding the middleware to root.config.ts's server.middlewares, which are mounted app-wide with no path scope.

loginUrl?: string;

The login page to redirect unauthenticated users to. Defaults to /cms/login. The original URL is added as a continue query param so the user is returned to the requested page after signing in.

ResolvedChangeinterface

interface ResolvedChange

One resolved change, ready to be written (or reported by a dry run).

Members (5)
changeIndex: number;
kind: string;
target: string;
summary: string;

Short human-readable description of what will be written.

paths?: string[];

Field paths the change touches, for doc and release changes.

resolveDependencyGraphConfigfunction

function resolveDependencyGraphConfig(option?: boolean | CMSDependencyGraphConfig): CMSDependencyGraphConfig | null;

Normalizes the dependencyGraph cmsPlugin option. Returns the config object when the feature is enabled, or null when disabled.

resolveDependencyGraphFiltersfunction

function resolveDependencyGraphFilters(config?: CMSDependencyGraphConfig): ResolvedFilters;

resolveLocaleFallbacksfunction

function resolveLocaleFallbacks(i18nConfig: LocaleFallbacksI18nConfig | undefined, locale: string): string[];

Resolves the ordered locale fallback chain for a locale, starting with the locale itself and ending with the default locale. Fallback chains are followed recursively (breadth first) with cycle protection, and locale keys are matched case-insensitively while preserving the configured casing of each fallback value.

resolvePromisesMapfunction

function resolvePromisesMap(promisesMap: Record<string, Promise<any>>): Promise<Record<string, any>>;

RootCMSClientclass

class RootCMSClient
Members (56)
readonly rootConfig: RootConfig;
readonly cmsPlugin: CMSPlugin;
readonly projectId: string;
readonly app: App;
readonly db: Firestore;
readonly proposal?: ProposalOverlay;

An unapplied change proposal overlaid on top of everything this client reads. Lets a developer preview proposed content against a running site before accepting it. Reads only — writes never fold the proposal in.

overlayDoc<T = any>(doc: T): T;

Applies the proposal overlay (if any) to an unmarshaled doc.

Called from every non-raw read path. getRawDoc() and listDocs({raw: true}) deliberately skip this: those exist to feed setRawDoc(), and folding proposed content into them would leak it into a write.

proposedDocsInCollection(collectionId: string, existingIds: Set<string>): Doc<any>[];

Returns docs the proposal creates in a collection that do not exist in the database yet, so list and count reads can include them.

getDoc<Fields = any>(collectionId: string, slug: string, options: GetDocOptions): Promise<Doc<Fields> | null>;

Retrieves doc data from Root.js CMS.

getRawDoc(collectionId: string, slug: string, options: GetDocOptions): Promise<any | null>;

Retrieves raw doc data as stored in the database. Only use this if you know what you are doing.

dbCollectionDocsPath(collectionId: string, options: {
    mode: 'draft' | 'published';
}): string;

Firestore path for a collection.

dbDocPath(collectionId: string, slug: string, options: {
    mode: 'draft' | 'published';
}): string;

Firestore path for a content doc.

dbDocRef(collectionId: string, slug: string, options: {
    mode: 'draft' | 'published';
}): FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;

Firestore doc ref for a content doc.

getCollection(collectionId: string): Promise<Collection | null>;

Returns a collection's schema definition as defined in /collections/<id>.schema.ts.

saveDraftData(docId: string, fieldsData: any, options?: SaveDraftOptions): Promise<void>;

Saves draft data to a doc.

Note: this saves data to the "fields" attr of the draft doc. If you need to modify the sys-level attributes of the doc, use setRawDoc().

updateDraftData(docId: string, path: string, fieldValue: any, options?: UpdateDraftOptions): Promise<void>;

Updates a specific field path in a draft doc.

This allows partial updates to nested fields without replacing the entire document. For example: updateDraftData('Pages/home', 'hero.title', 'New Title')

setRawDoc(collectionId: string, slug: string, data: any, options: SetDocOptions): Promise<void>;

Sets the raw document data directly in Firestore.

CAUTION Prefer using saveDraftData('Pages/foo', data) in most cases. Only use this method if you need to manipulate system-level (sys) fields directly or if you're implementing low-level data operations.

## Validation & Normalization

This method automatically validates and normalizes sys fields to prevent data integrity issues that can cause runtime errors like "e.toMillis is not a function".

### Timestamp Fields (auto-converted) The following fields accept multiple formats and are automatically converted to Firestore Timestamp objects: - sys.createdAt - sys.modifiedAt - sys.publishedAt - sys.firstPublishedAt

Accepted formats: - Firestore Timestamp object (unchanged) - number - Interpreted as milliseconds since epoch, converted to Timestamp - Date object - Converted to Timestamp

### Required Fields (auto-populated with defaults if missing) - sys.createdAt - Defaults to current time if not provided - sys.modifiedAt - Defaults to current time if not provided - sys.createdBy - Defaults to 'root-cms-client' if not provided - sys.modifiedBy - Defaults to 'root-cms-client' if not provided - sys.locales - Defaults to ['en'] if not provided

### Optional Fields (validated if present) - sys.publishedBy - String identifier - sys.firstPublishedBy - String identifier - sys.publishingLocked - Object with optional until Timestamp

### Document Identity The id, collection, and slug fields are always set to match the provided parameters, overwriting any existing values to prevent data inconsistencies.

// Minimal example - sys fields use defaults
await client.setRawDoc('Pages', 'home', {
  sys: {},  // All sys fields will be auto-populated with defaults
  fields: {
    title: 'Home Page'
  }
}, { mode: 'draft' });

// Full example - with number timestamps (auto-converted)
await client.setRawDoc('Pages', 'home', {
  id: 'Pages/home',
  collection: 'Pages',
  slug: 'home',
  sys: {
    createdAt: Date.now(),  // Auto-converted to Timestamp.
    createdBy: 'user@example.com',
    modifiedAt: Date.now(),  // Auto-converted to Timestamp.
    modifiedBy: 'user@example.com',
    locales: ['en', 'es']
  },
  fields: {
    title: 'Home Page'
  }
}, { mode: 'draft' });
listDocs<T>(collectionId: string, options: ListDocsOptions): Promise<{
    docs: T[];
}>;

Lists docs from a Root.js CMS collection.

getDocsCount(collectionId: string, options: GetCountOptions): Promise<number>;

Returns the number of docs in a Root.js CMS collection.

publishDocs(docIds: string[], options?: {
    publishedBy: string;
    batch?: WriteBatch;
    releaseId?: string;
}): Promise<any[]>;

Batch publishes a set of docs by id.

unpublishDocs(docIds: string[], options?: {
    unpublishedBy?: string;
    batch?: WriteBatch;
}): Promise<any[]>;

Batch unpublishes a set of docs by id.

publishScheduledDocs(): Promise<any[]>;

Publishes scheduled docs.

dbReleasePath(releaseId: string): string;

Returns the Firestore path for a release doc.

getRelease(releaseId: string): Promise<Release | null>;

Retrieves a release by id, or null if it does not exist.

listReleases(): Promise<Release[]>;

Lists all releases, most recently created first.

setRelease(releaseId: string, release: Partial<Release>, options?: {
    modifiedBy?: string;
}): Promise<void>;

Creates or updates a release.

Only the fields describing the release's contents are written (description, docIds, dataSourceIds); scheduling and publishing state is left untouched, so this can never publish or schedule a release as a side effect.

publishScheduledReleases(): Promise<void>;

Publishes docs in scheduled releases.

syncScheduledDataSources(): Promise<void>;

Syncs data sources that have cron scheduling enabled and are due for sync.

testPublishingLocked(doc: Doc): boolean;

Checks if a doc is currently "locked" for publishing.

getTranslationsManager(): TranslationsManager;

Returns a TranslationsManager object for managing translations.

To get translations:

await tm.loadTranslations({
  ids: ['Global/strings', 'Pages/index'],
  locales: ['es'],
});

NOTE: The TranslationsManager is a v2 feature that will eventually replace the v1 translations system.

isV2TranslationsEnabled(): boolean;

Returns true if the v2 TranslationsManager is enabled via the experiments.v2TranslationsManager plugin config flag.

loadTranslations(options?: LoadTranslationsOptions): Promise<TranslationsMap>;

Loads translations saved in the translations collection, optionally filtered by tag.

Returns a map like:

{
  "<hash>": {"source": "Hello", "es": "Hola", "fr": "Bonjour"},
}
saveTranslations(translations: {
    [source: string]: {
        [locale: string]: string;
    };
}, tags?: string[]): Promise<void>;

Saves a map of translations, e.g.:

await client.saveTranslations({
  "Hello": {"es": "Hola", "fr": "Bonjour"},
});
verifyPassword(stored: PasswordHash | null | undefined, password: string): Promise<boolean>;

Verifies a candidate password against the hashed value stored by a password field. Returns false when the field is empty or malformed.

const doc = await cmsClient.getDoc('Members', 'alice', {mode: 'published'});
const ok = await cmsClient.verifyPassword(doc?.fields.password, input);
getTranslationKey(source: string): string;

Returns the "key" used for a translation as stored in the db. Translations are stored under Projects/<project id>/Translations/<sha1 hash>.

normalizeString(str: string): string;

Cleans a string that's used for translations. Performs the following: - Removes any leading/trailing whitespace - Removes spaces at the end of any line

loadTranslationsForLocale(locale: string, options?: LoadTranslationsOptions): Promise<LocaleTranslations>;

Loads translations for a particular locale.

The locale is expanded through the fallback chain configured in i18n.fallbacks (ending with i18n.defaultLocale), so a string missing a translation for the locale falls through to its fallbacks before the source string is used.

Returns a map like:

{
  "Hello": "Bonjour",
}
getDataSource(dataSourceId: string): Promise<DataSource | null>;

Returns a data source configuration object.

archiveDataSource(dataSourceId: string, options?: {
    archivedBy?: string;
}): Promise<void>;

Archives a data source. Archived data sources cannot be synced or published.

unarchiveDataSource(dataSourceId: string): Promise<void>;

Unarchives a data source.

syncDataSource(dataSourceId: string, options?: {
    syncedBy?: string;
}): Promise<void>;

Syncs a data source to draft state.

publishDataSource(dataSourceId: string, options?: {
    publishedBy?: string;
}): Promise<void>;
unpublishDataSource(dataSourceId: string): Promise<void>;

Unpublishes a data source. Removes the publishedAt/publishedBy metadata from the DataSource doc and deletes the Data/published doc.

publishDataSources(dataSourceIds: string[], options?: {
    publishedBy: string;
    batch?: WriteBatch;
    commitBatch?: boolean;
}): Promise<void>;
getFromDataSource<T = any>(dataSourceId: string, options?: {
    mode?: 'draft' | 'published';
}): Promise<DataSourceData<T> | null>;

Fetches data from a data source.

dbDataSourceDataPath(dataSourceId: string, options: {
    mode: 'draft' | 'published';
}): string;

Firestore path for a datasource data.

dbDataSourceDataRef(dataSourceId: string, options: {
    mode: 'draft' | 'published';
}): FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;

Firestore doc ref for a datasource data.

getUserAcl(email: string): Promise<{
    exists: boolean;
    role: UserRole | null;
}>;

Looks up the user's entry in the project's ACL with a single Firestore read. exists is true if the user (or their domain's wildcard entry, e.g. *@example.com) is in the ACL; role may be null for entries without an assigned role.

getUserRole(email: string): Promise<UserRole | null>;

Gets the user's role from the project's ACL.

userExistsInAcl(email: string): Promise<boolean>;

Verifies user exists in the ACL list.

listActions(options?: ListActionsOptions): Promise<Action[]>;

Lists action logs from the database.

logAction(action: string, options?: {
    by?: string;
    metadata?: any;
    links?: {
        label: string;
        url: string;
        target?: string;
    }[];
}): Promise<void>;
sendEmail(options: SendEmailOptions): Promise<string>;

Queues an email in the Projects/${projectId}/Emails collection in firestore, which is processed by the Root.js email service (apps/root-services). Returns the id of the queued email doc.

Delivery is asynchronous: the email service sends pending emails when its /_/send_emails endpoint is called (typically via a cron). Pass the emailService option to notify the service right away.

getDependencyGraph(options: {
    mode: DocMode;
}): Promise<DependencyGraph>;

Returns the dependency graph for a given mode, which tracks reference field usages between docs. Requires the dependencyGraph option to be enabled on the cmsPlugin config (the graph is kept up to date by the CMS cron job).

Example:

const graph = await cmsClient.getDependencyGraph({mode: 'published'});
const depIds = graph.getDependencies(['Pages/index']);
// => ['Authors/alice', 'BlogPosts/hello-world', ...]
getDocDependencies(docIds: string | string[], options: {
    mode: DocMode;
    transitive?: boolean;
}): Promise<string[]>;

Returns the ids of the docs referenced by the given doc(s), i.e. the additional docs that need to be fetched when fetching the given docs. Dependencies are resolved transitively by default (pass transitive: false for direct references only).

Requires the dependencyGraph option to be enabled on the cmsPlugin config.

Example:

const depIds = await cmsClient.getDocDependencies(['Pages/index'], {
  mode: 'published',
});
const req = cmsClient.createBatchRequest({mode: 'published'});
req.addDoc('Pages/index');
depIds.forEach((docId) => req.addDoc(docId));
const res = await req.fetch();
createBatchRequest(options: BatchRequestOptions): BatchRequest;

Creates a batch request that is capable of fetching one or more docs, corresponding translations, and dataSources.

RootCMSClientOptionsinterface

interface RootCMSClientOptions

Options for constructing a RootCMSClient.

Members (1)
proposal?: Proposal;

An unapplied change proposal to overlay on top of every read. Use this to preview proposed content on a running site before accepting it.

RootCMSDocinterface

interface RootCMSDoc<Fields = any>
Members (5)
id: string;

The id of the doc, e.g. "Pages/foo-bar".

collection: string;

The collection id of the doc, e.g. "Pages".

slug: string;

The slug of the doc, e.g. "foo-bar".

sys: {
    createdAt: number;
    createdBy: string;
    modifiedAt: number;
    modifiedBy: string;
    firstPublishedAt?: number;
    firstPublishedBy?: string;
    publishedAt?: number;
    publishedBy?: string;
    locales?: string[];
    sortKey?: string;
};

System-level metadata.

fields?: Fields;

User-entered field values from the CMS.

Routeinterface

interface Route
Members (3)
handle: (req: RouteRequest, res: RouteResponse) => Promise<void>;

SSR handler.

getStaticProps?: GetStaticProps;

SSG handler props handler, enabled with {ssg: true}.

getStaticPaths?: GetStaticPaths;

SSG path params provider, enabled with {ssg: true}.

RouteContextinterface

interface RouteContext
Members (5)
req?: RouteRequest;

HTTP request object. Only available in SSR mode.

slug: string;

The slug of the page being requested.

mode: DocMode;

Doc publishing mode.

cmsClient: RootCMSClient;

Client for interacting with Root CMS data.

params: RouteParams;

URL param map from filesystem routing.

RouteRequesttype

type RouteRequest = Request & {
    rootConfig: RootConfig;
    cmsClient: RootCMSClient;
};

RouteResponsetype

type RouteResponse = Response;

SaveDraftOptionsinterface

interface SaveDraftOptions
Members (3)
locales?: string[];

Locales to enable.

modifiedBy?: string;

Email of user modifying the doc. If blank, defaults to root-cms-client.

validate?: boolean;

Whether to validate fieldsData against the collection schema before saving. If validation fails, an error will be thrown with details about the validation errors.

schemanamespace

namespace schema
Exports (58)
function array(field: Omit<ArrayField, 'type'>): ArrayField;
type ArrayField = CommonFieldProps & {
    type: 'array';
    default?: any[];
    itemDefault?: Record<string, any>;
    preview?: string | string[];
    of: ObjectLikeField;
    buttonLabel?: string;
    defaultOpen?: boolean;
};
type AspectRatio = string | number;

An aspect ratio, expressed either as a string like '16:9', '16/9' or '16x9', or as a number like 1.7778 (width divided by height).

function boolean(field: Omit<BooleanField, 'type'>): BooleanField;
type BooleanField = CommonFieldProps & {
    type: 'boolean';
    default?: boolean;
    checkboxLabel?: string;
};
const collection: typeof defineCollection;
type Collection = SchemaWithTypes & {
    id: string;
    group?: string;
    domain?: string;
    url?: string;
    previewUrl?: string;
    preview?: {
        title?: string | string[];
        image?: string | string[];
        defaultImage?: {
            src: string;
        };
        fit?: 'cover' | 'contain';
    };
    slugRegex?: string;
    autoSlug?: string;
    autolock?: boolean;
    autolockReason?: string;
    sortOptions?: Array<{
        id: string;
        label: string;
        field: string;
        direction?: 'asc' | 'desc';
    }>;
    customSorting?: boolean;
    viewOptions?: {
        compact?: boolean;
    };
    publishing?: CollectionPublishingOptions;
};
interface CollectionPublishingOptions

Publishing options configured on a collection.

interface CommonFieldProps
function date(field: Omit<DateField, 'type'>): DateField;
type DateField = CommonFieldProps & {
    type: 'date';
    default?: string;
};
function datetime(field: Omit<DateTimeField, 'type'>): DateTimeField;
type DateTimeField = CommonFieldProps & {
    type: 'datetime';
    default?: string;
    timezone?: string;
};
const define: typeof defineSchema;

Defines the schema for a collection or reusable component.

function defineCollection(collection: Omit<Collection, 'id'>): Omit<Collection, 'id'>;
function definePreset<T = Record<string, any>>(preset: SchemaPreset<T>): SchemaPreset<T>;

Defines a preset for use within schema.define({presets: [...]}). The optional generic parameter T constrains the shape of the preset's data field. Pass an auto-generated fields type to enable type-safety:

import type {HeroFields} from './generated/types.d.ts';

schema.definePreset<HeroFields>({
  id: 'big',
  label: 'Big hero',
  data: {title: 'Welcome'},
});
function defineSchema(schema: Schema): Schema;
interface DocumentSource

Sources the selectable values for a field from a field within another CMS document.

Use this when a set of allowed values is managed centrally in the CMS (e.g. a list of feature flags defined in a global module) and you want other fields to pick from that list instead of typing free-form strings.

Example: source a flag select's options from the name of each item in the flags array of the GlobalModules/flags document.

schema.select({
  id: 'flag',
  source: {
    doc: 'GlobalModules/flags',
    field: 'flags',
    valueKey: 'name',
    helpKey: 'description',
  },
  creatable: true,
});
type Field = StringField | NumberField | PasswordField | DateField | DateTimeField | BooleanField | SelectField | MultiSelectField | ImageField | FileField | ObjectField | ArrayField | OneOfField | RichTextField | ReferenceField | ReferencesField;
type FieldValueSource = DocumentSource;

The source of selectable values for a field. Currently only a CMS document source is supported; other source types may be added in the future.

type FieldWithId = Field;

Similar to Field but with a required id. TODO(stevenle): fix this.

function file(field: Omit<FileField, 'type'>): FileField;
type FileField = CommonFieldProps & {
    type: 'file';
    exts?: string[];
    preserveFilename?: boolean;
    cacheControl?: string;
    alt?: boolean;
    aspectRatio?: AspectRatio | AspectRatio[];
};
function glob(pattern: string, options?: GlobOptions): SchemaPattern;

Creates a schema pattern that is resolved at project load time.

This is the recommended way to reference multiple schemas in a oneOf field, especially for self-referencing schemas like containers. The pattern is resolved after all schemas are loaded, completely avoiding circular import issues.

// Simple usage - include all templates:
export default schema.define({
  name: 'Container',
  fields: [
    schema.array({
      id: 'children',
      of: schema.oneOf({
        types: schema.glob('/templates/*\/*.schema.ts'),
      }),
    }),
  ],
});

// With exclusions:
schema.oneOf({
  types: schema.glob('/templates/*\/*.schema.ts', {
    exclude: ['DeprecatedTemplate'],
  }),
});

// With field omissions (useful for nested contexts):
schema.oneOf({
  types: schema.glob('/blocks/*\/*.schema.ts', {
    omitFields: ['id'],
  }),
});
interface GlobOptions

Options for schema.glob().

function image(field: Omit<ImageField, 'type'>): ImageField;
type ImageField = CommonFieldProps & {
    type: 'image';
    translate?: boolean;
    exts?: string[];
    cacheControl?: string;
    alt?: boolean;
    aspectRatio?: AspectRatio | AspectRatio[];
};
function multiselect(field: Omit<MultiSelectField, 'type'>): MultiSelectField;
type MultiSelectField = Omit<SelectField, 'type'> & {
    type: 'multiselect';
};
function number(field: Omit<NumberField, 'type'>): NumberField;
type NumberField = CommonFieldProps & {
    type: 'number';
    default?: number;
};
function object(field: Omit<ObjectField, 'type'>): ObjectField;
type ObjectField = CommonFieldProps & {
    type: 'object';
    fields: FieldWithId[];
    variant?: 'drawer' | 'inline';
    drawerOptions?: {
        collapsed?: boolean;
        inline?: boolean;
    };
};
type ObjectLikeField = ImageField | FileField | ObjectField | OneOfField | ReferenceField;
function oneOf(field: Omit<OneOfField, 'type'>): OneOfField;
type OneOfField = CommonFieldProps & {
    type: 'oneof';
    variant?: 'dropdown' | 'picker';
    types: Schema[] | string[] | Array<Schema | string> | SchemaPattern;
};
function password(field: Omit<PasswordField, 'type'>): PasswordField;
type PasswordField = CommonFieldProps & {
    type: 'password';
    default?: never;
    minLength?: number;
    iterations?: number;
};

A field for securely storing a password.

The password is hashed in the CMS UI before it is saved, so the plain text value is never written to the db. The stored value is a PasswordHash containing the algorithm, salt and hash. Use verifyPassword() from @blinkk/root-cms (or RootCMSClient.verifyPassword()) to check whether a candidate password matches.

schema.password({id: 'password', label: 'Password'});

// Server-side:
const ok = await cmsClient.verifyPassword(doc.fields.password, input);
type PasswordFieldValue = PasswordHash;

The value stored in the db for a PasswordField.

interface PasswordHash

The value stored in the db for a password field. The salt and the derived hash are stored base64-encoded alongside the parameters used to compute them, so the algorithm or its cost can change without invalidating existing hashes.

type PasswordHashAlgorithm = 'pbkdf2-sha256';

The hashing algorithms supported by hashPassword.

const preset: typeof definePreset;

Alias for definePreset.

interface PublishCheckConfig

A check configured to run as part of a collection's publishing flow.

type PublishCheckLevel = 'required' | 'warning';

Severity level for a check configured on a collection.

- required: an error result halts publishing. - warning: publishing continues, and the message is surfaced afterwards.

function reference(field: Omit<ReferenceField, 'type'>): ReferenceField;
type ReferenceField = CommonFieldProps & {
    type: 'reference';
    collections?: string[];
    initialCollection?: string;
    buttonLabel?: string;
};
function references(field: Omit<ReferencesField, 'type'>): ReferencesField;
type ReferencesField = CommonFieldProps & {
    type: 'references';
    collections?: string[];
    initialCollection?: string;
    buttonLabel?: string;
};
function richtext(field: Omit<RichTextField, 'type'>): RichTextField;
type RichTextField = CommonFieldProps & {
    type: 'richtext';
    translate?: boolean;
    placeholder?: string;
    autosize?: boolean;
    blockComponents?: Schema[];
    inlineComponents?: Schema[];
    paragraphSizes?: Array<RichTextParagraphSizeOption | string>;
};
interface Schema
interface SchemaPattern

A schema pattern that is resolved at project load time. This allows schemas to reference other schemas by glob pattern without causing circular import issues, since resolution is deferred until all schemas are loaded.

interface SchemaPreset<T = Record<string, any>>
type SchemaWithTypes = Schema & {
    types?: Record<string, Schema>;
};
function select(field: Omit<SelectField, 'type'>): SelectField;
type SelectField = CommonFieldProps & {
    type: 'select';
    default?: string;
    options?: Array<{
        value: string;
        label?: string;
    }> | string[];
    source?: FieldValueSource;
    creatable?: boolean;
    translate?: boolean;
    searchable?: boolean;
};
function string(field: Omit<StringField, 'type'>): StringField;
type StringField = CommonFieldProps & {
    type: 'string';
    default?: string;
    translate?: boolean;
    variant?: 'input' | 'textarea' | 'json';
    maxRows?: number;
    autosize?: boolean;
};

SendEmailOptionsinterface

interface SendEmailOptions

Options for RootCMSClient.sendEmail().

Emails are queued in the Projects/${projectId}/Emails collection in firestore and delivered by the Root.js email service (apps/root-services), which sends pending emails using the App Engine Mail API.

Members (7)
to: string | string[];

Recipient email address(es).

from?: string;

Sender email address. The sender must be authorized to send email via the App Engine Mail API, e.g. noreply@<gcp-project-id>.appspotmail.com. Defaults to noreply@<gcp-project-id>.appspotmail.com.

subject: string;

Subject line.

body?: string;

Plain-text body. When omitted, a plain-text body is derived from htmlBody.

htmlBody?: string;

Optional HTML body.

expiresAt?: Date;

Optional expiration date. If the email is still unsent when the email service processes the queue after this date (e.g. the service was down), the email is marked as expired and skipped instead of being delivered late.

emailService?: string | boolean;

Email service used to trigger delivery immediately after the email is queued. Setting this to true uses the default hosted service at https://services.rootjs.dev. Set to a base URL to use a self-hosted deployment of the service (apps/root-services). When unset, the email remains queued until the email service's cron next processes the queue.

serializeProposalfunction

function serializeProposal(proposal: Proposal): string;

Serializes a proposal back to YAML.

Deterministic by construction: keys are emitted in a fixed order, and every string is quoted or written as a block scalar so it can never be re-read as a boolean, number, or date. parse -> serialize -> parse is a fixed point.

SetDocOptionsinterface

interface SetDocOptions
Members (1)
mode: DocMode;

Mode, either "draft" or "published".

SingleLocaleTranslationsMapinterface

interface SingleLocaleTranslationsMap

A translations map containing translations for a single locale.

Example:

{
  "one": "uno",
  "two": "dos"
}

SourceStringtype

type SourceString = string;

toArrayObjectfunction

function toArrayObject(arr: any[]): ArrayObject;

Serializes an array into an ArrayObject, e.g.:

marshalArray([1, 2, 3])
// => {a: 1, b: 2, c: 3, _array: ['a', 'b', 'c']}

This database storage method makes it easier to update a single field in a deeply nested array object.

toDocEditOperationsfunction

function toDocEditOperations(ops: ProposalOp[]): Array<{
    op: string;
    path: string;
    value?: any;
    index?: number;
}>;

Maps a change's ops onto the DocEditOperation shape that applyDocEdits() consumes, dropping the review-only before values.

TranslatedStringtype

type TranslatedString = string;

Translationinterface

interface Translation
Members (1)
source: string;

TranslationsChangeinterface

interface TranslationsChange

Translation changes for one translations doc id.

Members (4)
kind: 'translations';
translationsId: string;
note?: string;
entries: TranslationsEntry[];

TranslationsDocModetype

type TranslationsDocMode = 'draft' | 'published';

TranslationsEntryinterface

interface TranslationsEntry

A single source string and its proposed translations, keyed by locale.

Members (2)
source: string;
locales: Record<string, {
    before?: string | null;
    after: string;
}>;

translationsForLocalefunction

function translationsForLocale(translationsMap: TranslationsMap, locale: string | string[]): LocaleTranslations;

Converts a translations map from loadTranslations() to a map of source to translated string for a particular locale.

The locale is either a single locale (e.g. "fr"), which falls back to en and then the source string, or an ordered fallback chain (e.g. ["en-CA", "en-GB", "en"]) where the first locale with a translation wins before falling back to the source string. Use resolveLocaleFallbacks() to build the chain from the i18n.fallbacks config:

const fallbackLocales = resolveLocaleFallbacks(rootConfig.i18n, 'en-CA');
const translations = translationsForLocale(translationsMap, fallbackLocales);

Returns a map like:

{
  "Hello": "Bonjour",
}

translationsForLocaleV2function

function translationsForLocaleV2(multiLocaleStrings: MultiLocaleTranslationsMap, fallbackLocales: Locale[]): SingleLocaleTranslationsMap;

Converts a multi-locale translations map to a flat single-locale map using a locale fallback chain. For each source string, the first locale in the chain with a non-empty translation wins; if no locale matches, the source string is returned.

const multiLocaleStrings = {
  'one': {'en-GB': 'one!', es: 'uno'},
  'two': {es: 'dos'}
};
translationsForLocaleV2(multiLocaleStrings, ['en-CA', 'en-GB', 'en']);
// =>
// {
//   "one": "one!",
//   "two": "two",
// }

TranslationsLinkedSheetinterface

interface TranslationsLinkedSheet
Members (4)
spreadsheetId: string;
gid: number;
linkedAt: Timestamp;
linkedBy: string;

TranslationsLocaleDocinterface

interface TranslationsLocaleDoc

The TranslationsLocaleDoc is the internal doc type stored in the DB. For a translations doc, the translations for each locale is stored in a separate doc represented by this type.

This type is not meant to be used by external callers since this is primarily an internal implementation detail.

Members (5)
id: string;

Translations id. In most cases, this is the same as the doc id, e.g. Pages/foo--bar.

locale: string;
tags: string[];
strings: TranslationsLocaleDocHashMap;
sys: {
    modifiedAt: Timestamp;
    modifiedBy: string;
    publishedAt?: Timestamp;
    publishedBy?: string;
    linkedSheet?: TranslationsLinkedSheet;
};

TranslationsLocaleDocEntryinterface

interface TranslationsLocaleDocEntry
Members (2)
source: SourceString;
translation: TranslatedString;

TranslationsLocaleDocHashMapinterface

interface TranslationsLocaleDocHashMap

TranslationsLocaleDocWithRefinterface

interface TranslationsLocaleDocWithRef

A translations locale doc paired with its Firestore doc ref.

Members (2)
ref: DocumentReference;
data: TranslationsLocaleDoc;

TranslationsManagerclass

class TranslationsManager
Members (9)
cmsClient: RootCMSClient;
saveTranslations(id: string, strings: MultiLocaleTranslationsMap, options?: {
    tags?: string[];
    modifiedBy?: string;
    linkedSheet?: TranslationsLinkedSheet;
}): Promise<void>;

Saves draft translations for a translations doc id.

Example:

const strings = {
  'one': {es: 'uno', fr: 'un'},
  'two': {es: 'dos', fr: 'deux'},
};
await tm.saveTranslations('Pages/index', strings);
publishTranslations(id: string, options?: {
    batch?: WriteBatch;
    publishedBy?: string;
}): Promise<void>;

Publishes a translations doc.

publishTranslationsBulk(ids: string[], options?: {
    publishedBy?: string;
}): Promise<{
    publishedIds: string[];
}>;

Publishes multiple translations docs by id, e.g.:

await tm.publishTranslationsBulk(['Pages/index', 'common']);
addPublishTranslationsOps(localeDocs: TranslationsLocaleDocWithRef[], batch: WriteBatch, options?: {
    publishedBy?: string;
}): number;

Adds the write ops for publishing a set of draft translations locale docs to a batch. For each locale doc, the draft doc's sys is updated with publishedAt/By and a copy is saved to the published collection. Returns the number of ops added to the batch (2 per locale doc).

getTranslationsLocaleDocs(ids: string[], mode: TranslationsDocMode): Promise<Record<string, TranslationsLocaleDocWithRef[]>>;

Fetches the translations locale docs (with their doc refs) for a set of translations doc ids, grouped by id.

loadTranslations(options?: {
    ids?: string[];
    tags?: string[];
    locales?: Locale[];
    mode?: TranslationsDocMode;
}): Promise<MultiLocaleTranslationsMap>;

Fetches translations from one or more translations docs in the translations manager.

Example:

await tm.loadTranslations();
// =>
// {
//   "one": {"es": "uno", "fr": "un"},
//   "two": {"es": "dos", "fr": "deux"}
// }

To load a specific set of translations docs by id:

const translationsToLoad = ['Global/strings', 'Global/header', 'Global/footer', 'Pages/index'];
await tm.loadTranslations({ids: translationsToLoad});
// =>
// {
//   "one": {"es": "uno", "fr": "un"},
//   "two": {"es": "dos", "fr": "deux"}
// }

To load a subset of locales (more performant):

await tm.loadTranslations({locales: ['es']});
// =>
// {
//   "one": {"es": "uno"},
//   "two": {"es": "dos"}
// }
loadTranslationsForLocale(locale: Locale, options?: {
    mode?: TranslationsDocMode;
    fallbackLocales?: Locale[];
}): Promise<SingleLocaleTranslationsMap>;

Fetches translations for a given locale, with optional fallbacks. The return value is a map of source string to translated string.

If no fallbackLocales are provided, the fallback chain is resolved from the project's i18n.fallbacks config.

Example:

await translationsDoc.loadTranslationsForLocale('es');
// =>
// {
//   "one": "uno",
//   "two": "dos",
// }
importTranslationsFromV1(): Promise<ImportTranslationsFromV1Result>;

Imports translations from the v1 system to the TranslationsManager.

Each v1 string is grouped into a v2 translations doc per tag (e.g. a string tagged Pages/index is saved to the Pages/index translations doc). Untagged strings are grouped into a v1-untagged doc so that nothing is dropped. The imported translations are saved as drafts; use publishTranslationsBulk() to publish them.

TranslationsMapinterface

interface TranslationsMap

TranslationsMigrationStateinterface

interface TranslationsMigrationState
Members (7)
version: number;
status: TranslationsMigrationStatus;
startedAt?: Timestamp;
startedBy?: string;
finishedAt?: Timestamp;
error?: string;
stats?: {
    numStrings: number;
    numDocs: number;
};

TranslationsMigrationStatustype

type TranslationsMigrationStatus = 'running' | 'complete' | 'error';

unmarshalArrayfunction

function unmarshalArray(arrObject: ArrayObject): any[];

Converts an ArrayObject to a normal array.

unmarshalDatafunction

function unmarshalData(data: any): any;

Walks the data tree and converts any Timestamp objects to millis and any _array maps to normal arrays.

E.g.:

normalizeData({ sys: {modifiedAt: Timestamp(123)}, fields: { _array: ['asdf'], asdf: {title: 'hello'} } }) // => {sys: {modifiedAt: 123}, fields: {foo: [{title: 'hello'}]}}

UpdateDraftOptionsinterface

interface UpdateDraftOptions
Members (1)
validate?: boolean;

Whether to validate the updated field against the collection schema. If validation fails, an error will be thrown with details about the validation errors.

UserRoletype

type UserRole = 'ADMIN' | 'EDITOR' | 'CONTRIBUTOR' | 'VIEWER';

verifyPasswordfunction

function verifyPassword(stored: PasswordHash | null | undefined, password: string): Promise<boolean>;

Verifies a candidate password against a stored PasswordHash.

Returns false (never throws) when the stored value is missing or malformed, so callers can pass a field value straight from a doc.

const ok = await verifyPassword(doc.fields.password, req.body.password);

@blinkk/root-cms/browser-client

import {…} from '@blinkk/root-cms/browser-client';

AIEventstype

type AIEvents = Pick<EmbedWindowEvents, 'ready' | 'error' | 'close'>;

Events emitted by EmbeddedAI.

EditorEventstype

type EditorEvents = EmbedWindowEvents;

Events emitted by EmbeddedEditor.

EmbeddedAIclass

class EmbeddedAI

Handle to a headless Root AI chat opened via RootCMSBrowserClient.openRootAI.

Members (6)
get element(): HTMLIFrameElement | null;

The iframe element (iframe mode only).

get window(): Window | null;

The pop-up window (popup mode only).

on<K extends keyof AIEvents>(type: K, cb: (payload: AIEvents[K]) => void): () => void;

Subscribes to an event. Returns an unsubscribe function.

reload(): void;

Reloads the embedded chat (starts a fresh chat).

close(): void;

Closes the embedded chat.

closeAndReload(): void;

Closes the embedded chat and reloads the host page.

EmbeddedEditorclass

class EmbeddedEditor

Handle to a headless doc editor opened via RootCMSBrowserClient.openEditor.

Note: the embedded editor does not autosave; closing it discards any unsaved changes.

Members (8)
readonly docId: string;

The doc being edited, e.g. Pages/about.

get element(): HTMLIFrameElement | null;

The iframe element (iframe mode only).

get window(): Window | null;

The pop-up window (popup mode only).

on<K extends keyof EditorEvents>(type: K, cb: (payload: EditorEvents[K]) => void): () => void;

Subscribes to an editor event. Returns an unsubscribe function.

focusField(deepKey: string): void;

Scrolls the editor to (focuses) a field, e.g. hero.title.

reload(): void;

Reloads the embedded editor.

close(): void;

Closes the embedded editor, discarding any unsaved changes.

closeAndReload(): void;

Closes the embedded editor and reloads the host page.

EmbedWindowEventsinterface

interface EmbedWindowEvents

Events emitted by an embedded CMS window (iframe or pop-up).

Members (5)
ready: {
    docId?: string;
};

The embedded page finished loading.

saved: {
    docId?: string;
    saveState?: SaveState;
};

The doc was saved.

published: {
    docId?: string;
    publishedAt?: number;
};

The doc was published.

error: {
    error?: string;
};

An error occurred (e.g. the pop-up was blocked).

close: void;

The embedded window was closed (via close() or by the user).

HighlightNodeMessageinterface

interface HighlightNodeMessage

Requests that the preview page highlight the node associated with a field. A null deepKey clears all highlights.

Members (1)
highlightNode: {
    deepKey: string | null;
    options?: {
        scroll: boolean;
    };
};

NavigateToDocMessageinterface

interface NavigateToDocMessage

Requests that the doc editor switch to another document.

Posted by a page rendered inside the CMS preview pane to say which doc it is showing, so the editor can follow the preview as the user navigates the site. The page knows its own doc id, which is why the CMS doesn't try to derive one from the url.

The doc id is untrusted input: the CMS checks the collection exists and the slug is well-formed before routing anywhere.

Members (1)
navigateToDoc: {
    docId: string;
    confirm?: boolean;
};

OpenEditorOptionsinterface

interface OpenEditorOptions
Members (5)
mode?: 'popup' | 'iframe';

How to open the editor. Defaults to popup.

container?: HTMLElement;

Container the iframe is appended to. Required when mode is iframe.

deeplink?: string;

Deep key of a field to scroll to on load, e.g. hero.title.

width?: number;

Pop-up window width. Defaults to 480.

height?: number;

Pop-up window height. Defaults to 720.

OpenRootAIOptionsinterface

interface OpenRootAIOptions extends Omit<OpenEditorOptions, 'deeplink'>
Members (1)
docId?: string;

Doc to provide as context to the AI chat, e.g. Pages/about.

PreviewConnectionclass

class PreviewConnection

Channel between a site rendered inside the in-CMS preview pane and the doc editor that frames it. The preview iframe is same-origin with the CMS, so no configuration is needed. Safe to construct unconditionally: outside the preview pane the connection is inert (isEmbedded is false).

Members (5)
readonly isEmbedded: boolean;

Whether this page is rendered inside the in-CMS preview pane.

focusField(deepKey: string): void;

Requests that the doc editor scroll to (focus) a field, e.g. hero.title. Used to implement "click to edit".

navigateToDoc(docId: string, options?: {
    confirm?: boolean;
}): void;

Tells the doc editor to switch to a document, so the editor follows the preview as the user navigates the site.

Call it with the page's own doc id when the page loads. isEmbedded is false outside the CMS preview pane, so the call is a no-op on the live site. The CMS only acts on it when the plugin's preview.channel enables messages from the preview.

The editor asks the user before switching by default, since a doc changing on its own is disorienting for someone who doesn't know the page can do that. Pass {confirm: false} to switch straight away, for sites that have their own affordance for it (a toggle the user turned on, say).

Usage:

const preview = new PreviewConnection();
if (preview.isEmbedded) {
  preview.navigateToDoc(doc.id);
}
on(type: 'highlight', cb: (event: PreviewHighlightEvent) => void): () => void;

Subscribes to field highlight requests from the doc editor (sent when the user hovers/focuses a field). Returns an unsubscribe function.

disconnect(): void;

Removes the message listener and all subscriptions.

PreviewHighlightEventinterface

interface PreviewHighlightEvent

Event emitted when the doc editor requests a field highlight.

Members (2)
deepKey: string | null;

Deep key of the field to highlight, or null to clear highlights.

scroll: boolean;

Whether to scroll the highlighted node into view.

RootCMSBrowserClientclass

class RootCMSBrowserClient

Client for embedding Root CMS into another site.

Members (5)
readonly cmsOrigin: string;

Normalized origin of the Root CMS server.

openEditor(docId: string, options?: OpenEditorOptions): EmbeddedEditor;

Opens the headless doc editor for a doc (e.g. Pages/about) in a pop-up or iframe. In popup mode, call from a user gesture (e.g. a click handler) to avoid pop-up blockers.

The slug portion of the docId is normalized the same way the CMS normalizes slugs, so a URL-like path also works, e.g. Pages/about/foo -> Pages/about--foo.

openRootAI(options?: OpenRootAIOptions): EmbeddedAI;

Opens the headless Root AI chat in a pop-up or iframe, optionally with a doc as context. In popup mode, call from a user gesture (e.g. a click handler) to avoid pop-up blockers.

static connectPreview(): PreviewConnection;

Connects to the doc editor from a site rendered inside the in-CMS preview pane, enabling "click to edit" (PreviewConnection.focusField) and field highlighting (on('highlight', ...)).

static isInPreviewIframe(): boolean;

Returns whether this page is rendered inside the in-CMS preview pane.

The parent's location is the reliable signal: the preview iframe is same-origin, so the CMS path is readable. The referrer only names the CMS for the first page the pane loads -- once the user follows a link inside the preview it names the previous page of the site instead -- so it is kept as a fallback for the cases where the parent can't be read.

RootCMSBrowserClientOptionsinterface

interface RootCMSBrowserClientOptions
Members (1)
cmsOrigin: string;

Origin of the Root CMS server, e.g. https://cms.example.com.

RootEmbedMessageinterface

interface RootEmbedMessage

Lifecycle messages posted from the headless (embedded) pages (the doc editor and the Root AI panel) to the parent window that frames them. All messages are namespaced under root to avoid colliding with the un-namespaced {scrollToDeeplink} / {highlightNode} messages used by the in-CMS preview channel.

Members (1)
root: {
    type: 'ready' | 'saved' | 'published' | 'error';
    docId?: string;
    saveState?: SaveState;
    publishedAt?: number;
    error?: string;
};

SaveStateenum

enum SaveState {
    NO_CHANGES = "NO_CHANGES",
    UPDATES_PENDING = "UPDATE_PENDING",
    SAVING = "SAVING",
    SAVED = "SAVED",
    ERROR = "ERROR"
}

Save state of the draft doc.

ScrollToDeeplinkMessageinterface

interface ScrollToDeeplinkMessage

Requests that the doc editor scroll to (focus) a specific field.

Members (1)
scrollToDeeplink: {
    deepKey: string;
};

@blinkk/root-cms/client

import {…} from '@blinkk/root-cms/client';

Actioninterface

interface Action<T = any>
Members (5)
action: string;

The name of the action.

by?: string;

The user's email that performed the action (or "system").

timestamp: Timestamp;

Timestamp when the action occurred.

metadata?: T;

Metadata for the action.

links?: {
    label: string;
    url: string;
    target?: string;
}[];

Optional list of quick links to display in the UI.

ArrayObjectinterface

interface ArrayObject
Members (1)
_array: string[];

BatchRequestclass

class BatchRequest
Members (6)
cmsClient: RootCMSClient;
addDoc(docId: string): void;

Adds a doc to the batch request.

addDataSource(dataSourceId: string): void;

Adds a data source to the batch request.

addQuery(queryId: string, collectionId: string, queryOptions?: BatchRequestQueryOptions): void;

Adds a collection-based query to the batch request.

addTranslations(translationsId: string): void;

Adds a translations doc to the request.

fetch(): Promise<BatchResponse>;

Fetches data from the DB.

BatchRequestOptionsinterface

interface BatchRequestOptions
Members (3)
mode: 'draft' | 'published';
translate?: boolean;

Whether to automatically fetch translations for the docs retrieved in the request (including docs returned by queries).

locales?: string[];

Locales to fetch translations for. Each locale is expanded through its fallback chain (per the i18n.fallbacks config). Defaults to the locales configured in i18n.locales.

BatchRequestQueryinterface

interface BatchRequestQuery
Members (3)
queryId: string;
collectionId: string;
queryOptions?: BatchRequestQueryOptions;

BatchRequestQueryOptionsinterface

interface BatchRequestQueryOptions
Members (5)
offset?: number;
limit?: number;
orderBy?: string;
orderByDirection?: 'asc' | 'desc';
query?: (query: Query) => Query;

BatchResponseclass

class BatchResponse
Members (5)
docs: Record<string, Doc>;
queries: Record<string, Doc[]>;
dataSources: Record<string, DataSourceData>;
translations: Record<string, Record<string, TranslationsLocaleDoc>>;

Translations locale docs retrieved in the request, keyed by translations id then by locale. The ids are in precedence order: generic translations (e.g. "common") first, doc-specific translations last.

getTranslations(locale: string | string[]): LocaleTranslations;

Returns a map of translations for a given locale or locale fallbacks.

The input is either a single locale (e.g. "de"), which is expanded through the fallback chain configured in i18n.fallbacks, or an array of locales representing an explicit fallback chain, e.g. ["en-CA", "en-GB", "en"].

The returned value is a flat map of source string to translated string, e.g.: {"<source>": "<translation>"}

CronScheduleTypetype

type CronScheduleType = 'interval' | 'daily' | 'weekly' | 'custom';

Data source sync schedule type.

- interval: sync every N minutes/hours/days (uses interval + unit). - daily / weekly / custom: sync on a specific cron schedule (uses expression + timezone). daily and weekly are UI presets that are stored as standard cron expressions; custom allows an arbitrary expression.

CronUnittype

type CronUnit = 'minutes' | 'hours' | 'days';

DataSourceinterface

interface DataSource
Members (15)
id: string;
description?: string;
type: 'http' | 'gsheet';
url: string;
dataFormat?: GsheetDataFormat;

Currently only used by gsheet. map returns the sheet as an array of objects keyed by the header row, grid returns the sheet as an array of arrays of strings (including the header row).

httpOptions?: {
    method: HttpMethod;
    headers?: Record<string, string>;
    body?: string;
};

Options for HTTP requests.

cron?: DataSourceCron;
createdAt: Timestamp;
createdBy: string;
syncedAt?: Timestamp;
syncedBy?: string;
publishedAt?: Timestamp;
publishedBy?: string;
archivedAt?: Timestamp;
archivedBy?: string;

DataSourceCroninterface

interface DataSourceCron
Members (7)
enabled: boolean;
schedule?: CronScheduleType;

Scheduling mode. Defaults to interval when unset (for backwards compatibility with data sources created before specific schedules were supported).

interval?: number;

Interval value, used when schedule is interval.

unit?: CronUnit;

Interval unit, used when schedule is interval.

expression?: string;

Standard 5-field cron expression, used when schedule is daily, weekly, or custom, e.g. 0 19 * * * for "every day at 7pm".

timezone?: string;

IANA timezone used to evaluate expression, e.g. America/New_York. Defaults to UTC when unset.

autoPublish?: boolean;

DataSourceDatainterface

interface DataSourceData<T = any>
Members (3)
dataSource: DataSource;
data: T;
headers?: string[];

Optional list of column headers (for gsheet sources).

DataSourceGridRowinterface

interface DataSourceGridRow

A single row of grid data as stored in Firestore. Rows are wrapped in a map because Firestore doesn't support arrays nested directly within arrays.

Members (1)
cells: string[];

DataSourceModetype

type DataSourceMode = 'draft' | 'published';

Docinterface

interface Doc<Fields = any>
Members (5)
id: string;

The id of the doc, e.g. "Pages/foo-bar".

collection: string;

The collection id of the doc, e.g. "Pages".

slug: string;

The slug of the doc, e.g. "foo-bar".

sys: {
    createdAt: number;
    createdBy: string;
    modifiedAt: number;
    modifiedBy: string;
    firstPublishedAt?: number;
    firstPublishedBy?: string;
    publishedAt?: number;
    publishedBy?: string;
    publishingLocked: {
        lockedAt: string;
        lockedBy: string;
        reason: string;
        until?: Timestamp;
    };
    locales?: string[];
    assets?: string[];
    sortKey?: string;
};
fields: Fields;

DocModetype

type DocMode = 'draft' | 'published';

getCmsPluginfunction

function getCmsPlugin(rootConfig: RootConfig): CMSPlugin;

GetCountOptionsinterface

interface GetCountOptions
Members (2)
mode: DocMode;
query?: (query: Query) => Query;

GetDocOptionsinterface

interface GetDocOptions
Members (1)
mode: DocMode;

Mode, either "draft" or "published".

GsheetDataFormattype

type GsheetDataFormat = 'map' | 'grid';

Format used to store data synced from a csv-style data source.

- map: an array of objects keyed by the sheet's header row, e.g. [{header1: 'foo', header2: 'bar'}]. - grid: an array of arrays of strings, including the header row, e.g. [['header1', 'header2'], ['foo', 'bar']].

hashPasswordfunction

function hashPassword(password: string, options?: HashPasswordOptions): Promise<PasswordHash>;

Hashes a password with a freshly generated random salt.

const stored = await hashPassword('hunter2');
// => {algorithm: 'pbkdf2-sha256', iterations: 600000, salt: '...', hash: '...'}

HashPasswordOptionsinterface

interface HashPasswordOptions
Members (1)
iterations?: number;

The number of PBKDF2 iterations. Defaults to DEFAULT_ITERATIONS. Higher values are slower to compute and slower to brute force.

HttpMethodtype

type HttpMethod = 'GET' | 'POST';

isPasswordHashfunction

function isPasswordHash(value: unknown): value is PasswordHash;

Returns whether a value has the shape of a PasswordHash.

isRichTextDatafunction

function isRichTextData(data: any): boolean;

Returns true if the data is a rich text data object.

ListActionsOptionsinterface

interface ListActionsOptions
Members (3)
action?: string;

Filter by a specific action. Defaults to all actions.

by?: string;

Filter by a specific user. Defaults to all users.

limit?: number;

Max number of actions to return. Defaults to 100.

ListDocsOptionsinterface

interface ListDocsOptions
Members (7)
mode: DocMode;
offset?: number;
limit?: number;
orderBy?: string;

DB field path to order results by, e.g. sys.createdAt. Defaults to slug. The default is skipped when a query fn is provided, since the query fn may supply its own ordering.

orderByDirection?: 'asc' | 'desc';
query?: (query: Query) => Query;
raw?: boolean;

Whether to fetch the "raw" version of the doc (for use in conjunction with setRawDoc()).

LoadTranslationsOptionsinterface

interface LoadTranslationsOptions
Members (1)
tags?: string[];

LocaleFallbacksI18nConfiginterface

interface LocaleFallbacksI18nConfig

Utilities for resolving the locale fallback chain used by translations.

The i18n.fallbacks config in root.config.ts maps a locale to an ordered list of fallback locales. When a translation is missing for a locale, each fallback locale is checked (in order) before falling back to i18n.defaultLocale and finally the source string. For example:

i18n: {
  locales: ['en', 'en-GB', 'en-CA'],
  fallbacks: {
    'en-CA': ['en-GB'],
  },
}

With the config above, resolveLocaleFallbacks(i18n, 'en-CA') returns ['en-CA', 'en-GB', 'en']. Fallbacks are resolved recursively (breadth first), and all matching is case-insensitive.

Members (3)
locales?: string[];
defaultLocale?: string;
fallbacks?: Record<string, string[]>;

LocaleTranslationsinterface

interface LocaleTranslations

marshalArrayvariable

const marshalArray: typeof toArrayObject;

marshalDatafunction

function marshalData(data: any): any;

Walks the data tree and converts any array of objects into "array objects" for storage in firestore.

normalizeDatafunction

function normalizeData(data: any): any;

Deprecated. Use `unmarshalData()` instead.

parseDocIdfunction

function parseDocId(docId: string): {
    collection: string;
    slug: string;
};

Parses a docId (e.g. Pages/foo) and returns the collection and slug.

PasswordHashinterface

interface PasswordHash

The value stored in the db for a password field. The salt and the derived hash are stored base64-encoded alongside the parameters used to compute them, so the algorithm or its cost can change without invalidating existing hashes.

Members (4)
algorithm: PasswordHashAlgorithm;

The key derivation algorithm used, e.g. pbkdf2-sha256.

iterations: number;

The number of PBKDF2 iterations used to derive the hash.

salt: string;

The random salt used to derive the hash, base64-encoded.

hash: string;

The derived hash, base64-encoded.

PasswordHashAlgorithmtype

type PasswordHashAlgorithm = 'pbkdf2-sha256';

The hashing algorithms supported by hashPassword.

ProposalOverlayclass

class ProposalOverlay

Indexes a change proposal so it can be applied cheaply to docs and translations as they are read.

Only the change kinds that affect rendering are handled: doc edits, doc creation, and translations. Release changes affect publishing rather than content, so they are ignored here and only take effect when the proposal is applied.

Members (9)
readonly proposal: Proposal;
applyToDoc(doc: Doc): Doc;

Applies any edit ops for doc.id to an unmarshaled doc.

hasNewDocsInCollection(collectionId: string): boolean;

True if the proposal creates any doc in collectionId.

countNewDocsInCollection(collectionId: string): number;

Number of docs the proposal creates in collectionId.

newDocsInCollection(collectionId: string, existingIds: Set<string>): Doc[];

Builds Doc objects for the docs the proposal creates in collectionId, skipping any that already exist in the db.

buildNewDoc(docId: string): Doc | null;

Synthesizes a Doc for a doc id the proposal creates, or null if the proposal does not create it. Duplicated docs are not resolved here (the source doc is not available synchronously), so they come back with empty fields.

newDocIds(): string[];

Every doc id the proposal creates, in proposal order.

allTranslations(): Record<string, Record<string, string>> | null;

All proposed translations, flattened to {source: {locale: string}}.

localeDocStrings(translationsId: string, locale: string): Record<string, {
    source: string;
    translation: string;
}> | null;

Returns proposed strings for one translations id and locale, shaped like the strings hash map stored on a translations locale doc, or null when the proposal has nothing for it.

Releaseinterface

interface Release
Members (10)
id: string;
description?: string;
docIds?: string[];
dataSourceIds?: string[];
createdAt?: Timestamp;
createdBy?: string;
scheduledAt?: Timestamp;
scheduledBy?: string;
publishedAt?: Timestamp;
publishedBy?: string;

resolveLocaleFallbacksfunction

function resolveLocaleFallbacks(i18nConfig: LocaleFallbacksI18nConfig | undefined, locale: string): string[];

Resolves the ordered locale fallback chain for a locale, starting with the locale itself and ending with the default locale. Fallback chains are followed recursively (breadth first) with cycle protection, and locale keys are matched case-insensitively while preserving the configured casing of each fallback value.

RootCMSClientclass

class RootCMSClient
Members (56)
readonly rootConfig: RootConfig;
readonly cmsPlugin: CMSPlugin;
readonly projectId: string;
readonly app: App;
readonly db: Firestore;
readonly proposal?: ProposalOverlay;

An unapplied change proposal overlaid on top of everything this client reads. Lets a developer preview proposed content against a running site before accepting it. Reads only — writes never fold the proposal in.

overlayDoc<T = any>(doc: T): T;

Applies the proposal overlay (if any) to an unmarshaled doc.

Called from every non-raw read path. getRawDoc() and listDocs({raw: true}) deliberately skip this: those exist to feed setRawDoc(), and folding proposed content into them would leak it into a write.

proposedDocsInCollection(collectionId: string, existingIds: Set<string>): Doc<any>[];

Returns docs the proposal creates in a collection that do not exist in the database yet, so list and count reads can include them.

getDoc<Fields = any>(collectionId: string, slug: string, options: GetDocOptions): Promise<Doc<Fields> | null>;

Retrieves doc data from Root.js CMS.

getRawDoc(collectionId: string, slug: string, options: GetDocOptions): Promise<any | null>;

Retrieves raw doc data as stored in the database. Only use this if you know what you are doing.

dbCollectionDocsPath(collectionId: string, options: {
    mode: 'draft' | 'published';
}): string;

Firestore path for a collection.

dbDocPath(collectionId: string, slug: string, options: {
    mode: 'draft' | 'published';
}): string;

Firestore path for a content doc.

dbDocRef(collectionId: string, slug: string, options: {
    mode: 'draft' | 'published';
}): FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;

Firestore doc ref for a content doc.

getCollection(collectionId: string): Promise<Collection | null>;

Returns a collection's schema definition as defined in /collections/<id>.schema.ts.

saveDraftData(docId: string, fieldsData: any, options?: SaveDraftOptions): Promise<void>;

Saves draft data to a doc.

Note: this saves data to the "fields" attr of the draft doc. If you need to modify the sys-level attributes of the doc, use setRawDoc().

updateDraftData(docId: string, path: string, fieldValue: any, options?: UpdateDraftOptions): Promise<void>;

Updates a specific field path in a draft doc.

This allows partial updates to nested fields without replacing the entire document. For example: updateDraftData('Pages/home', 'hero.title', 'New Title')

setRawDoc(collectionId: string, slug: string, data: any, options: SetDocOptions): Promise<void>;

Sets the raw document data directly in Firestore.

CAUTION Prefer using saveDraftData('Pages/foo', data) in most cases. Only use this method if you need to manipulate system-level (sys) fields directly or if you're implementing low-level data operations.

## Validation & Normalization

This method automatically validates and normalizes sys fields to prevent data integrity issues that can cause runtime errors like "e.toMillis is not a function".

### Timestamp Fields (auto-converted) The following fields accept multiple formats and are automatically converted to Firestore Timestamp objects: - sys.createdAt - sys.modifiedAt - sys.publishedAt - sys.firstPublishedAt

Accepted formats: - Firestore Timestamp object (unchanged) - number - Interpreted as milliseconds since epoch, converted to Timestamp - Date object - Converted to Timestamp

### Required Fields (auto-populated with defaults if missing) - sys.createdAt - Defaults to current time if not provided - sys.modifiedAt - Defaults to current time if not provided - sys.createdBy - Defaults to 'root-cms-client' if not provided - sys.modifiedBy - Defaults to 'root-cms-client' if not provided - sys.locales - Defaults to ['en'] if not provided

### Optional Fields (validated if present) - sys.publishedBy - String identifier - sys.firstPublishedBy - String identifier - sys.publishingLocked - Object with optional until Timestamp

### Document Identity The id, collection, and slug fields are always set to match the provided parameters, overwriting any existing values to prevent data inconsistencies.

// Minimal example - sys fields use defaults
await client.setRawDoc('Pages', 'home', {
  sys: {},  // All sys fields will be auto-populated with defaults
  fields: {
    title: 'Home Page'
  }
}, { mode: 'draft' });

// Full example - with number timestamps (auto-converted)
await client.setRawDoc('Pages', 'home', {
  id: 'Pages/home',
  collection: 'Pages',
  slug: 'home',
  sys: {
    createdAt: Date.now(),  // Auto-converted to Timestamp.
    createdBy: 'user@example.com',
    modifiedAt: Date.now(),  // Auto-converted to Timestamp.
    modifiedBy: 'user@example.com',
    locales: ['en', 'es']
  },
  fields: {
    title: 'Home Page'
  }
}, { mode: 'draft' });
listDocs<T>(collectionId: string, options: ListDocsOptions): Promise<{
    docs: T[];
}>;

Lists docs from a Root.js CMS collection.

getDocsCount(collectionId: string, options: GetCountOptions): Promise<number>;

Returns the number of docs in a Root.js CMS collection.

publishDocs(docIds: string[], options?: {
    publishedBy: string;
    batch?: WriteBatch;
    releaseId?: string;
}): Promise<any[]>;

Batch publishes a set of docs by id.

unpublishDocs(docIds: string[], options?: {
    unpublishedBy?: string;
    batch?: WriteBatch;
}): Promise<any[]>;

Batch unpublishes a set of docs by id.

publishScheduledDocs(): Promise<any[]>;

Publishes scheduled docs.

dbReleasePath(releaseId: string): string;

Returns the Firestore path for a release doc.

getRelease(releaseId: string): Promise<Release | null>;

Retrieves a release by id, or null if it does not exist.

listReleases(): Promise<Release[]>;

Lists all releases, most recently created first.

setRelease(releaseId: string, release: Partial<Release>, options?: {
    modifiedBy?: string;
}): Promise<void>;

Creates or updates a release.

Only the fields describing the release's contents are written (description, docIds, dataSourceIds); scheduling and publishing state is left untouched, so this can never publish or schedule a release as a side effect.

publishScheduledReleases(): Promise<void>;

Publishes docs in scheduled releases.

syncScheduledDataSources(): Promise<void>;

Syncs data sources that have cron scheduling enabled and are due for sync.

testPublishingLocked(doc: Doc): boolean;

Checks if a doc is currently "locked" for publishing.

getTranslationsManager(): TranslationsManager;

Returns a TranslationsManager object for managing translations.

To get translations:

await tm.loadTranslations({
  ids: ['Global/strings', 'Pages/index'],
  locales: ['es'],
});

NOTE: The TranslationsManager is a v2 feature that will eventually replace the v1 translations system.

isV2TranslationsEnabled(): boolean;

Returns true if the v2 TranslationsManager is enabled via the experiments.v2TranslationsManager plugin config flag.

loadTranslations(options?: LoadTranslationsOptions): Promise<TranslationsMap>;

Loads translations saved in the translations collection, optionally filtered by tag.

Returns a map like:

{
  "<hash>": {"source": "Hello", "es": "Hola", "fr": "Bonjour"},
}
saveTranslations(translations: {
    [source: string]: {
        [locale: string]: string;
    };
}, tags?: string[]): Promise<void>;

Saves a map of translations, e.g.:

await client.saveTranslations({
  "Hello": {"es": "Hola", "fr": "Bonjour"},
});
verifyPassword(stored: PasswordHash | null | undefined, password: string): Promise<boolean>;

Verifies a candidate password against the hashed value stored by a password field. Returns false when the field is empty or malformed.

const doc = await cmsClient.getDoc('Members', 'alice', {mode: 'published'});
const ok = await cmsClient.verifyPassword(doc?.fields.password, input);
getTranslationKey(source: string): string;

Returns the "key" used for a translation as stored in the db. Translations are stored under Projects/<project id>/Translations/<sha1 hash>.

normalizeString(str: string): string;

Cleans a string that's used for translations. Performs the following: - Removes any leading/trailing whitespace - Removes spaces at the end of any line

loadTranslationsForLocale(locale: string, options?: LoadTranslationsOptions): Promise<LocaleTranslations>;

Loads translations for a particular locale.

The locale is expanded through the fallback chain configured in i18n.fallbacks (ending with i18n.defaultLocale), so a string missing a translation for the locale falls through to its fallbacks before the source string is used.

Returns a map like:

{
  "Hello": "Bonjour",
}
getDataSource(dataSourceId: string): Promise<DataSource | null>;

Returns a data source configuration object.

archiveDataSource(dataSourceId: string, options?: {
    archivedBy?: string;
}): Promise<void>;

Archives a data source. Archived data sources cannot be synced or published.

unarchiveDataSource(dataSourceId: string): Promise<void>;

Unarchives a data source.

syncDataSource(dataSourceId: string, options?: {
    syncedBy?: string;
}): Promise<void>;

Syncs a data source to draft state.

publishDataSource(dataSourceId: string, options?: {
    publishedBy?: string;
}): Promise<void>;
unpublishDataSource(dataSourceId: string): Promise<void>;

Unpublishes a data source. Removes the publishedAt/publishedBy metadata from the DataSource doc and deletes the Data/published doc.

publishDataSources(dataSourceIds: string[], options?: {
    publishedBy: string;
    batch?: WriteBatch;
    commitBatch?: boolean;
}): Promise<void>;
getFromDataSource<T = any>(dataSourceId: string, options?: {
    mode?: 'draft' | 'published';
}): Promise<DataSourceData<T> | null>;

Fetches data from a data source.

dbDataSourceDataPath(dataSourceId: string, options: {
    mode: 'draft' | 'published';
}): string;

Firestore path for a datasource data.

dbDataSourceDataRef(dataSourceId: string, options: {
    mode: 'draft' | 'published';
}): FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;

Firestore doc ref for a datasource data.

getUserAcl(email: string): Promise<{
    exists: boolean;
    role: UserRole | null;
}>;

Looks up the user's entry in the project's ACL with a single Firestore read. exists is true if the user (or their domain's wildcard entry, e.g. *@example.com) is in the ACL; role may be null for entries without an assigned role.

getUserRole(email: string): Promise<UserRole | null>;

Gets the user's role from the project's ACL.

userExistsInAcl(email: string): Promise<boolean>;

Verifies user exists in the ACL list.

listActions(options?: ListActionsOptions): Promise<Action[]>;

Lists action logs from the database.

logAction(action: string, options?: {
    by?: string;
    metadata?: any;
    links?: {
        label: string;
        url: string;
        target?: string;
    }[];
}): Promise<void>;
sendEmail(options: SendEmailOptions): Promise<string>;

Queues an email in the Projects/${projectId}/Emails collection in firestore, which is processed by the Root.js email service (apps/root-services). Returns the id of the queued email doc.

Delivery is asynchronous: the email service sends pending emails when its /_/send_emails endpoint is called (typically via a cron). Pass the emailService option to notify the service right away.

getDependencyGraph(options: {
    mode: DocMode;
}): Promise<DependencyGraph>;

Returns the dependency graph for a given mode, which tracks reference field usages between docs. Requires the dependencyGraph option to be enabled on the cmsPlugin config (the graph is kept up to date by the CMS cron job).

Example:

const graph = await cmsClient.getDependencyGraph({mode: 'published'});
const depIds = graph.getDependencies(['Pages/index']);
// => ['Authors/alice', 'BlogPosts/hello-world', ...]
getDocDependencies(docIds: string | string[], options: {
    mode: DocMode;
    transitive?: boolean;
}): Promise<string[]>;

Returns the ids of the docs referenced by the given doc(s), i.e. the additional docs that need to be fetched when fetching the given docs. Dependencies are resolved transitively by default (pass transitive: false for direct references only).

Requires the dependencyGraph option to be enabled on the cmsPlugin config.

Example:

const depIds = await cmsClient.getDocDependencies(['Pages/index'], {
  mode: 'published',
});
const req = cmsClient.createBatchRequest({mode: 'published'});
req.addDoc('Pages/index');
depIds.forEach((docId) => req.addDoc(docId));
const res = await req.fetch();
createBatchRequest(options: BatchRequestOptions): BatchRequest;

Creates a batch request that is capable of fetching one or more docs, corresponding translations, and dataSources.

RootCMSClientOptionsinterface

interface RootCMSClientOptions

Options for constructing a RootCMSClient.

Members (1)
proposal?: Proposal;

An unapplied change proposal to overlay on top of every read. Use this to preview proposed content on a running site before accepting it.

SaveDraftOptionsinterface

interface SaveDraftOptions
Members (3)
locales?: string[];

Locales to enable.

modifiedBy?: string;

Email of user modifying the doc. If blank, defaults to root-cms-client.

validate?: boolean;

Whether to validate fieldsData against the collection schema before saving. If validation fails, an error will be thrown with details about the validation errors.

SendEmailOptionsinterface

interface SendEmailOptions

Options for RootCMSClient.sendEmail().

Emails are queued in the Projects/${projectId}/Emails collection in firestore and delivered by the Root.js email service (apps/root-services), which sends pending emails using the App Engine Mail API.

Members (7)
to: string | string[];

Recipient email address(es).

from?: string;

Sender email address. The sender must be authorized to send email via the App Engine Mail API, e.g. noreply@<gcp-project-id>.appspotmail.com. Defaults to noreply@<gcp-project-id>.appspotmail.com.

subject: string;

Subject line.

body?: string;

Plain-text body. When omitted, a plain-text body is derived from htmlBody.

htmlBody?: string;

Optional HTML body.

expiresAt?: Date;

Optional expiration date. If the email is still unsent when the email service processes the queue after this date (e.g. the service was down), the email is marked as expired and skipped instead of being delivered late.

emailService?: string | boolean;

Email service used to trigger delivery immediately after the email is queued. Setting this to true uses the default hosted service at https://services.rootjs.dev. Set to a base URL to use a self-hosted deployment of the service (apps/root-services). When unset, the email remains queued until the email service's cron next processes the queue.

SetDocOptionsinterface

interface SetDocOptions
Members (1)
mode: DocMode;

Mode, either "draft" or "published".

toArrayObjectfunction

function toArrayObject(arr: any[]): ArrayObject;

Serializes an array into an ArrayObject, e.g.:

marshalArray([1, 2, 3])
// => {a: 1, b: 2, c: 3, _array: ['a', 'b', 'c']}

This database storage method makes it easier to update a single field in a deeply nested array object.

Translationinterface

interface Translation
Members (1)
source: string;

translationsForLocalefunction

function translationsForLocale(translationsMap: TranslationsMap, locale: string | string[]): LocaleTranslations;

Converts a translations map from loadTranslations() to a map of source to translated string for a particular locale.

The locale is either a single locale (e.g. "fr"), which falls back to en and then the source string, or an ordered fallback chain (e.g. ["en-CA", "en-GB", "en"]) where the first locale with a translation wins before falling back to the source string. Use resolveLocaleFallbacks() to build the chain from the i18n.fallbacks config:

const fallbackLocales = resolveLocaleFallbacks(rootConfig.i18n, 'en-CA');
const translations = translationsForLocale(translationsMap, fallbackLocales);

Returns a map like:

{
  "Hello": "Bonjour",
}

TranslationsMapinterface

interface TranslationsMap

unmarshalArrayfunction

function unmarshalArray(arrObject: ArrayObject): any[];

Converts an ArrayObject to a normal array.

unmarshalDatafunction

function unmarshalData(data: any): any;

Walks the data tree and converts any Timestamp objects to millis and any _array maps to normal arrays.

E.g.:

normalizeData({ sys: {modifiedAt: Timestamp(123)}, fields: { _array: ['asdf'], asdf: {title: 'hello'} } }) // => {sys: {modifiedAt: 123}, fields: {foo: [{title: 'hello'}]}}

UpdateDraftOptionsinterface

interface UpdateDraftOptions
Members (1)
validate?: boolean;

Whether to validate the updated field against the collection schema. If validation fails, an error will be thrown with details about the validation errors.

UserRoletype

type UserRole = 'ADMIN' | 'EDITOR' | 'CONTRIBUTOR' | 'VIEWER';

verifyPasswordfunction

function verifyPassword(stored: PasswordHash | null | undefined, password: string): Promise<boolean>;

Verifies a candidate password against a stored PasswordHash.

Returns false (never throws) when the stored value is missing or malformed, so callers can pass a field value straight from a doc.

const ok = await verifyPassword(doc.fields.password, req.body.password);

@blinkk/root-cms/functions

import {…} from '@blinkk/root-cms/functions';

cronfunction

function cron(options?: CronOptions): import("firebase-functions/scheduler").ScheduleFunction;

Runs offline CMS tasks, such as publishing of scheduled docs.

CronOptionsinterface

interface CronOptions
Members (2)
rootDir?: string;
scheduleOptions?: Partial<ScheduleOptions>;

@blinkk/root-cms/plugin

import {…} from '@blinkk/root-cms/plugin';

AiConfiginterface

interface AiConfig

Full AI config registered on the cms plugin.

Members (6)
models: AiModelConfig[];

Models exposed in the model picker. The first entry is the default.

defaultModel?: string;

Id of the default model. Defaults to the first model in models.

imageModels?: AiModelConfig[];

Image generation models. Used by the image generator and any other features that produce images. Supported providers are openai (e.g. gpt-image-*), and google or google-vertex with a Gemini image model (e.g. gemini-3-pro-image-preview). Imagen models are not supported.

defaultImageModel?: string;

Id of the default image model. Defaults to the first entry in imageModels.

systemPrompt?: string;

Optional system prompt prepended to every conversation. If a ROOT.md file exists at the project root, its contents are appended to this prompt automatically.

maxSteps?: number;

Maximum tool-loop steps before stopping. Defaults to 10.

AiModelConfiginterface

interface AiModelConfig

Configuration for a single chat model.

Inspired by Ollama's Modelfile and LiteLLM's model config: each entry maps a CMS-facing id to a provider, model id and credentials.

Members (11)
id: string;

Stable id used by the CMS UI and stored alongside chat history.

label?: string;

Optional human-readable label rendered in the model picker.

description?: string;

Optional description shown under the label.

provider: AiProvider;

AI provider/family to route requests to.

modelId?: string;

Provider-specific model id (e.g. gpt-4o, claude-opus-4-5, gemini-2.5-pro). Defaults to id if omitted.

apiKey?: string;

API key for the provider. For google-vertex this is an optional express-mode API key; omit it to authenticate with Google Cloud Application Default Credentials instead.

baseURL?: string;

Override the provider's base URL (required for openai-compatible).

headers?: Record<string, string>;

Custom headers to send with each request.

project?: string;

Google Cloud project id (google-vertex only). Defaults to the CMS's firebaseConfig.projectId, then to the Application Default Credentials project.

location?: string;

Vertex AI location (google-vertex only), e.g. us-central1. Defaults to global.

capabilities?: AiModelCapabilities;

Capabilities advertised to the UI.

AiProvidertype

type AiProvider = 'openai' | 'openai-compatible' | 'anthropic' | 'google' | 'google-vertex';

Provider type for an AI model. Use openai-compatible for any OpenAI-style endpoint (e.g. local Ollama, vLLM, OpenRouter). Use google-vertex to call Gemini through Vertex AI with Google Cloud credentials instead of a Gemini API key.

buildCommentEmailTemplateDatafunction

function buildCommentEmailTemplateData(action: Action, ctx: NotificationServiceContext, options?: Pick<CommentEmailNotificationsOptions, 'cmsUrl'>): CommentEmailTemplateData | null;

Builds the template data for a comment action. Returns null when the action isn't a field comment action or is missing required metadata.

CheckContextinterface

interface CheckContext

Context passed to a check function during execution.

Members (6)
rootConfig: RootConfig;

The Root.js config.

cmsClient: RootCMSClient;

The Root CMS client for accessing the database.

docId: string;

The document ID, e.g. Pages/index.

collectionId: string;

The collection ID, e.g. Pages.

slug: string;

The slug, e.g. index.

collectionSchema: schema.Collection | null;

The collection schema.

CheckResultinterface

interface CheckResult

Result returned by a check function after execution.

Members (3)
status: CheckStatus;

Whether the check succeeded, warned, or failed.

message: string;

A message describing the result. Supports markdown.

metadata?: Record<string, any>;

Optional metadata to include with the result.

CheckStatustype

type CheckStatus = 'success' | 'warning' | 'error';

The result status of a check.

CMSAIConfiginterface

interface CMSAIConfig

Deprecated. The `experiments.ai` flag is now used only as a sidebar toggle. Configure chat models on the top-level `ai` plugin option instead.

Members (2)
endpoint?: string;

Custom API endpoint for chat prompts.

model?: string;

Gen AI model to use.

CMSBuiltInSidebarTooltype

type CMSBuiltInSidebarTool = 'home' | 'content' | 'releases' | 'data' | 'assets' | 'translations' | 'ai' | 'settings';

Built-in sidebar tools that can be toggled on/off in the CMS UI.

CMSCheckinterface

interface CMSCheck

Configuration for defining a CMS check.

Members (5)
id: string;

Unique ID for the check.

label: string;

Human-readable label displayed in the UI.

description?: string;

Optional description explaining what the check does.

collections?: string[];

Optional list of collection IDs to restrict this check to. When set, the check is only shown and runnable for documents belonging to one of the listed collections. When omitted, the check applies to all collections.

run: (ctx: CheckContext) => Promise<CheckResult>;

Function that runs the check on the server-side and returns a result.

CMSDependencyGraphConfiginterface

interface CMSDependencyGraphConfig

Configuration options for the dependency graph. Pass true (or an empty object) to the dependencyGraph plugin option to enable the feature with default options.

Members (2)
includeCollections?: string[];

Collections to track in the dependency graph. If specified, only docs in these collections are scanned for outgoing references. If unset, all collections are scanned (subject to excludeCollections).

Note that these filters only control which docs are scanned for outgoing references — referenced doc ids are always recorded as-is, even when the referenced doc lives in a collection that is not scanned.

excludeCollections?: string[];

Collections to exclude from the dependency graph. Applied after includeCollections.

CMSNotificationServiceinterface

interface CMSNotificationService extends CMSService

Configuration for defining a CMS notification service.

Notification services react to actions in the CMS (publishes, schema changes, comments, etc.) and dispatch them to an external channel. Initially this is intended for email, with Slack, webhooks, and other transports planned. See emailNotifications() for a built-in email implementation backed by the Root.js email service.

Multiple notification services may be registered; each independently decides whether and how to handle a given action.

Example:

cmsPlugin({
  services: {
    notifications: [
      {
        id: 'sendgrid',
        label: 'SendGrid',
        onAction: async (ctx, action) => {
          if (action.action === 'doc.publish') {
            await sendgrid.send({ ... });
          }
        },
      },
    ],
  },
});
Members (1)
onAction?: (ctx: NotificationServiceContext, action: Action) => Promise<void | NotificationResult>;

Async function called when an action occurs in the CMS. Receives the action and may dispatch a notification (e.g. send email, post to Slack) via the service's underlying transport. Can optionally return a NotificationResult describing delivery status.

cmsPluginfunction

function cmsPlugin(options: CMSPluginOptions): CMSPlugin;

CMSPlugintype

type CMSPlugin = Plugin & {
    name: 'root-cms';
    getConfig: () => CMSPluginOptions;
    getFirebaseApp: () => App;
    getFirestore: (options?: {
        databaseId?: string;
    }) => Firestore;
};

CMSPluginOptionstype

type CMSPluginOptions = {
    id?: string;
    name?: string;
    firebaseConfig: {
        [key: string]: string | undefined;
        apiKey: string;
        authDomain: string;
        projectId: string;
        storageBucket: string;
        databaseId?: string;
    };
    gapi?: {
        apiKey: string;
        clientId: string;
    };
    cookieSecret?: string | string[];
    isUserAuthorized?: (req: Request, user: CMSUser) => boolean | Promise<boolean>;
    isLoginRequired?: (req: Request) => boolean;
    allowedIframeOrigins?: string[];
    gci?: string | boolean;
    sidebar?: {
        tools?: Record<string, CMSSidebarTool>;
        hiddenBuiltInTools?: CMSBuiltInSidebarTool[];
    };
    favicon?: string;
    minimalBranding?: boolean;
    themes?: CMSTheme[];
    defaultTheme?: string;
    onAction?: (action: Action) => any;
    ai?: AiConfig;
    experiments?: {
        ai?: boolean | CMSAIConfig;
        v2TranslationsManager?: boolean;
        taskManager?: boolean;
    };
    logLevel?: 'info' | 'warn' | 'error' | 'silent' | 'debug';
    watch?: boolean;
    preview?: {
        channel: boolean | 'to-preview' | 'from-preview';
    };
    checks?: CMSCheck[];
    translations?: CMSTranslationService[];
    notifications?: CMSNotificationService[];
    searchIndex?: CMSSearchIndexConfig;
    dependencyGraph?: boolean | CMSDependencyGraphConfig;
    defaultOneOfVariant?: 'dropdown' | 'picker';
    excludeLocalesFromTranslations?: string[];
};

CMSSearchIndexConfiginterface

interface CMSSearchIndexConfig

Filters applied to the spotlight / global search indexer. By default every collection and every doc is indexed. Use these options to scope the index to a subset of the project's content.

Doc ids use the <collectionId>/<slug> format (e.g. Pages/about), which matches the format used elsewhere in the CMS.

Members (4)
includeCollections?: string[];

Collections to include in the search index. If specified, only these collections are indexed. If unset, all collections are indexed (subject to excludeCollections).

excludeCollections?: string[];

Collections to exclude from the search index. Applied after includeCollections.

includeDocIds?: string[];

Doc ids to include in the search index. If specified, only these docs are indexed (within collections that pass the collection filter). Format: <collectionId>/<slug>.

excludeDocIds?: string[];

Doc ids to exclude from the search index. Applied after includeDocIds. Format: <collectionId>/<slug>.

CMSServiceinterface

interface CMSService

Base shape for a service registered with the CMS plugin.

Services are extension points that let plugins provide capabilities (e.g. email delivery, cache, translations) that root-cms can call into. Every service shares the same identifying fields — id, label, optional icon — and is registered via the services option on cmsPlugin().

Concrete service interfaces (e.g. CMSEmailService, CMSTranslationService) extend this base and add the handler functions specific to their capability.

Members (3)
id: string;

Unique ID for the service (e.g. 'sendgrid', 'crowdin').

label: string;

Human-readable label displayed in the UI (e.g. "SendGrid").

icon?: string;

Optional icon URL displayed in the UI next to the label. Similar to the sidebar tools icon option, this should be a URL to an image.

CMSServiceContextinterface

interface CMSServiceContext

Base context passed to service handler functions. Concrete services may extend this with additional fields specific to the call site (e.g. the translation context adds docId, collectionId, etc.).

Members (3)
rootConfig: RootConfig;

The Root.js config.

cmsClient: RootCMSClient;

The Root CMS client for accessing the database.

user?: {
    email: string;
};

Email of the user that triggered the action, if any. Some service calls are initiated by the system rather than a user, in which case this is undefined.

CMSSidebarToolinterface

interface CMSSidebarTool
Members (5)
icon?: string;

Sidebar icon URL. For consistency with other icons used by Root CMS, we recommend picking an icon from https://tabler.io/icons with the stroke weight set to "1.5". Paste the "Data URI" or "Base64 Data URI" from the icon's download modal.

label?: string;

Label.

iframeUrl?: string;

Iframe URL to render for the tool.

cmsUrl?: string;

CMS URL that should be opened when the tool is selected. The url must start with /cms/. Use this to create shortcuts to CMS pages instead of rendering the tool inside an iframe.

externalUrl?: string;

External URL that should open in a new tab when the tool is selected.

CMSThemeinterface

interface CMSTheme

A theme for the CMS UI: a stylesheet loaded after the CMS's own, styling whatever the CSS reaches. Themes are registered by the project (a plugin or a package can export one); each user chooses which of them to use.

Members (4)
id: string;

Identifies the theme in config, in the URL and in a user's preference.

name?: string;

Shown in the theme picker. Defaults to the id.

file?: string;

Path to a CSS file, relative to the project root (root.config.ts).

css?: string;

CSS appended after the file, or the whole theme on its own.

CMSTranslationServiceinterface

interface CMSTranslationService extends CMSService

Configuration for defining a CMS translation service.

Members (2)
onImport?: (ctx: TranslationServiceContext, data: TranslationRow[]) => Promise<TranslationRow[] | TranslationImportResult>;

Async function to import translations from the service. Should return an array of translation rows that will be merged into the CMS translations database, or a TranslationImportResult object to display a notification without importing any rows.

onExport?: (ctx: TranslationServiceContext, data: TranslationRow[]) => Promise<void | TranslationExportResult>;

Async function to export translations to the service. Receives the current translation rows for the document. Can optionally return an object with a message to display in the success notification.

CMSUserinterface

interface CMSUser
Members (2)
email: string;
role?: UserRole | null;

The user's role within the project's ACL, or null if unassigned. This is populated on req.user for every authenticated request.

CollectionPublishingOptionsinterface

interface CollectionPublishingOptions

Publishing options configured on a collection.

Members (1)
checks?: Array<string | PublishCheckConfig>;

Checks to run before docs in this collection are published or scheduled. Accepts either a check ID (which defaults to the required level) or a {id, level} config object.

commentEmailNotificationsfunction

function commentEmailNotifications(options?: CommentEmailNotificationsOptions): CMSNotificationService;

Creates a CMSNotificationService that emails users when field comments are added, resolved, or reopened. By default the recipients are the users @mentioned in the comment plus everyone who previously commented on the same field, excluding the user performing the action.

Emails are queued in Projects/{projectId}/Emails and delivered by the Root.js email service, the same as emailNotifications().

Example:

cmsPlugin({
  notifications: [
    commentEmailNotifications({
      emailService: true,
      // Also notify a shared inbox for comments on the "Pages" collection.
      to: (data) => (data.collectionId === 'Pages' ? ['web@example.com'] : []),
    }),
  ],
});

CommentEmailNotificationsOptionsinterface

interface CommentEmailNotificationsOptions

Options for the commentEmailNotifications service.

Members (14)
id?: string;

Unique ID for the service. Defaults to 'comment-email'.

label?: string;

Human-readable label displayed in the UI. Defaults to 'Comment emails'.

icon?: string;

Optional icon URL displayed in the UI next to the label.

actions?: FieldCommentAction[];

Comment actions that trigger an email. Defaults to new comments, resolves and reopens (doc.comment.add, doc.comment.resolve, doc.comment.reopen).

notifyMentions?: boolean;

Whether to notify users mentioned in the comment via @mention. Defaults to true.

notifyParticipants?: boolean;

Whether to notify previous participants of the thread (everyone who has commented on the field). Defaults to true.

notifySelf?: boolean;

Whether to include the user that performed the action in the recipients. Defaults to false.

to?: string[] | ((data: CommentEmailTemplateData, ctx: NotificationServiceContext) => string[] | Promise<string[]>);

Additional recipients, either a static list or a function returning the list for a given action (e.g. doc watchers). Merged with the mentioned users and participants.

filter?: (data: CommentEmailTemplateData, ctx: NotificationServiceContext) => boolean | Promise<boolean>;

Optional filter called before sending. Return false to skip the notification, e.g. to ignore comments on certain collections.

from?: string;

Sender email address. Defaults to noreply@<gcp-project-id>.appspotmail.com.

cmsUrl?: string;

Base URL of the site hosting the CMS, used to build links to the commented field (e.g. https://example.com). Defaults to rootConfig.domain.

template?: EmailNotificationTemplate | ((data: CommentEmailTemplateData, ctx: NotificationServiceContext) => EmailNotificationTemplate | Promise<EmailNotificationTemplate>);

Custom email content. Either {placeholder} string templates resolved against CommentEmailTemplateData (e.g. {fieldLabel}, {by}, {content}, {url}) or a function returning the final email. When unset, a default subject and body describing the comment are used.

emailService?: string | boolean;

Email service used to trigger delivery immediately after the email is queued. See emailNotifications() for details.

expireAfterMinutes?: number;

Number of minutes after which an unsent email expires.

CommentEmailTemplateDatainterface

interface CommentEmailTemplateData

Template data passed to CommentEmailNotificationsOptions.template.

Members (13)
action: Action<FieldCommentActionMetadata>;

The action log entry that triggered the notification.

actionName: FieldCommentAction;

Action name, e.g. doc.comment.add.

by: string;

Email of the user that performed the action.

docId: string;

Doc id in the form <collection>/<slug>.

collectionId: string;
slug: string;
fieldKey: string;

Deep key of the commented field.

fieldLabel: string;

Human-readable label of the field, falling back to the deep key.

content: string;

Plain-text content of the comment (empty for resolve/reopen actions).

mentions: string[];

Lower-cased emails mentioned in the comment.

participants: string[];

Lower-cased emails of everyone who has commented on the thread.

summary: string;

Short human-readable description of what happened.

url: string;

Link to the field in the CMS, built from rootConfig.domain (or the cmsUrl option). Empty when neither is configured.

DEFAULT_COMMENT_EMAIL_TEMPLATEvariable

const DEFAULT_COMMENT_EMAIL_TEMPLATE: Readonly<Required<EmailNotificationTemplate>>;

Default subject and body templates used by commentEmailNotifications.

DEFAULT_EMAIL_TEMPLATEvariable

const DEFAULT_EMAIL_TEMPLATE: Readonly<Required<EmailNotificationTemplate>>;

Default templates used by emailNotifications when no custom template content is provided. Exported so custom templates can compose with the defaults, e.g. template: {...DEFAULT_EMAIL_TEMPLATE, subject: 'Custom subject'}.

emailNotificationsfunction

function emailNotifications<T = any>(options: EmailNotificationsOptions<T>): CMSNotificationService;

Creates a CMSNotificationService that sends email notifications when actions occur in the CMS (publishes, schema changes, etc.).

Emails are queued in the Projects/${projectId}/Emails collection in firestore and delivered by the Root.js email service (apps/root-services) using the App Engine Mail API.

Example:

cmsPlugin({
  notifications: [
    emailNotifications({
      actions: ['doc.publish', 'release.*'],
      to: ['cms-alerts@example.com'],
      template: {
        subject: '[cms] {metadata.docId} published by {by}',
      },
      emailService: true,
    }),
  ],
});

EmailNotificationsOptionsinterface

interface EmailNotificationsOptions<T = any>

Options for the emailNotifications service.

Members (11)
id?: string;

Unique ID for the service. Defaults to 'email'.

label?: string;

Human-readable label displayed in the UI. Defaults to 'Email'.

icon?: string;

Optional icon URL displayed in the UI next to the label.

to: string[] | ((action: Action, ctx: NotificationServiceContext) => string[] | Promise<string[]>);

Recipients of the email notification. Either a static list of email addresses or a function that returns the recipients for a given action (e.g. to look up watchers of a doc). Returning an empty list skips the notification.

from?: string;

Sender email address. The sender must be authorized to send email via the App Engine Mail API, e.g. noreply@<gcp-project-id>.appspotmail.com. Defaults to noreply@<gcp-project-id>.appspotmail.com.

actions?: string[];

Actions that trigger an email notification, e.g. ['doc.publish', 'release.*']. Patterns support wildcards, where * matches any number of characters and ? matches a single character. Matching is case-insensitive. When unset, all actions trigger an email notification.

filter?: (action: Action, ctx: NotificationServiceContext) => boolean | Promise<boolean>;

Optional filter called after the actions patterns match. Return false to skip the notification (e.g. to ignore actions performed by certain users).

transformData?: (action: Action, ctx: NotificationServiceContext) => T | Promise<T>;

Optional transformation applied to the action log data before the email templates are rendered. The returned value is passed to the templates as the template data. When unset, the action log data object is used as-is.

template?: EmailNotificationTemplate | ((data: T, action: Action, ctx: NotificationServiceContext) => EmailNotificationTemplate | Promise<EmailNotificationTemplate>);

Templates used to render the email. Either an object with {placeholder} string templates or a function that returns the final email content for full control. Function results are used verbatim, i.e. no placeholder rendering or HTML escaping is applied.

A missing subject falls back to the default subject template. The default body and html templates (see DEFAULT_EMAIL_TEMPLATE) apply only when neither body nor html is provided. Providing only body sends a plain-text-only email; providing only html derives the plain-text body from the html.

emailService?: string | boolean;

Email service used to trigger delivery immediately after the email is queued. Setting this to true uses the default hosted service at https://services.rootjs.dev. Set to a base URL to use a self-hosted deployment of the service (apps/root-services). When unset, the email remains queued until the email service's cron next processes the queue.

expireAfterMinutes?: number;

Number of minutes after which an unsent email expires. Expired emails are skipped by the email service instead of being delivered late. When unset, queued emails never expire.

EmailNotificationTemplateinterface

interface EmailNotificationTemplate

Email content used by the emailNotifications service.

When provided as string templates (via the template option), each value supports {placeholder} tokens that are resolved against the template data — the action log data object, or the result of transformData() when configured. Tokens support dot notation for nested values, e.g. {metadata.docId}. Unknown placeholders are left untouched.

Members (3)
subject?: string;

Subject line.

body?: string;

Plain-text body.

html?: string;

HTML body. Values injected into an HTML string template are HTML-escaped. Omit (while providing body) to send a plain-text-only email.

FieldCommentinterface

interface FieldComment

A single entry in a field comment thread.

Members (12)
id: string;

Unique id of the entry within the thread.

type?: FieldCommentType;

Kind of entry. Defaults to comment when missing.

body?: RichTextData | null;

Rich text body of the comment.

content?: string;

Plain-text rendering of the body, used for previews and notifications.

mentions?: string[];

Lower-cased emails of users mentioned via @mention in the body.

createdAt: CommentTimestamp;
createdBy: string;
updatedAt?: CommentTimestamp;
updatedBy?: string;
deleted?: boolean;

Set when the comment was deleted by its author. The body is cleared.

deletedAt?: CommentTimestamp;
deletedBy?: string;

FieldCommentActiontype

type FieldCommentAction = (typeof FIELD_COMMENT_ACTIONS)[keyof typeof FIELD_COMMENT_ACTIONS];

FieldCommentActionMetadatainterface

interface FieldCommentActionMetadata

Metadata attached to field comment actions in the action log. Notification services (e.g. commentEmailNotifications()) read these values to decide who to notify and what to say.

Members (10)
docId: string;
collectionId: string;
slug: string;
fieldKey: string;
fieldLabel?: string;
threadId: string;
commentId?: string;

Id of the comment entry, for add, edit and delete actions.

content?: string;

Plain-text content of the comment, truncated for the log.

mentions?: string[];

Lower-cased emails of users mentioned in the comment.

participants?: string[];

Lower-cased emails of everyone who has commented on the thread.

FieldCommentThreadinterface

interface FieldCommentThread

A thread of comments attached to a single field of a doc.

Members (13)
id: string;

Thread id. Open threads use the id derived from the field key; resolved threads carry a timestamp suffix.

docId: string;

Doc id in the form <collection>/<slug>.

fieldKey: string;

Deep key of the field within the doc, e.g. fields.hero.title.

fieldLabel?: string;

Human-readable label of the field at the time of the first comment.

status: FieldCommentThreadStatus;
comments: FieldComment[];

Chronological history of comments and status changes.

participants: string[];

Lower-cased emails of everyone who has commented on the thread.

createdAt: CommentTimestamp;
createdBy: string;
updatedAt?: CommentTimestamp;
updatedBy?: string;
resolvedAt?: CommentTimestamp | null;
resolvedBy?: string | null;

FieldCommentThreadStatustype

type FieldCommentThreadStatus = 'open' | 'resolved';

Lifecycle status of a field comment thread.

NotificationResultinterface

interface NotificationResult

Result returned by a notification service onAction handler.

Members (2)
status?: 'success' | 'info' | 'error';

Delivery status. - 'success': the notification was delivered. - 'error': delivery failed; message should describe why. - 'info' (default): the service handled the action but did not deliver a notification (e.g. the action was filtered out).

message?: string;

Optional human-readable message describing the result.

NotificationServiceContextinterface

interface NotificationServiceContext extends CMSServiceContext

Context passed to notification service handler functions.

PublishCheckConfiginterface

interface PublishCheckConfig

A check configured to run as part of a collection's publishing flow.

Members (2)
id: string;

ID of a check registered via cmsPlugin({checks: [...]}).

level?: PublishCheckLevel;

Severity level for the check. Defaults to required.

PublishCheckLeveltype

type PublishCheckLevel = 'required' | 'warning';

Severity level for a check configured on a collection.

- required: an error result halts publishing. - warning: publishing continues, and the message is surfaced afterwards.

PublishCheckResultinterface

interface PublishCheckResult

The result of running a single check against a single doc.

Members (7)
docId: string;

The doc the check ran against, e.g. Pages/index.

checkId: string;

ID of the check that produced this result.

label: string;

Human-readable label for the check.

level: PublishCheckLevel;

The severity level the check was configured with.

status: PublishCheckStatus;

Outcome of the check run.

message: string;

A message describing the result. Supports markdown.

metadata?: Record<string, any>;

Optional metadata returned by the check.

PublishCheckStatustype

type PublishCheckStatus = 'success' | 'warning' | 'error';

The result status of a check.

renderEmailTemplatefunction

function renderEmailTemplate(template: string, data: any, options?: {
    escapeHtml?: boolean;
}): string;

Renders a {placeholder} template string using values from data. Placeholders support dot notation for nested lookups, e.g. {metadata.docId}. Unknown placeholders are left untouched. Values are stringified based on their type: firestore timestamps and dates are converted to ISO strings and objects are converted to formatted JSON. When escapeHtml is set, values are HTML-escaped before injection.

RootLocaletype

type RootLocale = string;

A site-defined locale identifier, as configured in i18n.locales. Root is agnostic to its format.

TranslationExportResultinterface

interface TranslationExportResult

Result returned by an onExport handler.

Members (4)
title?: string;

Optional title displayed in the notification after export.

message?: string;

Optional message displayed in the notification after export.

link?: {
    url: string;
    label?: string;
};

Optional link displayed in the notification (e.g. to the translation service).

status?: 'success' | 'info' | 'error';

Notification status controlling the color of the notification. - 'success': green - 'error': red - 'info' (default): neutral

TranslationImportResultinterface

interface TranslationImportResult

Result returned by an onImport handler when no rows are imported.

Members (4)
title?: string;

Optional title displayed in the notification after import.

message?: string;

Optional message displayed in the notification after import.

link?: {
    url: string;
    label?: string;
};

Optional link displayed in the notification (e.g. to the translation service).

status?: 'success' | 'info' | 'error';

Notification status controlling the color of the notification. - 'success': green - 'error': red - 'info' (default): neutral

TranslationLanguagetype

type TranslationLanguage = string;

The language identifier used by translation systems (CSV/Sheets columns, translation services, CMS translations pages), mapped from a root locale via i18n.translationLanguages. Defaults to the root locale id.

TranslationRowinterface

interface TranslationRow

A row of translation data keyed by translation language.

Members (3)
source: string;

The source string.

translations: Record<TranslationLanguage, string>;

Map of translation language to translated string. Translation languages are derived from the project's locales via the i18n.translationLanguages config in root.config.ts; when no mapping is configured, the keys are the locale ids themselves.

description?: string;

Optional translator notes/context for the source string.

translationsCheckfunction

function translationsCheck(options?: TranslationsCheckOptions): CMSCheck;

A first-party CMS check that verifies all translatable strings in a document have translations for each of the document's enabled locales.

TranslationsCheckOptionsinterface

interface TranslationsCheckOptions
Members (1)
collections?: string[];

Optional list of collection IDs to restrict this check to. When set, the check is only shown and runnable for documents in these collections.

TranslationServiceContextinterface

interface TranslationServiceContext extends CMSServiceContext

Context passed to translation service import/export functions.

Members (6)
docId: string;

The document ID, e.g. Pages/index.

collectionId: string;

The collection ID, e.g. Pages.

slug: string;

The slug, e.g. index.

locales: RootLocale[];

The locales configured for the project.

translationLanguages: TranslationLanguage[];

The translation languages for the project, derived from locales via the i18n.translationLanguages config in root.config.ts (deduped, since multiple locales may share a translation language). Equal to locales when no mapping is configured. The translations keys in TranslationRow data use these languages.

user: {
    email: string;
};

The email of the user performing the action.

@blinkk/root-cms/project

import {…} from '@blinkk/root-cms/project';

convertOneOfTypesfunction

function convertOneOfTypes(collection: schema.Collection, schemaModules?: Record<string, SchemaModule>): schema.Collection;

Converts all oneof field type definitions into a map keyed by the type name. The field definitions are replaced with an array of type names.

String references (used for self-referencing schemas) are resolved from the project's schema modules. SchemaPatterns are resolved by matching file paths.

Schemas pulled in via SchemaPattern or string-name reference are always deep-cloned before being walked. The walk rewrites oneof fields in place, and skipping the clone would mutate the shared entries in SCHEMA_MODULES and corrupt subsequent calls (e.g. when building multiple collections).

getCollectionSchemafunction

function getCollectionSchema(collectionId: string): schema.Collection | null;

Returns a collection's schema definition as defined in /collections/<id>.schema.ts.

getProjectSchemasfunction

function getProjectSchemas(): Record<string, schema.Schema>;

Returns a map of all schema.ts files defined in the project as fileId => schema. This is used by generate-types.ts to build the root-cms.d.ts file and by the CMS app to list collections.

resolveOneOfPatternsfunction

function resolveOneOfPatterns(schemaObj: schema.Schema, schemaModules?: Record<string, SchemaModule>): schema.Schema;

Resolves SchemaPattern objects in oneOf fields to arrays of Schema objects. This is needed for type generation which expects field.types to be an array.

SCHEMA_MODULESvariable

const SCHEMA_MODULES: Record<string, SchemaModule>;

SchemaModuleinterface

interface SchemaModule
Members (1)
default: schema.Schema;

@blinkk/root-cms/richtext

import {…} from '@blinkk/root-cms/richtext';

RichTextfunction

function RichText(props: RichTextProps): import("preact").JSX.Element | null;
namespace RichText {
    var Block: (props: RichTextBlock) => import("preact").JSX.Element | null;
    var ParagraphBlock: (props: RichTextParagraphBlockProps) => import("preact").JSX.Element | null;
    var HeadingBlock: (props: RichTextHeadingBlockProps) => import("preact").JSX.Element | null;
    var ListBlock: (props: RichTextListBlockProps) => import("preact").JSX.Element | null;
    var ImageBlock: (props: RichTextImageBlockProps) => import("preact").JSX.Element | null;
    var HtmlBlock: (props: RichTextHtmlBlockProps) => import("preact").JSX.Element | null;
    var TableBlock: (props: RichTextTableBlockProps) => import("preact").JSX.Element | null;
}

Renders data from the "richtext" field.

RichTextBlockinterface

interface RichTextBlock
Members (2)
type: string;
data?: any;

RichTextBlockComponenttype

type RichTextBlockComponent = RichTextComponent;

RichTextComponenttype

type RichTextComponent = FunctionalComponent<any>;

RichTextComponentMaptype

type RichTextComponentMap = Record<string, RichTextComponent>;

RichTextContextvariable

const RichTextContext: import("preact").Context<RichTextContextProps>;

RichTextContextPropsinterface

interface RichTextContextProps
Members (2)
components?: RichTextComponentMap;

Rich text components override for both inline and block level components.

t?: (msg: string, params?: Record<string, string | number>) => string;

Translator function override.

RichTextDatainterface

interface RichTextData
Members (1)
blocks: RichTextBlock[];

RichTextHeadingBlockPropsinterface

interface RichTextHeadingBlockProps
Members (2)
type: 'heading';
data?: {
    level?: number;
    text?: string;
};

RichTextHtmlBlockPropsinterface

interface RichTextHtmlBlockProps
Members (2)
type: 'html';
data?: {
    html?: string;
};

RichTextImageBlockPropsinterface

interface RichTextImageBlockProps
Members (2)
type: 'image';
data?: {
    caption?: string;
    file?: {
        url: string;
        width: string | number;
        height: string | number;
        alt: string;
    };
};

RichTextInlineComponenttype

type RichTextInlineComponent = RichTextComponent;

RichTextListBlockPropsinterface

interface RichTextListBlockProps
Members (2)
type: 'orderedList' | 'unorderedList';
data?: {
    style?: 'ordered' | 'unordered';
    items?: ListItem[];
};

RichTextParagraphBlockPropsinterface

interface RichTextParagraphBlockProps
Members (2)
type: 'paragraph';
data?: {
    size?: RichTextParagraphSize;
    text?: string;
    components?: Record<string, any>;
};

RichTextPropsinterface

interface RichTextProps
Members (3)
data: RichTextData | undefined;
components?: Record<string, RichTextBlockComponent>;
translate?: boolean;

Deprecated.

RichTextTableBlockPropsinterface

interface RichTextTableBlockProps
Members (2)
type: 'table';
data?: {
    rows?: Array<{
        cells: Array<{
            blocks: RichTextBlock[];
            type: 'header' | 'data';
        }>;
    }>;
};

testContentfunction

function testContent(data: RichTextData): boolean;

Returns whether the rich text value is truthy.

useRichTextContextfunction

function useRichTextContext(): RichTextContextProps;
1
2
3
4
5
6
7
8
9
10
11
12
Breakpoint: