Skip to content

Packages

Most functionality lives in laikacms as subpath exports; specialized integrations live in their own packages.

Repository layout (June 2026, updated July 2026). This monorepo now carries only the two core packages — laikacms and @laikacms/decap (@laikacms/decap-ai and the client-side decap extras moved into the @laikacms/decap-cms fork in July 2026, DCMS-492). The other packages documented below (@laikacms/aws, @laikacms/github, @laikacms/gitlab, @laikacms/bitbucket, @laikacms/git-gateway, laikacli, decap-cms-widget-lexicaleditor, decap-cms-widget-portabletext-editor, decap-cms-lexical-core, and the rest of the adapters) are still published to npm under the same names but are now developed in separate repositories. Their npm names are unchanged, so consumers install them exactly as before.

laikacms

The core package: domain types, API factories, default implementations, serializers, and shared utilities. Imported via subpath exports.

Domain (packages/laikacms/src/domain/)

SubpathDescription
laikacms/storageStorage abstractions (objects, folders, atoms)
laikacms/documentsDocument management with revisions
laikacms/assetsAsset/media management
laikacms/contentbase-settingsContentBase configuration

API (packages/laikacms/src/api/)

SubpathDescription
laikacms/storage/apiJSON:API for storage
laikacms/documents/apiJSON:API for documents
laikacms/assets/apiJSON:API for assets
laikacms/contentbase-apiJSON:API for ContentBase

Implementations (packages/laikacms/src/impl/)

SubpathDescription
laikacms/storage/r2Cloudflare R2 storage
laikacms/storage/fsFilesystem storage
laikacms/storage/s3S3→R2Bucket adapter (createS3Bucket()) — pairs with R2StorageRepository, not a StorageRepository itself. See @laikacms/aws/storage-s3 for the full S3 StorageRepository.
laikacms/storage/webdavWebDAV server
laikacms/storage/drizzleDrizzle ORM storage
laikacms/storage/jsonapi-proxyStorage JSON:API proxy
laikacms/assets/r2R2 asset storage
laikacms/assets/contentbaseAssets on top of storage
laikacms/assets/obsidianObsidian-vault-backed assets
laikacms/assets/jsonapi-proxyAssets JSON:API proxy
laikacms/documents/contentbaseDocuments on top of storage
laikacms/documents/drizzleDrizzle document storage
laikacms/documents/obsidianObsidian-vault-backed documents
laikacms/documents/jsonapi-proxyDocuments JSON:API proxy
laikacms/contentbase-settings-defaultDefault settings impl
laikacms/contentbase-settings-decapDecap-CMS-compatible settings

Testing utilities (packages/laikacms/src/)

SubpathDescription
laikacms/documents/testingContract test harness + InMemoryDocumentsRepository reference implementation for DocumentsRepository — use as a fast in-memory backend in integration tests or as a contract-compliance reference
laikacms/storage/testingContract test harness for StorageRepository implementations
laikacms/assets/testingContract test harness for AssetsRepository implementations

Serializers (packages/laikacms/src/serializers/)

SubpathDescription
laikacms/serializers/jsonJSON serialization
laikacms/serializers/yamlYAML serialization
laikacms/serializers/markdownMarkdown with frontmatter
laikacms/serializers/rawRaw binary/text

Shared (packages/laikacms/src/shared/)

