open source · MIT · v1.2.0
A CMS that runs where your site does.
One config file in — database, REST + GraphQL APIs, and a full admin panel out. Built for SvelteKit and Cloudflare, and happy to sit on top of the database you already have.
$ npx highseam-init
highseam scaffolded.
created:
+ src/cms.config.ts
+ src/routes/admin/[collection]/[id]/+page.svelte
+ src/routes/api/graphql/+server.ts
+ src/routes/media/[...key]/+server.ts
… 32 more (REST + admin + media stubs, hooks, theme)
wired:
~ vite.config.ts (added @tailwindcss/vite plugin)
~ src/routes/+layout.svelte (imported highseam.css)
Next steps:
1. REQUIRED — install Tailwind v4 (the admin needs it):
npm i -D tailwindcss @tailwindcss/vite
2. npm run dev → open /admin and create the first user.
3. Shape your content in src/cms.config.ts — collections there get a
database, REST + GraphQL APIs, and admin UI automatically.Real output, v1.2.0. The quickstart actually working is a feature.
One file in. Three out.
Declare a collection in cms.config.ts and it gets a database presence, REST + GraphQL APIs, and an admin UI, automatically. Point it at a table you already own and nothing migrates.
import { defineCollection } from 'highseam';
const venues = defineCollection({
slug: 'venues',
table: { name: 'venues', autoId: true }, // ← your existing table
admin: {
useAsTitle: 'name',
groupsAs: 'tabs',
filters: ['suburb', 'dog_friendly']
},
fields: [
// names match your columns
{ name: 'name', type: 'text', required: true },
{ name: 'suburb', type: 'text' },
{ name: 'dog_friendly', type: 'checkbox' }, // 0/1 in SQL
{ name: 'rich_html', type: 'html' } // sanitized on write
]
});name
dog_friendly
8,412 documents · page 1 of 421
SaveREST
GET /api/venues?where[suburb][equals]=brunswick&sort=-updated_atGraphQL
{ Venues { docs { name suburb dog_friendly } } }An 8,000-row table owned by your pipeline? It just got an admin, faceted search, and APIs. Nothing migrated.
Sits on your database
Your tables. Its admin.
Add table: to a collection and it maps onto an existing SQL table: one column per field, names matching your columns. A pipeline-fed table, a legacy schema, same admin, same APIs, same drafts as everything else.
Filters and sorts compile to plain SQL. Writes touch only the columns you declare; the rest of the table is none of the CMS’s business.
Table mode skips localized fields and upload collections, and drafts want a _status column. Document mode has no such limits.
-- the admin's faceted filters become plain SQL
SELECT * FROM venues
WHERE suburb = ?1 AND dog_friendly = 1
ORDER BY name ASC LIMIT 20;
-- writes touch only the columns you declared
UPDATE venues SET name = ?1, rich_html = ?2 WHERE id = ?3;src/cms.config.ts your collections — the file you touch
src/lib/core/ framework-agnostic, no SvelteKit imports
crypto.ts Web Crypto only: PBKDF2 + HS256
adapters/memory.ts JSON file in dev
adapters/d1.ts Cloudflare D1 in productionwrangler d1 create highseam-db
npm run deploy # same core, now on WorkersRuns at the edge
No Node-only corners.
The core uses only Web Crypto: no bcrypt, no jsonwebtoken, no Node built-ins. The engine that runs in vite dev is the engine that runs on Cloudflare Workers, byte for byte.
Documents live in D1, files in R2, passwords as PBKDF2, sessions as HS256, all through the platform. Nothing is bolted on. Storage is an adapter, so dev runs on a JSON file you can read with your eyes.
Media with a memory
Every image knows its own story.
Alt text, caption, credit and focal point live on the image and are inherited everywhere it’s used; each use overrides only what it needs. The focal point is honoured with plain CSS. The picker is a real library: search, filters, keyboard. It loads nothing until opened, so it doesn’t care whether you have forty images or forty thousand.
Imports are provenance-aware: batches keyed on external identity (never URL equality), and files already sitting in your storage are adopted in place. A corpus backfill moves no bytes. Every image shows where it’s used, with backlinks.
import { resolveMedia, focalObjectPosition } from 'highseam';
// the image's own metadata is the default; each use overrides
// only what it needs — '' means "deliberately empty"
const img = resolveMedia(post.coverImage, { alt: post.coverAlt });<img src={img.url} alt={img.alt} class="object-cover"
style:object-position={focalObjectPosition(img.focalPoint)} />And the parts you’d expect.
The long tail is where recreating a serious CMS architecture pays off.
Drafts & versions
Save draft, publish, unpublish, restore, plus scheduled publishing with no job runner.
Structured rich text
Typed nodes with embedded components, and sanitized HTML fields for pipeline-written content.
Live preview across two deploys
The admin mints short-lived signed tokens; your public site verifies and renders drafts.
Access control
Booleans or query constraints, enforced identically in REST, GraphQL, and the admin.
Localization
Per-field locales with default-locale fallback; filters and unique checks are locale-aware.
Document locking
Five-minute rolling edit locks; a second editor sees who is already in the document.
API keys
Per-user keys authenticate any REST or GraphQL call.
Plugins
A plugin is a config transform, applied in order. No runtime magic to debug.
Generated TypeScript types
Selects become literal unions; blocks become discriminated unions on blockType.
Faceted filters & bulk actions
URL-synced facet controls; select rows, then publish, unpublish, or delete across the set.
Globals & Local API
Singletons in the same config, and server-side CRUD that skips HTTP entirely.
Light / dark / system theming
A theme is a CSS token file. Copy it, change values. That’s the entire contract.
Galleries & embeds
Ordered image sets with per-use overrides, and click-to-load video embeds resolved server-side from a provider allowlist.
Upload dedupe
Identical bytes return the existing document (SHA-256 content hash) instead of a second copy. Opt out per collection.
Drift-checked upgrades
npx highseam-init --check-stubs diffs your route stubs against the installed templates. Your customisations are never flagged.
What it isn’t
- No collaborative cursors. One editor per document, enforced by rolling edit locks.
- No built-in image transform service. Focal point is honoured via CSS today; resizing is yours to bring.
- A young ecosystem. The plugin API is there; a marketplace isn’t.
- One production deployment of note: an 8,000-row directory with a pipeline-fed table and an 8.4k-image media mirror. It’s where most of this was proven.
Weighing alternatives? There’s an honest comparison table