Aphex

Contributing

How to work on Aphex itself — the monorepo, dev workflow, releases, and the studio → template → CLI sync chain.

Aphex is open source and contributions are welcome — bug fixes, new field types, database or storage adapters, UI improvements, and docs all help. Browse the open issues for ideas.

Prerequisites

  • Node.js 22+ — one supported version across the repo. It's the engines floor in every package.json, the base image the templates' Dockerfile runs (node:22-alpine), and what CI installs. Newer works; older is not tested.
  • pnpm 10 — pinned by packageManager, so corepack enable gets you the exact version rather than whatever is installed globally.
  • Docker + Docker Compose — only for the Postgres path and Mailpit; the SQLite path needs neither.
  • Git with SSH keys configured

Repository layout

aphex/
├── apps/
│   └── studio/            # Reference SvelteKit app — features land here first
├── packages/
│   ├── cms-core/          # Core engine + admin UI (adapter-agnostic via ports)
│   ├── auth/              # Better Auth wiring in one call
│   ├── postgresql-adapter/
│   ├── sqlite-adapter/    # libsql — a first-class alternative, not a reduced tier
│   ├── storage-s3/
│   ├── nodemailer-adapter/
│   ├── resend-adapter/
│   ├── ai-openai/         # (ai-ollama and ai-anthropic exist but are private)
│   ├── ui/                # Shared shadcn-svelte components
│   ├── visual-editing/    # Live preview overlay + stega helpers
│   └── create-aphex/      # `pnpm create aphex` scaffolder
├── plugins/
│   ├── plugin-forms/      # Editor-composed forms
│   ├── plugin-seo/
│   └── plugin-color-picker/
├── templates/
│   ├── base/              # Starter template (mirrored to aphex-base)
│   └── website/           # Page builder, posts, forms, SEO (mirrored to aphex-website)
├── docs/
│   └── aphex-docs/        # This site (mirrored to aphex-docs)
└── .github/workflows/     # release.yml, sync-template.yml, sync-docs.yml

Local setup

git clone [email protected]:IcelandicIcecream/aphex.git
cd aphex
pnpm install

cp apps/studio/.env.example apps/studio/.env
echo 'APHEX_DATABASE=sqlite' >> apps/studio/.env

pnpm dev          # studio + cms-core via Turborepo

That's the no-Docker path: SQLite pushes its schema at boot, so there is nothing to start and nothing to migrate. For Postgres instead — worth doing before touching anything RLS-related, since row-level security is Postgres-only:

pnpm db:start     # Postgres + Mailpit via Docker
pnpm db:migrate   # apply committed migrations
pnpm dev

pnpm db:push is a dev-only shortcut and must never be pointed at a production Postgres database — it can drop columns silently. Generate, review and commit a migration instead.

Admin UI lands at http://localhost:5173/admin. The first user to sign up at /login becomes super admin and gets a default org.

Hot reload behavior

