Pre-v1 Open source · Self-hostable

The Open Source Sanity-style CMS, built for Svelte.

Aphex CMS is an open-source CMS that lives inside your SvelteKit app. Define your schemas in TypeScript. Bring your own Postgres. Extend anything.

Get release updates
Or try it out
$ pnpm create aphex Read the docs
GitHub ↗ SvelteKit · Postgres
01 / STUDIO
The editing surface

Three panels. No surprises

Types on the left, documents in the middle, the editor on the right. On mobile, it collapses into breadcrumbs.

Aphex studio — collections list, document table, and document editor
02 / DETAILS
Editor pet peeves, fixed

The little things add up.

Editing is a thousand small judgments an hour. Aphex spends its detail budget on the ones that actually slow you down — versioning, validation, autosave, references.

01 01 · Versioning
Version history panel

Every save is a checkpoint.

Aphex keeps a rolling 25 versions per document. Compare, restore, or branch without leaving the editor. Republishing identical content is a no-op.

02 02 · Perspectives
Draft and published perspective toggle

Draft & published, in parallel.

Every document has both streams. Edit the draft freely; readers see only what you publish. Switch perspectives in a click — queries return whichever you ask for.

03 03 · Validation
Inline validation in the editor

Red where it would have shipped.

Validation runs on every keystroke, on save, and at publish. Errors block the publish button — not your front-end at 2am.

04 04 · References
Nested reference modal overlay

Nested editing, no context lost.

Click a referenced doc and it slides open as a modal — same studio, deeper. Save bubbles up; the parent stays exactly where you left it.

05 05 · Autosave
Autosave indicator

Saves when you pause.

Drafts persist after two seconds of typing-rest — debounced, never on every keystroke. Nothing gets lost; publishing stays a deliberate action.

Version history panelDraft and published perspective toggleInline validation in the editorNested reference modal overlayAutosave indicator
03 / SCHEMA
One schema, four outputs.

Declare once.
Everything else follows.

One schema becomes a validated form in the studio, a typed REST payload, a GraphQL type with resolvers, and a database migration. You write the shape; Aphex writes the plumbing.

  • Document and object types, nested as deep as you need
  • Chainable validation: Rule.required().max(100)
  • Auto-resolved references, cycle-safe

No codegen step. No build artifacts. Save the file, the studio reflects it.

// schemaTypes/page.ts — your page document type
import type { SchemaType } from '@aphexcms/cms-core';

export const page: SchemaType = {
  type: 'document',
  name: 'page',
  title: 'Page',
  fields: [
    {
      name: 'title',
      type: 'string',
      validation: (Rule) => Rule.required().max(100),
    },
    {
      name: 'slug',
      type: 'slug',
      source: 'title',            // auto-generate
    },
    {
      name: 'content',
      type: 'array',
      of: [
        { type: 'textBlock' },
        { type: 'imageBlock' },
        { type: 'catalogBlock' },
      ],
    },
    {
      name: 'author',
      type: 'reference',
      to: [{ type: 'author' }],
    },
  ],
};
04 / API
REST · GraphQL · Local

One schema,
three dialects.

Same types, same validation, same access control. Pick the dialect that fits the problem in front of you — switch on Tuesday without rewriting Wednesday.

  • Auto-generated REST + GraphQL — zero boilerplate
  • Type-safe local API — call it directly from +page.server.ts
  • Per-field access control, enforced at the API layer
  • Perspective-aware — query drafts or published per request

GraphiQL mounts at /api/graphql by default.. Schema changes propagate live — no restart, no stale queries.

// src/routes/+page.server.ts — type-safe local API
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ locals }) => {
  const { localAPI } = locals.aphexCMS;
  const ctx = { organizationId: locals.org.id };

  const { docs } = await localAPI.collections.page.find(ctx, {
    where: { slug: { equals: 'home' } },
    perspective: 'published',
    limit: 1,
  });

  return { page: docs[0] ?? null };
};
05 / ADAPTERS
Database · Storage · Email · Cache

Swappable by nature.

Every external boundary is an adapter you wire in. Start on Postgres + local disk in dev, ship on Neon + S3 + Resend in prod — without touching a line of CMS code.

  • Database — Postgres adapter today; the contract is open for more
  • Storage — local filesystem in dev, S3/R2 in prod
  • Email — Mailpit in dev, Resend in prod, Nodemailer for any SMTP
  • Cache — in-memory today; Redis on the way

Adapters are plain modules — no decorators, no DI container. Import. Configure. Pass to createCMSConfig.

// src/lib/server/db/index.ts — Postgres today, more on the way
import postgres from 'postgres';
import { env } from '$env/dynamic/private';
import {
  createPostgreSQLProvider,
  pgConnectionUrl,
} from '@aphexcms/postgresql-adapter';
import type { DatabaseAdapter } from '@aphexcms/cms-core/server';

export const client = postgres(pgConnectionUrl(env), {
  max: 50,
  idle_timeout: 20,
});

const provider = createPostgreSQLProvider({
  client,
  multiTenancy: { enableRLS: true, enableHierarchy: true },
});

export const db = provider.createAdapter() as DatabaseAdapter;
06 / DX
Developer experience

The small things compound.

Six decisions that feel like gravity once you're inside a real codebase.

01 · Runtime

Svelte 5 runes all the way down.

$state, $derived, $effect. No stores, no Svelte 4 leftovers. The studio reads like an app from next year.

02 · CLI

One command to the first draft.

Local-first setup. pnpm create aphex, install, db:start, migrate, dev. Postgres and email run in Docker — no cloud accounts, no signup flow, no credit card to see the studio.

03 · Packages

Upgrades are a version bump.

Everything ships as npm packages — cms-core, postgresql-adapter, storage-s3, ui. No forked monorepo, no vendor lock.

04 · Types

Schemas in, autocomplete out.

Run aphex generate:types once. Every collection in localAPI.collections.<name> becomes typed end-to-end — rename a field, every consumer lights up red. No any left when you stop.

05 · Same code, both sides

No headless disconnect.

Your +page.server.ts calls the same localAPI.collections.<name>.find() the studio uses internally. One mental model, zero RPC layer between editing and reading.

06 · Escape hatches

Custom code is just code.

Mount custom routes through api(app) — a Hono instance, 30-line learning curve. Custom storage is a plain class. Custom email is a function. Nothing decorator-heavy, nothing reflection-driven.

07 / ROADMAP
Shipping in the open

A roadmap, when there is one.

No quarters, no JIRA. The repo is the roadmap; this is a rough shape of where things are pointed — not a promise. Order is loose; priority shifts with what bites first.

● Today What works
  • Schema engine
  • Postgres adapter
  • Multi-tenant orgs + RLS
  • REST + GraphQL
  • Better Auth + API keys
● Next Building
  • Markdown & rich text content
  • Better assets — transforms, hotspot, signed URLs
  • WYSIWYG / visual editor
  • Plugins + extendable admin UI
  • MCP server — let AI talk to your content
● Then On the horizon
  • Aphex Club — blog platform & templates
  • Multiplayer — comments, real-time
  • Hosted Aphex Studio (headless)