SubpathDescription
laikacms/compatPromise-bridge helpers (runTask, collectStream) for consuming LaikaTask/LaikaStream without importing effect directly
laikacms/coreTypes, errors, utilities
laikacms/core/errorsDomain error classes (LaikaError subclasses: NotFoundError, BadRequestError, InternalError, etc.)
laikacms/core/errors-extraHTTP adapter utilities: ErrorCodeToStatusMap (error code → HTTP status), ErrorCodeToKeyMap, ErrorClasses
laikacms/core/types/*Targeted type modules — datetime, effect, ext-name, mime-type, pagination, role, role-permission
laikacms/core/utilitiesDependency-free helpers (memoize, lazy, lazyAsync, Url, Header, Paths, TemplateLiteral) — safe for bundled consumers that must not pull in Effect or the laika domain graph
laikacms/cryptoCryptographic utilities (barrel — imports all modules)
laikacms/crypto/*Per-module granular access: laikacms/crypto/constant-time, laikacms/crypto/hash, laikacms/crypto/password, laikacms/crypto/random, laikacms/crypto/timing — avoids dragging unused crypto deps (e.g. bcryptjs) when only one module is needed
laikacms/file-sanitizerFile upload sanitization
laikacms/i18nInternationalization (bundle index)
laikacms/i18n/enEnglish translations
laikacms/i18n/nlDutch translations
laikacms/json-apiJSON:API utilities
laikacms/sanitizerInput sanitization

@laikacms/aws

AWS service implementations.

SubpathDescription
@laikacms/aws/contentbase-settings-ddbDynamoDB-backed contentbase settings
@laikacms/aws/storage-s3S3-backed StorageRepository (also works with MinIO, Backblaze B2, DigitalOcean Spaces)
@laikacms/aws/storage-ddbDynamoDB single-table StorageRepository
@laikacms/aws/assets-s3S3-backed AssetsRepository

@laikacms/decap

Decap CMS server-side integrations: the Decap-compatible API and the OAuth2 server.

Moved (July 2026, DCMS-492): AI chat (@laikacms/decap-ai, now discontinued), the icon widgets, the Dutch locale, the embedded-entry editor component, and the config type utilities all moved into the @laikacms/decap-cms fork: …/widgets/aichat, …/widgets/lucide-icon, …/widgets/radix-icon, …/locales/nl, …/editor-component-embedded-entry, and …/config-types.

SubpathDescription
@laikacms/decap/decap-cms-backend-laikaDecap CMS backend (createLaikaBackend())
@laikacms/decap/decap-apiDecap-compatible API
@laikacms/decap/decap-oauth2OAuth2 server with PKCE
@laikacms/decap/decap-oauth2/i18ni18n bundle index for the OAuth2 UI
@laikacms/decap/decap-oauth2/i18n/enEnglish translations for the OAuth2 UI
@laikacms/decap/decap-oauth2/i18n/nlDutch translations for the OAuth2 UI

decap-cms-lexical-core

Developed in a separate repository (moved out June 2026). Still published to npm under the same name.

Lexical-specific bindings for the editor-agnostic @laikacloud/portabletext-core: Portable Text ↔ Lexical bridge, headless editor factory, custom blocks subsystem, and the LexicalRichtextValue class that derives canonical Portable Text from a Lexical editor state on every change.

Install

bash
pnpm add decap-cms-lexical-core

Main exports

ExportDescription
LexicalRichtextValueRichtextValue subclass that owns a Lexical EditorState and produces Portable Text
createHeadlessEditor()Creates a Lexical headless editor with the standard node set pre-registered
defaultNodesArray of Lexical EditorNode constructors used by the standard headless editor
lexicalToPortableText()Convert a Lexical EditorState to a PortableTextDocument
portableTextToLexical()Populate a Lexical editor from a PortableTextDocument
emptyPortableText()Returns a minimal valid empty PortableTextDocument
BlockNode / blocksContextCustom block subsystem for embedding arbitrary Decap entries inside Lexical
Everything from @laikacloud/portabletext-coreRe-exported for convenience (Mapper, RichtextValue, createKeyGenerator, …)

decap-cms-widget-lexicaleditor

Developed in a separate repository (moved out June 2026). Still published to npm under the same name.

Lexical-based rich text widget for Decap CMS, built on a shadcn-editor fork. Stores content as Portable Text (via decap-cms-lexical-core) and renders a full-featured editor toolbar in the Decap CMS control panel.

Install

bash
pnpm add decap-cms-widget-lexicaleditor decap-cms-lexical-core

Main exports

ExportDescription
WidgetDecap CMS widget definition object — pass to CMS.registerWidget()
LexicalControlReact control component (rendered in the Decap CMS editor panel)
LexicalPreviewReact preview component (rendered in the Decap CMS preview panel)
lexicalEditorWidgetSchemaZod schema for the widget field configuration (outdated?)
passthroughSerializerSerializer that stores the Portable Text value as-is (no conversion)
EditorThe standalone Lexical editor React component (usable outside Decap)

Basic usage

ts
import CMS from 'decap-cms-app';
import { Widget } from 'decap-cms-widget-lexicaleditor';

CMS.registerWidget(Widget);

Then in your Decap CMS config:

yaml
collections:
  - name: posts
    fields:
      - name: body
        widget: lexicaleditor

decap-cms-widget-portabletext-editor

Developed in a separate repository (moved out June 2026). Still published to npm under the same name.

Decap CMS widget backed by @portabletext/editor (Sanity's native Portable Text editor). A sibling of decap-cms-widget-lexicaleditor — choose this one when you want the official Portable Text editing experience instead of Lexical.

Install

bash
pnpm add decap-cms-widget-portabletext-editor

Main exports

ExportDescription
WidgetDecap CMS widget definition object — pass to CMS.registerWidget()
PortableTextEditorControlReact control component (rendered in the Decap CMS editor panel)
PortableTextEditorPreviewReact preview component (rendered in the Decap CMS preview panel)
PortableTextEditorViewThe standalone Portable Text editor React component (usable outside Decap)
schemaDefault @portabletext/editor schema used by the widget

Basic usage

ts
import CMS from 'decap-cms-app';
import { Widget } from 'decap-cms-widget-portabletext-editor';

CMS.registerWidget(Widget);

Then in your Decap CMS config:

yaml
collections:
  - name: posts
    fields:
      - name: body
        widget: portabletext-editor

@laikacms/github

GitHub-backed StorageRepository (GitHub App authentication).

SubpathDescription
@laikacms/github/storage-ghGitHub-backed storage repository

GithubDataSourceOptions

GithubStorageRepository accepts a GithubDataSourceOptions object. Auth is a discriminated union — supply either a pre-built octokit instance or the three GitHub App credential fields:

OptionTypeRequired whenDescription
octokitOctokitusing PAT / custom authPre-configured Octokit instance. When provided, App credentials (appId etc.) are not required.
appIdstring | numberApp auth (no octokit)GitHub App ID.
privateKeystringApp auth (no octokit)GitHub App private key (PEM). Literal \n sequences and surrounding quotes are normalised.
installationIdstring | numberApp auth (no octokit)GitHub App installation ID for the target repository.
ownerstringalwaysGitHub repository owner (user or org).
repostringalwaysGitHub repository name.
branchstringalwaysBranch to read from and commit to.
tokenTtlSecondsnumber (optional)Installation token TTL in seconds. Defaults to 50 minutes (tokens last ~1 h).
userAgentstring (optional)Custom User-Agent header for GitHub API requests. Defaults to @laikacms/github.

@laikacms/gitlab

GitLab-backed StorageRepository via the REST v4 API. Authenticates with a Personal Access Token, an OAuth bearer token, or a CI job token. Runtime-agnostic — only depends on fetch.

SubpathDescription
@laikacms/gitlab/storage-glGitLab-backed storage repository

GitlabStorageRepositoryOptions

OptionTypeRequiredDescription
projectIdstring | numberalwaysNumeric project ID or URL-encoded path (group/subgroup/project).
branchstringalwaysBranch to read from and commit to.
authGitlabAuthAuth credentials. Omit for anonymous reads on public projects. See auth union below.
apiUrlstring (optional)API base URL. Defaults to https://gitlab.com/api/v4. Override for self-hosted GitLab.
serializerRegistryStorageSerializerRegistryalwaysMap of extension → serializer (e.g. { md: markdownSerializer }).
defaultFileExtensionstringalwaysExtension used when creating objects (e.g. 'md').
commitAuthor{ name: string, email: string } (optional)Author attached to every commit. Omit to use the token owner's identity.
ignoreListreadonly string[] (optional)Glob patterns to exclude from directory listings. Defaults hide .keep, .DS_Store, etc.
determineExtensionDetermineExtension (optional)Custom strategy for picking the on-server file extension. Defaults to defaultDetermineExtension.

GitlabAuth union — supply exactly one of:

FieldTypeDescription
tokenstringPersonal access token. Sent as PRIVATE-TOKEN header.
oauthTokenstringOAuth 2.0 bearer token. Sent as Authorization: Bearer ….
jobTokenstringCI job token. Sent as JOB-TOKEN header.
headersRecord<string, string> (optional)Extra headers merged into every request.

@laikacms/bitbucket

Bitbucket Cloud-backed StorageRepository via the REST v2 API. Authenticates with an app password or an OAuth 2.0 token. Runtime-agnostic — only depends on fetch.

SubpathDescription
@laikacms/bitbucket/storage-bbBitbucket-backed storage repository

BitbucketStorageRepositoryOptions

OptionTypeRequiredDescription
workspacestringalwaysBitbucket workspace slug (e.g. 'acme').
repostringalwaysRepository slug within the workspace.
branchstringalwaysBranch every commit lands on.
authBitbucketAuthalwaysAuth credentials. See auth union below.
apiUrlstring (optional)API base URL. Defaults to https://api.bitbucket.org/2.0.
serializerRegistryStorageSerializerRegistryalwaysMap of extension → serializer (e.g. { md: markdownSerializer }).
defaultFileExtensionstringalwaysExtension used when creating objects (e.g. 'md').
commitAuthor{ name: string, email: string } (optional)Author attached to every commit.
ignoreListreadonly string[] (optional)Glob patterns to exclude from directory listings. Defaults hide .keep, .DS_Store, etc.
determineExtensionDetermineExtension (optional)Custom strategy for picking the on-server file extension.

BitbucketAuth union — supply one of:

FieldTypeDescription
appPassword{ username: string, password: string }App-password tuple. Sent as HTTP Basic.
oauthTokenstringOAuth 2.0 access token. Sent as Bearer.
tokenProvider() => string | Promise<string>Async token provider — called before every request (useful for token refresh).
headersRecord<string, string> (optional)Extra headers merged into every request.

@laikacms/git-gateway

Drop-in Netlify git-gateway-compatible HTTP handler. Lets Decap CMS (configured with backend: { name: git-gateway }) talk to a fixed GitHub repo through a GitHub App installation token, behind a pluggable Bearer-token verifier. Runtime-agnostic — runs on Cloudflare Workers, Node, Bun, Deno, or anywhere Hono is supported.

SubpathDescription
.gitGateway() Hono app factory (root export)

gitGateway(options) options

OptionTypeDescription
verifyToken(token: string) => Promise<User | null>Validates the incoming Bearer token. Return null (or throw) to reject.
github{ appId, privateKey, installationId, owner, repo, apiBase? }GitHub App credentials. apiBase defaults to https://api.github.com (useful for GHE).
allowedRolesstring[] (optional)When set, the user returned by verifyToken must have at least one matching role.
logger{ error, warn, info?, debug? } (optional)Pluggable structured logger (pino, bunyan, etc.). Only error and warn are required; info and debug are optional. Defaults to a no-op.
userAgentstring (optional)Custom User-Agent for outgoing GitHub API requests. Defaults to @laikacms/git-gateway.

Endpoints

MethodPathAuthDescription
GET/healthReturns { ok: true }. Cheap load-balancer health check.
GET/settingsReturns { version, github_enabled, roles, user }.
ALL/github/*Proxies to https://api.github.com/repos/{owner}/{repo}/* via an installation token.

The /github/* proxy allows only the same subset of endpoints as Netlify's gateway: git/*, contents/*, pulls/*, branches/*, merges/*, statuses/*, compare/*, commits/*, and issues/:n/labels. All other paths return 403 FORBIDDEN.

Usage

Mount it inside an existing Hono app:

ts
import { gitGateway } from '@laikacms/git-gateway';
import { Hono } from 'hono';

const app = new Hono<{ Bindings: Env }>();

app.route(
  '/.netlify/git',
  gitGateway({
    verifyToken: async token => {
      const r = await fetch('https://api.github.com/user', {
        headers: { Authorization: `token ${token}`, 'User-Agent': 'gg' },
      });
      if (!r.ok) return null;
      const u = await r.json();
      return { id: String(u.id), email: u.email, name: u.name };
    },
    github: {
      appId: env.GITHUB_APP_ID,
      privateKey: env.GITHUB_APP_PRIVATE_KEY,
      installationId: env.GITHUB_APP_INSTALLATION_ID,
      owner: 'acme',
      repo: 'website',
    },
  }),
);

Then in your Decap CMS config:

yaml
backend:
  name: git-gateway
  gateway_url: https://your-worker.dev/.netlify/git

laikacli

The Laika CMS command-line interface for local development workflows. Provides a short laika bin alias once installed; the canonical package name is laikacli (the laika npm name is taken by an unrelated package).

Supersedes the deprecated @laikacms/local package.

Install

sh
pnpm add -D laikacli

Or run without installing:

sh
npx laikacli local serve
pnpm dlx laikacli local serve

Commands

All local-file dev tooling lives under the local namespace.

CommandWhat it does
local serveStart a local-file JSON:API storage server for dev workflows (--root, --port, --host, --default-extension (default: md), --auth-token)
local generateGenerate a typed TypeScript module from a Decap CMS config.yaml (add --watch to keep it fresh)
local migrateCopy every atom between any two backends. Use --source-backend/--destination-backend for cross-backend migration (e.g. FS → R2, FS → SurrealDB); -s/-d are FS shortcuts for the common FS-to-FS case. Additional flags: --dry-run (preview without writing), --overwrite, --concurrency (default: 4), --page-size (default: 1000).

Run laika local <command> --help for the full flag reference.

Programmatic API

All CLI commands are also exported from the package root for embedding in your own scripts or Effect CLI applications:

ts
import { generateConfig, layerStorageServer, runMigrate } from 'laikacli';

Released under the MIT License.