Getting started
This guide walks you through starting a new project on the CEOX Foundation — from an empty machine to a running app and a green quality gate. The framework is a universal Django 6 + HTMX + Tabler starter: server-rendered, secure by default, bilingual, with a built-in onboarding stack (registration, Google login, email 2FA, Stripe subscriptions, invoicing).
But the framework is more than a Django starter: it is built for agent-driven development. The repository ships a full Claude Code integration (a startup contract, a rulebook, guarding hooks, slash commands, and an MCP server that serves a Tabler catalog) that steers the agent toward the framework's principles and blocks the most common mistakes with machine gates. That is why this guide begins with section 0 — setting up Claude Code — and only then moves on to installing the Django project.
If you only care about the classic install, jump to 1. Prerequisites. If you work with an agent (Claude Code), read 0. and the MCP section first.
0. Setting up Claude Code (the agent environment)
Claude Code is the CLI-based agent environment you use to develop the framework. The repo is arranged so that the agent works by the framework's rules from the very first moment.
0.1 Prerequisites for Claude Code
- Claude Code CLI installed and logged in.
- Python 3.14 and a virtual environment (the same one the Django project uses — see
section 1). The MCP server needs the
mcp[cli]dev dependency, which is installed bypip install -e ".[dev]". - Git Bash on Windows (the
makerecipes are POSIX-syntax). - Start
claudefrom the repo root so the project-level settings (.claude/,.mcp.json,CLAUDE.md) load.
0.2 The repo's Claude Code integration
- Entry point:
CLAUDE.md. It is deliberately short and does not auto-load everything. At the top an@AGENTS.mdimport pulls in the universal developer contract (server-is-the- truth, the frontend order, and safe Git discipline). - Mandatory startup (step 0):
make preflight(=python tools/preflight.py --strict), which checks the required external access. On a non-zero exit, STOP and report what is missing. - Required reading after startup: the "project brain" (
project/AI_CONTEXT.md,SPECIFICATION.md,SYSTEM_DESIGN.md,DO_NOT_CHANGE.md,DECISIONS.md,MODULE_STATUS.md), then thedocs/GOVERNANCE.mdmap and the task-relevant.claude/rules/. The working method lives indocs/PLAYBOOK.md. - Do NOT re-read the source of a done/frozen module: work from the
project/interfaces/<module>.mdcard. This is deliberate context economy.
0.3 Recommended model
- Always use the newest, strongest model available (the framework is model-agnostic; the
rules live in
CLAUDE.md). - Plan with the expensive model, do mechanics with a cheaper one: planning
(
/module-plan) and security review deserve the strongest model; templated, well-scoped implementation or bulk renames can run on a faster model (or fast mode). - Use plan mode for any non-trivial task.
0.4 The guarding hooks (what they block)
.claude/settings.json wires up four hooks; the hook scripts live under tools/ (check_frozen.py directly, the rest under tools/hooks/), there is no .claude/hooks/ folder. These are defense-in-depth, not a sandbox — they
enforce the framework's principles mechanically:
check_frozen.py(before Edit/Write): reads the frozen rows ofproject/MODULE_STATUS.mdand blocks editing them. Note: the done state is only reading discipline; the hook enforces only the frozen path. To unfreeze, a human flips it back to "active".guard_paths.py(before Bash/PowerShell): guards frozen paths, the vendor (vendor_src/,static/vendor/) and the generated Tabler catalog. It blocks opaque command classes (inlinepython -c/sh -c, arbitrary scripts, non-namedpython -m/maketargets, working-tree-rewriting git commands, auto-formatters), and protected paths are deny-by- default. Only a narrow allow-list passes (namedmaketargets,tools/scripts, pytest/ruff, allowedmanage.pysubcommands). The vendor and catalog must not even be read (context pollution) — use MCP search instead.commit_gate.py(before Bash/PowerShell): if the command isgit commit, it runs the fast gate (ruff + frontend boundary + migration drift + pytest+coverage) and blocks the commit on failure. Escape hatch:CEOX_SKIP_COMMIT_GATE=1.format_edit.py(after Edit/Write): formats the changed.pywithruff format; non-blocking.
The settings.json permission list also carries deny entries: reading/writing .env*,
*.pem, *.key, vendor_src/**, static/vendor/** and the generated catalog JSON(L) is
forbidden. So don't try to write into the vendor or a frozen module from the shell, or read
.env — the right path is the Edit/Write tool and the named tools/ scripts.
0.5 Slash commands (the module lifecycle)
The commands under .claude/commands/ carry development through:
/bootstrap -> /module-plan -> (approval) -> /module-build
-> /module-review -> commit -> /module-close
/bootstrap [description]— start a new project; it does not code but runs preflight, assesses state from evidence, asks at most 8 targeted questions, fills the project brain, and writes a Phase 1 milestone into the roadmap./module-plan <module>— a module plan (data model, views+URL+auth, HTMX swaps, permission matrix, test list); updates the system design and writes an ADR. It does not code yet and asks for approval at the end./module-build <module>— implements the approved plan in a fixed order (model + migration → views with permissions → URL → template ONLY fromtemplates/components/+ Tabler → HTMX partial → tests), ending withmake gate. Commit only on explicit request./module-review <module>— a diff review against a 6-point checklist (server authority, security, N+1/Decimal, design system, regression test, Playwright screenshot); it reports first, does not fix./module-close <module>—make gate-full(green only), generates the interface card, sets MODULE_STATUS to "done"; afterward the source is not re-read./audit [area]— evidence-based state assessment, changes nothing, every claim with a file+line reference.
MCP servers: what they are for and what to watch
Claude Code gets two MCP servers from .mcp.json. They complement the repo's own
documentation, they do not replace it.
The two servers
tabler(local, stdio) — serves the FULL purchased Tabler catalog via typed tools. This is the source for daily frontend work. It is started by thetools/run_mcp.pycross-platform launcher.context7-cache(local, stdio) — live, version-pinned external library docs (Django, HTMX, Stripe, etc.), so the agent doesn't guess from possibly stale trained knowledge. We do not talk to Context7 directly, but through a local, caching MCP proxy (tools/context7_cache_mcp.py): it stores responses in SQLite and only goes to Context7 on a cache miss — so it's faster and spares the Free tier (several agents fetch the same Django/HTMX/Stripe question once).tabler(UI components) andcontext7-cache(external docs) are two separate layers, not redundant. Details and cache rules:docs/MCP.md.
The tabler tools:
tabler_search(query, kind, limit)— searches the whole Tabler (with Hungarian→English synonyms and a semantic rank); returns a stableref. This is the first step of the daily session.kindnarrows:component | page | icon | asset | library | all.tabler_component(ref)— the original, integrity-checked markup of a component/page + the Django/HTMX boundary contract + exact dependencies.tabler_dependencies(ref)— precise plugin dependencies flaggedlocalvscdn_only(a CDN-only item must be served from your own origin because of the CSP).tabler_icon(name, style)— an inline SVG icon (outline|filled). In templates prefer the{% tabler_icon "name" %}tag.tabler_asset(name, kind)— addressable variants of an illustration/avatar/email.tabler_email(name, variant)— ready-made email HTML (inline CSS deliberately stays).
Publishing a plugin runtime into static/vendor is CLI-only, not an MCP tool: run
python tools/tabler_publish.py --ref <ref> (or --lib <name>); only a local (vaulted) plugin
is published, never from a CDN, and the deploy runs the same step. The MCP surface stays
read-only (2027 audit: the redundant materialize writing tool was removed from MCP).
What to watch during INSTALL
mcp[cli]is a DEV-only dependency. It is pinned in the[dev]extra ofpyproject.toml(mcp[cli]>=1.27,<2). The production runtime (gunicorn) does not import it. The<2upper bound is mandatory because mcp v2 changes the import path. So you need the.[dev]install; a plainpip install -e .won't bring it in.- The
tools/run_mcp.pylauncher is deliberate..mcp.jsoncan't branch on the OS, and the venv Python path differs (Win.venv/Scripts/python.exevs POSIX.venv/bin/python); a hard-coded path would fail withspawn ENOENT. The launcher starts from any system Python, finds the venv Python, setsPYTHONPATH, and runs the server. - Both
tablerANDcontext7-cacheare auto-enabled (enabledMcpjsonServers: ["tabler", "context7-cache"]), and the three reading tools ofcontext7-cache(resolve_library_id,get_library_docs,cache_stats) are on the allow-list — they run silently. This is deliberate: both servers are local and read-dominant (andcontext7-cachealso caches). There is deliberately noenableAllProjectMcpServers— we enable by name. Put machine-specific paths/keys in the gitignored.claude/settings.local.json, not in the shared one. emaildeliberately prompts. The allow-list silently permits only the reading tools (tabler_search,tabler_component,tabler_dependencies,tabler_icon,tabler_asset);tabler_emailis not on it, so it asks for permission — a conscious gate. Publishing is no longer an MCP tool at all — it moved to thetools/tabler_publish.pyCLI.- Reading the vault/JSONL is FORBIDDEN.
vendor_src/**,static/vendor/**,staticfiles/**and the generatedai/tabler/generated/*.json(l)are under deny. Don't read the catalog directly (~200 MB / 20–35 MB, context pollution) — the MCP/CLI searches for you. - Catalog prerequisite: the tools call
_require_catalog(); if there is no generated catalog yet, first runpython tools/import_tabler.py --source-dir <folder>. context7-cacheone-time setup: a free key from context7.com, set it LOCALLY ($env:CONTEXT7_API_KEY="..."in PowerShell,export CONTEXT7_API_KEY=...in Git Bash, or a gitignored dev.env— the proxy also reads it from there if the env is missing), never commit it. Because of the empty default (${CONTEXT7_API_KEY:-}) a missing key won't break.mcp.jsonparsing. The cache lives in SQLite (.cache/context7-cache.sqlite3, gitignored); with several parallel agents the growth path is later Redis (shared cache + lock).- Self-check:
python tools/run_mcp.py --selfcheck(tabler, ormake tabler-mcp) andpython tools/context7_cache_mcp.py --selfcheck(context7-cache, no network) — they verify the server loads and all tools are visible.
What to watch during DEVELOPMENT
The order of the daily frontend session:
- Approved component FIRST — from
templates/components/and/styleguide/. We do not invent UI. - Search —
tabler_search(...)→ stableref(Hungarian works too;kind="icon"narrows). - Extract —
tabler_component("<ref>")→ balanced markup + exact dependencies + the HTMX contract; icon{% tabler_icon "name" %}. - Plugin if needed —
python tools/tabler_publish.py --ref <ref>(CLI-only) for the required runtime only. - Build a Django/HTMX-compatible component — the authoritative logic stays in Django;
don't copy the markup blindly, push it into
components/and the styleguide.
Two common pitfalls:
- The search is LEXICAL — search with Tabler's own words. Developer synonyms (
two factor,otp,2FA,checkout,landing) return 0 hits; usetwo step verification,sign in,pricing,marketing heroinstead. If you don't find it on the first try, do NOT hand-write it — try Tabler's vocabulary. - Never invent an established pattern — for sign-in, 2FA, registration, password reset, pricing, faq, stepper, invoice, empty state, first extract the Tabler block, then fit it to the framework's tokens.
- Vendor-CSS limit — the marketing pages'
.pricing-*/.hero-*/.section-*classes are NOT in the vendoredtabler.min.css; don't build on them. Thecore:blocks (card,ribbon,list-unstyled,display-5,badge) are in.
1. Prerequisites
- Python 3.14 is the target/CI version (the
pyprojectrange is more lenient,>=3.13, <3.15, so dev runs on 3.13 too, but CI enforces 3.14). - PostgreSQL (Google Cloud SQL) — locally reached ONLY through the Cloud SQL Proxy on port 5433. Docker is not used. Start it with:
cloud-sql-proxy ceox-hu-1:europe-west4:ceox-db --port 5433 --auto-iam-authn
- git, and a POSIX shell for
make(on Windows use Git Bash, because the recipes are POSIX-syntax). - The dev database defaults to SQLite (nothing to do); Postgres only with
DEV_USE_POSTGRES=1+DB_*, through the proxy.
2. Configure access (.env)
Copy the template and fill in the values:
cp .env.example .env
.env is never committed. Every setting is documented inline in .env.example, including
units where they aren't obvious. Turn features on only as you need them with the USE_*
switches — the preflight checks only the keys of the features you enabled.
Worth knowing:
- Where secrets live: only
.env(local) or GCP Secret Manager (production) — never in the repo. Nobody shell-sources.env(that would be a root-RCE path);python-dotenvloads it. An empty value means "not set yet". USE_*logic:flag()treats{1, true, yes, on}as truthy;USE_GITHUBdefaults on, the rest (USE_STRIPE/TURNSTILE/R2/INVOICING/POSTHOG/DEPLOY_SSH) default off. Double condition: Turnstile and PostHog are active only if theUSE_*is on AND the key is filled.- Runtime vs OPS secret:
GITHUB_TOKENandCLOUDFLARE_API_TOKENare provisioning/ops secrets — the running Django doesn't need them and they must NOT land in the production server's.env. - What preflight always requires:
DJANGO_SECRET_KEY, plusDB_NAME+DB_USER. The other keys are conditional on theUSE_*flags. With--strict, a missing required key gives exit 1.
3. Install
make install
This is pip install -e ".[dev]" (which pulls in the dev dependencies, including mcp[cli]
and Playwright) + playwright install chromium (needs network).
Then verify the required external access is in place:
make preflight
A non-zero exit tells you exactly which access is missing — stop and fix it before moving on.
Installation pitfalls
- A live Stripe key in dev halts the project. A
pk_live_-prefixed publishable key in DEBUG triggers theceox.E002Django system check (Error), and ansk_live_/rk_live_secret key triggersceox.E001. Because these are system checks, they halt themanage.pycommands (runserver/migrate/check) andmake gatetoo. In dev use test keys (pk_test_/sk_test_). Escape hatch (not recommended):ALLOW_LIVE_KEYS_IN_DEBUG=1. - Invoicing in dev:
USE_INVOICING=1with test mode off in DEBUG raisesceox.E003. - Bilingual invariant:
ceox.E004if there aren't exactly 2 languages configured orenis missing. In production aRuntimeErrorenforces this too. - Rotate secrets: least privilege + rotation. Rotate/revoke any keys you supplied for a
test afterward. The agent provisions the Turnstile secret rather than putting it on screen:
python tools/provision_turnstile.py --write. A blind manual key:python tools/env_set.py KEY. - No Docker/local PG. There is no Cloud Run / Docker for deploy; we don't use Docker in dev either. Postgres is reachable only through the Cloud SQL Proxy (dev 5433). Don't install a local/containerized PG — the pattern is: SQLite in dev, Postgres only through the proxy.
- Secret paths are locked:
.env*,*.pem,*.key,vendor_src/,static/vendor/are under deny for the agent. Don't write into or read from these via the shell. - Production boot guards: in production settings, a placeholder or shorter-than-32-character
DJANGO_SECRET_KEY, or a missingALLOWED_HOSTS/EMAIL_HOST, raises aRuntimeError— so the production boot fails right at startup if these aren't right.
4. Database
For local development SQLite is the default (zero setup). To develop against PostgreSQL, start
the Cloud SQL Proxy on port 5433 and set DEV_USE_POSTGRES=1 in .env.
Create the schema and seed the three example plans (Free / Starter / Pro):
python manage.py migrate
Create an admin user:
python manage.py createsuperuser
If you use Stripe, create the test products and monthly prices (idempotent — a stable
lookup_key means no duplicates, and it runs only for paid plans):
python manage.py setup_stripe_plans
5. Translations
The UI is exactly bilingual (English + the project language, default Hungarian), with English msgids. Mixed language is forbidden: if you add, delete or rename visible text, updating the catalog is mandatory — otherwise, in a Hungarian browser, untranslated (and fuzzy!) text renders in English.
The full cycle after a text change:
python manage.py makemessages -l hu -l en # or: make messages
python manage.py i18n_todo # shows missing and fuzzy entries
python manage.py compilemessages # or: make messages-compile
The .mo is loaded at startup, so after compilemessages restart the dev server.
6. Run
make run
The app is at http://127.0.0.1:8000. The landing page, sign-in, registration, dashboard and
the component gallery (/styleguide/ in dev) are all live.
Development golden rules
The framework's machine gates (the frontend-boundary checker, system checks, hooks) enforce these too — worth keeping in mind before the checker turns red:
- The server is the source of truth. Every business rule, permission, validation, computation, state, filter and pagination happens on the Django server. HTMX is only a fragment transport, JavaScript only a presentation add-on — client-side state can never be authoritative. Never implement "permissions" with CSS or by hiding buttons.
- Color/size/spacing only from tokens. No inline
style, no<script>, noon...handler, no free-written hex/px — only pre-defined classes.fetch/XMLHttpRequestonly with a documentedSERVER-TRANSPORT-EXCEPTION:marker;localStorage/sessionStorageonly with aUI-STATE-EXCEPTION:marker; every.jsmust open with the// UI-ONLY:contract. - Typography is class-based (
.t-*), min 16px, clamp 800→1600; the H-tag is semantics only. Self-hosted woff2 fonts (Inter + Space Grotesk) — Google Fonts is forbidden. - We do not invent UI. Build screens only from the approved components in
templates/components/; for known patterns start from the Tabler block (see the MCP section). - Bilingual is mandatory. Every visible string is
{% translate %}, and after every text change themakemessages → (translate) → compilemessagescycle runs. - Small things the checker catches: a checkbox is always square, never round; a Django
{# #}comment must be single-line only (use{% comment %}for multi-line, otherwise it leaks as visible text); flip cards are forbidden. - Python 3.14 / Django 6.0 — don't switch versions casually.
- Every new behavior and bug fix needs a regression test.
7. Quality gate
Before every commit:
make gate # lint + frontend boundary + migration drift + tests & coverage
make gate-full # the above + static build + Playwright end-to-end
make gateis fast (unit):ruff check, the frontend-boundary checker,makemigrations --check --dry-run(migration drift), andpytestwith coverage.make gate-fulladditionally runs a collectstatic dry run with production settings and the Playwright E2E (needsplaywright install chromium).- For the quick inner loop:
make test-fast(parallel, stops on first failure, no coverage — the fulltesttarget guards coverage).
Do not commit on a red gate. Push to main only after a green gate; no force push, no
unchecked reset --hard; before committing, review the staged diff and check there's no secret
in it. (The commit-gate hook enforces this mechanically too.) On Windows run make from Git
Bash.
Deploy (going to production)
The framework offers a one-command deploy. The target comes from deploy/targets.json
(parameterizable instance), and the process ends with a readiness gate + automatic rollback.
make ship # to the 'production' target
make ship TARGET=staging # another instance (just a different --target)
make ship M="what I changed" # if there are uncommitted changes (commit + push + deploy)
This runs the python tools/deploy.py --target production ... chain:
- Quality gate: runs
make gate; if red, the deploy stops (--no-gatecan disable it, but that's not recommended). - Commit (if dirty and
-mgiven) + push toorigin main; records the HEAD SHA. - Deploy over an IAP tunnel: on the VM a ROOT-owned deploy wrapper (outside the repo) installs the exact commit (no key in the script, and the firewall need not open port 22).
Copy targets.json from deploy/targets.example.json (gitignored) and parameterize it with
the instance/zone/GCP-project/app values. Another instance = just another entry + --target.
The VM-side wrapper steps: mandatory precondition check → deploy lock (flock, no concurrent
deploy) → detached checkout to the expected SHA → dependencies from the lock → check --deploy
→ migrate → collectstatic (with production settings) → systemctl restart → readiness
gate (querying /health/ready/ directly on the gunicorn unix socket, every 2 s, for 60 s
total). If there's no 200, an automatic code rollback to the previous commit. A failure in
any step after checkout triggers the same rollback.
Important: the rollback is code-level — it does NOT undo migrations that already ran. So keep migrations backward-compatible (additive first; deletion/tightening only in a later release).
8. Make it your own
- Brand & colors: set
BRAND_*in.env; change the palette instatic/css/theme.css(structure stays intokens.css). - Components: build screens only from the approved components in
templates/components/and the gallery. For known patterns use the Tabler catalog (thetablerMCP / CLI) — never hand-write markup for sign-in, pricing, 2FA, etc. - Content: edit the legal and docs pages under
content/. - Plans & features: adjust the seeded plans and the bilingual feature list
(
apps/billing).
That's it — you now have a running, tested, production-shaped starter to build your product on, and an agent environment that holds itself to the framework's principles.