Skip to main content

Getting started

KilnCMS is a CMS that is headless and traditional: the same content is served through JSON:API, GraphQL, MCP and a RAG endpoint, and rendered directly by the app. It runs on Phoenix · Ash · LiveView · Tailwind · Elixir, with the Ash Framework at its core — resources, not schemas, are the source of truth for storage, actions, authorization and the publishing state machine.

A KilnCMS install is meant to be overlaid, not forked: the reusable core lives in lib/, and a downstream site adds its own content types under projects/.

How that is distributed: KilnCMS is not on Hex and is not consumed as a dependency. A site pins this repository as a git submodule and moves the pin between tagged releases with mix kiln.update — see Downstream projects for the layout, Releasing for the update flow, and the status section of the Overview for what 1.0 promises about the surfaces you would build against. A prebuilt core image is also published on each release tag (docker pull ghcr.io/the-verscienta/kiln_cms:latest) if you want to run it without a checkout.

This page is the landing page for mix docs and the orientation path for a new contributor. It points at the right guide rather than restating it.

1. Run it locally

Prerequisites: Elixir 1.20+ / OTP 29+, Docker (for Postgres), Node.js 22. The full list, for running, building and developing, is in system-requirements.md.

.tool-versions pins the exact Elixir/OTP that CI and the release image build on — asdf install (or mise install) reproduces it. Developing on something newer is fine and expected; mix kiln.toolchain.check compares the declared versions to each other, never to the one you are running.

docker compose up -d postgres   # the only required service
mix setup                       # deps.get + ash.setup + assets.setup + npm install
mix phx.server

mix setup seeds a demo admin and editor. The full setup — optional infra profiles for cache, search and object storage, plus the environment quirks that bite people (PATH, a path with a space in it, the opt-in ML stack) — is in Overview and Contributing.

On a production instance nothing is seeded: visit /setup while the site has no admin and a short wizard creates the first one (see Deploy).

Once it boots, the surfaces worth opening:

URLWhat it is
/the delivered site
/editorthe authoring UI — this is the product
/adminAshAdmin raw CRUD (dev only)
/gql · /gql/playgroundGraphQL endpoint; the GraphiQL playground is dev only (production serves the endpoint alone)
/api/json/swaggeruiJSON:API browser over the OpenAPI spec (dev/test; API_DOCS_ENABLED in prod)
/mcpthe MCP endpoint for LLM authoring

2. Read the architecture

  • lib/kiln_cms/ — the domain, split by concern: cms/ (content, blocks, taxonomy), accounts/, media/, search/, firing/ (publishing to served artifacts), analytics/, mail/, and so on. Each has a matching Ash domain module beside it (cms.ex, accounts.ex, …).
  • lib/kiln_cms_web/ — endpoint, router, LiveViews (*Live), controllers and the component kit.
  • sdk/kiln_plugin/ — the extension points a downstream project or plugin builds against: Kiln.Plugin, Kiln.Block, Kiln.FieldType, Kiln.Advisory, published to Hex as kiln_plugin. lib/kiln/ holds the core side: the plugin registry, Kiln.Version, Kiln.Updates.
  • projects/ — downstream overlays. projects/example/ is the worked example. A plugin shipped as a Mix dependency is installed through the overlay's git-ignored config/project_deps.exs, not the core's mix.exs (see Downstream projects).
  • lib/mix/tasks/ — the operator surface (mix kiln.*).

Read the resource modules before changing behaviour — lib/kiln_cms/cms/content.ex is the centre of gravity. Its policies, state machine and paper-trail history are declarative, so the module is the specification.

3. Find the right guide

You want to…Read
Use the editorEditor shortcuts, Editorial advisories, Claim checking
Model new contentExtending the content model
Overlay the core with your own projectThe overlay contract — what you may rely on across releases; Downstream projects for the mechanics
Understand who can do whatAuthorization policy matrix, Granular RBAC
Run several sites from one installMulti-tenancy
Consume the content headlesslyHeadless consumer guide — it routes you to JSON:API, GraphQL, MCP or RAG
Wire up an external front endVisual-editing bridge, Static export
Style the admin UIDesign language, Design system
Write a pluginRuntime extensibility
Configure an installEnvironment variables
Deploy itDeploy — required environment, image, boot, health endpoints, first admin, backups, optional infrastructure
Operate it in productionBackups, Observability, Performance, Resilient delivery
Cut a releaseReleasing Kiln
Assess its security postureThreat model, Data flows & retention

Everything else is in the sidebar, grouped the same way. Two groups are archives rather than guidance: Design notes & decision records holds the plans behind shipped features, and Audits & release checklists holds point-in-time material that was true on its date and is kept for the record.

4. Make a change

  1. Branch off main.

  2. Model in Ash. If you change a resource's attributes or actions, regenerate migrations and snapshots — never hand-write them:

    mix ash.codegen <short_name>

    Commit the generated priv/repo/migrations/* and priv/resource_snapshots/* with your code; CI fails on drift via mix ash.codegen --check.

  3. Every action gets a domain code interface, and every resource gets policies. Policy changes need policy tests — see the existing *_policies_test.exs.

  4. Run the gate:

    mix format
    mix precommit

    If you touched a file containing gettext(...), also run mix gettext.extract --merge && git diff --exit-code -- priv/gettext — that gate is CI-only and drifting line references alone will fail it.

Contributing has the full workflow, the reasoning behind each rule, and the commit and PR conventions.

5. Build these docs

mix docs
open doc/index.html

The output covers every module in lib/ plus every guide under docs/. CI builds it too, so a renamed guide or a dead cross-reference fails a check instead of rotting.