@orion-studios/cms
v0.5.3
Published
Orion CMS v2 core engine — JSONB content model on Supabase primitives
Maintainers
Readme
@orion-studios/cms
Orion Studios' CMS engine: a single package that turns a custom Next.js site into a client-editable site. JSONB content on Supabase, a self-contained Studio admin, and a block system where one definition drives everything.
- No schema migrations for content changes. Pages are JSONB layouts; the
eight
cms_tables never change when you add or edit block types. defineBlock()is the single source of truth. A Zod schema + a React component per section type produces: TypeScript types, validation, defaults, the builder palette, the editing panel, inline click-to-edit, and the public-site renderer.- Three runtime modes, chosen by env:
supabase(production),memory(zero-setup dev/demo, in-process store),static(no CMS at all — renders fromsite-content.ts; how sites launch before a client buys the CMS).
Install
npm install @orion-studios/cmsRequires Next.js (App Router), React 19, Zod 3. Supabase only in supabase mode.
Entry points
| Import | Contents |
| --- | --- |
| @orion-studios/cms/blocks | defineBlock, defineGlobal, createBlockRegistry, createGlobalRegistry, field helpers (link, mediaRef, paragraphs), BlockPreviewProps, BlockData |
| @orion-studios/cms/server | createCmsRoutes (the whole API as one catch-all route), getServiceClient, resolveUser, runContentSync, getMemoryCms, permission matrix (can, roles) |
| @orion-studios/cms/studio | Studio (the full admin UI), LoginView, PasswordInput, UsersView, createStudioApi |
| @orion-studios/cms/content | createContentClient — anon-key reads for the public site (getPageByPath, listPublishedPaths, getGlobal) |
| @orion-studios/cms/forms | Form config types, processSubmission (validation, honeypot, rate limiting), shared SiteForm renderer |
| @orion-studios/cms/sql/bootstrap.sql | The complete idempotent schema (tables, RPCs, RLS, grants) |
| @orion-studios/cms/studio/styles.css | Studio styles (self-contained, ost- prefixed) |
The 5-minute wiring (already done in the site template)
// src/app/api/cms/[...path]/route.ts — the entire CMS API
import { createCmsRoutes } from '@orion-studios/cms/server'
import { registry } from '@/cms/registry'
export const { GET, POST, PATCH, DELETE } = createCmsRoutes({
registry, syncToken: process.env.CMS_SYNC_TOKEN,
})
// src/app/studio/page.tsx — the admin
import { Studio } from '@orion-studios/cms/studio'
import '@orion-studios/cms/studio/styles.css'
export default function Page() {
return <Studio registry={registry} globals={globals} siteName="Client Co" logoUrl="/logo.png" />
}Don't start here for a new site — copy templates/site from the
orion-cms-packages repo instead; it has all of this wired plus the scripts
(bootstrap, create-admin, sync). See NEW-SITE-PLAYBOOK.md at the repo
root for the full procedure.
Roles
Four tiers, enforced in the API (src/server/permissions.ts), UI adapts:
content— edit text/images on existing sections (no add/remove/reorder)editor: full content control including structure, new pages, exact-version publish, submissions, detailed analytics, and replacement of existing mediaadmin— everything + user management (Users panel in the Studio)developer— full access; the tier Orion Studios keeps for itself
Rank rules: you can only assign roles at or below your own, never change your own role, never delete yourself.
Environment
| Variable | Where | Purpose |
| --- | --- | --- |
| NEXT_PUBLIC_SUPABASE_URL | local + Vercel | project URL (also selects supabase mode) |
| NEXT_PUBLIC_SUPABASE_ANON_KEY | local + Vercel | public reads (RLS: published content only) |
| SUPABASE_SERVICE_ROLE_KEY | local + Vercel | server-only; powers the API layer |
| DATABASE_URL | local + Vercel | Postgres for bootstrap (use the transaction pooler on Vercel) |
| CMS_SYNC_TOKEN | local + Vercel | authorizes POST /api/cms/sync |
| CMS_STATIC=true | optional | static mode (no CMS) |
| CMS_MEMORY=true | optional | force memory mode |
The memory backend is privileged only outside production. In production it
can serve public ephemeral forms and events, but it rejects Studio and guarded
routes, the fixed development token, preview fallback, sync, publish cron,
notification email, and user management. NEXT_PUBLIC_CMS_MEMORY and other
public flags cannot enable the development credential.
Publication contract
Draft title, SEO, and layout are saved together as one immutable page version.
Manual publish requires that version's current draft_version_id. Scheduling
stores the selected version ID and later draft edits do not change what will go
live. A missing, stale, foreign, or already-published version fails with 409.
Content roles may create media and edit its metadata. Only editor, admin, and
developer roles may replace an existing file or read submissions and detailed
analytics.
Run the additive SQL migrations in filename order before deploying code that
calls the exact-version RPCs. bootstrap.sql has the same final schema,
functions, grants, and published-metadata backfill for new projects. If a
release fails after the migration, keep the fail-closed RPCs and roll forward.
Do not restore the old mutable publish signature.
Development
npm run typecheck && npm test # 24 node:test suites
npm run build # tsup → dist/
npm pack # tarball for vendoring / inspectionThe SQL bootstrap is idempotent and runs under restricted Postgres roles: it
avoids auth-schema privileges and explicitly grants the PostgREST roles
(service_role, anon, authenticated) table access — RLS policies filter
rows but do not grant access, and Supabase's default privileges only cover
postgres-created tables.