ChangeBehavior
Schema filesPicked up on the next request.
Component changesInstant via Vite HMR.
cms-core sourceLive — consumed from source via the workspace protocol.
Adapter sources (postgresql-adapter, sqlite-adapter, storage-s3, email, AI)Require a rebuild + dev server restart — they're consumed from dist, unlike cms-core.
Plugin sources (plugins/*)Same: built to dist and consumed from it.
Drizzle schema changespnpm db:push (dev) or generate + migrate cycle.

Common commands

pnpm dev              # studio + cms-core
pnpm build            # build everything (Turborepo)
pnpm check            # type-check all packages
pnpm lint             # Prettier + ESLint
pnpm format           # write Prettier formatting
pnpm test:package     # build + type-check cms-core
pnpm shadcn <name>    # add shadcn-svelte components to @aphexcms/ui

Tests live in apps/studio/tests/:

pnpm -F @aphexcms/studio test          # all
pnpm -F @aphexcms/studio test:local    # Local API only
pnpm -F @aphexcms/studio test:http     # HTTP API only
pnpm -F @aphexcms/studio test:graphql  # GraphQL only

Commits & PRs

Conventional Commits — feat:, fix:, docs:, refactor:, chore:, test:. One feature or fix per PR; aim for under 500 lines of diff.

If your PR touches a published package, add a changeset:

pnpm changeset

Pick the affected packages, the bump type (patch / minor / major), and write a one-liner. Commit the generated .changeset/*.md file with the rest of your PR. Skip the changeset for docs-only or studio-only changes.

Releases & publishing

Releases run through Changesets and .github/workflows/release.yml. The flow on every push to main:

Pending changesets exist → workflow opens (or updates) a chore: version packages PR that bumps versions in every affected package.json and regenerates each package's CHANGELOG.md.

Version PR merges → workflow runs pnpm release which is turbo build --filter='./packages/*' && changeset publish --provenance. Packages ship to npm with a signed SLSA provenance statement.

Tags and GitHub Releases are created automatically by changesets/action.

What gets published

Everything in packages/ and plugins/ that isn't marked private — fifteen packages:

GroupPackages
Core@aphexcms/cms-core, @aphexcms/auth, @aphexcms/ui, @aphexcms/visual-editing
Database@aphexcms/postgresql-adapter, @aphexcms/sqlite-adapter
Storage@aphexcms/storage-s3
Email@aphexcms/nodemailer-adapter, @aphexcms/resend-adapter
AI@aphexcms/ai-openai
Plugins@aphexcms/plugin-forms, @aphexcms/plugin-seo, @aphexcms/plugin-color-picker
Toolingcreate-aphex (the pnpm create aphex scaffolder)

@aphexcms/ai-ollama and @aphexcms/ai-anthropic are marked private and do not publish. Neither do the apps and templates: @aphexcms/studio, @aphexcms/base, @aphexcms/website and aphex-docs are listed in .changeset/config.json's ignore array, because the templates reach users as mirror repositories rather than npm packages.

The list above is derived from private in each package.json, so adding a package to packages/ or plugins/ without that flag publishes it on the next release. node scripts/verify-packed-packages.mjs enumerates exactly the same set — run it to see what a release would ship.

How templates reach users

Templates are not published to npm. .github/workflows/sync-template.yml pushes each templates/* folder to its own standalone repository — aphex-base and aphex-website — which is what create-aphex downloads from and what the Deploy buttons point at.

Two things about the timing are easy to get wrong:

  • The mirror runs only after a release that actually published, because a mirrored template pins its @aphexcms/* dependencies by version and the workflow installs them from npm. Mirroring first fails with "No matching version found".
  • A release run that only opens the version packages PR publishes nothing, and the sync is skipped on it. That's why release.yml calls the sync workflow explicitly rather than triggering on workflow_run — a workflow_run trigger can't read the triggering run's outputs, so it can't tell the two apart.

Each mirror needs a deploy key in this repository's secrets (TEMPLATE_REPO_DEPLOY_KEY, WEBSITE_TEMPLATE_REPO_DEPLOY_KEY). A missing key fails that template's job — it used to warn on an otherwise green run, which made "the release succeeded" and "the template was mirrored" look identical.

Required repo secrets:

SecretPurpose
GITHUB_TOKENAuto-provided. Used to open the version PR.
NPM_TOKENRequired unless npm trusted publishing is configured for the @aphexcms scope.

Studio → template sync

apps/studio is the working reference. templates/base/ is the starter shipped to end users via pnpm create aphex (or npm create aphex@latest). To flow studio changes into the template:

./scripts/sync-template.sh           # dry run — preview changes
./scripts/sync-template.sh --apply   # actually copy files

The script is template-driven: it walks every file tracked in templates/base/ and copies the matching file from apps/studio/ if it exists. Studio-only files (tests, seed routes) don't get copied because the template has no matching path. Three special cases:

  • src/lib/schemaTypes/** — skipped. Template keeps its post.ts example, not studio's fixtures.
  • src/app.css — skipped. Template uses node_modules/@aphexcms/*/dist paths for Tailwind @source.
  • package.json — merged. Studio content wins; template's name and version are preserved.

If studio adds a brand-new file or directory, create a placeholder in templates/base/ first so the next sync picks it up.

After applying, update templates/base/CHANGELOG.md under ## Unreleased so downstream users know what changed — the template is meant to be customized, so changes don't auto-apply to anyone's existing project.

How a template change reaches pnpm create aphex

The scaffolder bundles no templates. create-aphex publishes only dist/, and at run time giget downloads a tarball from the mirror at a tag named after the scaffolder's own version:

github:IcelandicIcecream/aphex-base#create-aphex-v<version>

So a template edit reaches users in three hops, and the last one is the part that catches people out:

  1. Merge the change — sync-template.yml mirrors templates/* to aphex-base / aphex-website on the next release that publishes something.
  2. That workflow cuts create-aphex-v<version> only if the tag doesn't exist yet.
  3. Which means the tag only moves when create-aphex itself bumps.

A template-only fix therefore needs a create-aphex changeset to ship, even though nothing in packages/create-aphex/ changed. Without one, the mirror's main updates and npm create aphex keeps scaffolding the old snapshot — the version pin is doing exactly its job, which is why the fix is to cut a new pin rather than move an old one.

Smoke-test the built scaffolder against the live mirror:

pnpm -F create-aphex build
node packages/create-aphex/dist/index.js my-test-app --template website

Mirror workflows

Two GitHub Actions push subdirectories of the monorepo to standalone public repos:

WorkflowWhatDestinationSecret
.github/workflows/sync-template.ymltemplates/base/ (with workspace:* resolved)IcelandicIcecream/aphex-baseTEMPLATE_REPO_DEPLOY_KEY
.github/workflows/sync-docs.ymldocs/aphex-docs/IcelandicIcecream/aphex-docsDOCS_REPO_DEPLOY_KEY

Both run on pushes to main that touch the relevant directory and use s0/git-publish-subdir-action under the hood.

Setting up a deploy key

ssh-keygen -t ed25519 -f my_repo_deploy -N ""

Public key (my_repo_deploy.pub) → destination repo → Settings → Deploy keys → Add deploy key. Tick "Allow write access".

Private key (full file content, including the -----BEGIN/-----END markers and trailing newline) → source repo → Settings → Secrets and variables → Actions → New repository secret. Name it to match the workflow (TEMPLATE_REPO_DEPLOY_KEY or DOCS_REPO_DEPLOY_KEY).

Push a change touching the relevant directory and watch the Actions tab — first run prints the SSH handshake.

Adding new things

What you're addingWhere it goes
New field typepackages/cms-core/src/lib/types/schemas.ts + an editor in components/admin/fields/
New database adapterNew package implementing DatabaseAdapter and its sub-interfaces
New storage adapterNew package implementing StorageAdapter
New email adapterNew package implementing EmailAdapter
Custom HTTP route or middlewareapi(app) hook in aphex.config.ts (no plugin system)

See the architecture overview for how the ports-and-adapters seam is laid out.

See also

Edit on GitHub

Last updated on