Skip to content

Reference

Generated from the source of this commit by scripts/gen_reference.py. CI regenerates it and fails when the committed copy differs, so what is here is what the code does.

Capabilities and status

stable: public under the stability promise and used by a reference application (EventHub or the Social Stats rebuild). experimental: public and covered by Karni's own tests, but no reference application uses it yet, so expect rough edges. planned: documented as coming; not in the code.

Models

Capability Status Evidence
BaseModel: UUID key, created and updated timestamps, soft delete, created_by and updated_by stable models.py
Realistic, connected seed data (karni seed, <app>/seeds.py) stable seeds.py
Model events and listeners, optionally queued to Celery (@on_updated) stable listeners.py
Field-level change history (class Karni: audit = True) stable models.py
Scheduled tasks (<app>/schedules.py, karni schedule) experimental Karni's own tests
A tenant scope on every model (the karni-tenancy module) planned documented

API

Capability Status Evidence
BaseViewSet: CRUD, filters, search, ordering, pagination, ?expand= and the OpenAPI document, with no serializer stable viewsets.py
access declarations (Public, Owned, HasRole, Where, Granted) compiled into both the request gate and the row filter stable viewsets.py
Query budgets: a list endpoint that runs more queries than declared fails its test stable viewsets.py
CSV and XLSX export of any list (/export/?format=) stable test_leads.py
Import from CSV or XLSX with a mandatory preview (/import/) stable leads.tsx
Response caching per viewset (Cache, Uncached) experimental Karni's own tests
TypeScript types and typed hooks from the OpenAPI document (karni types) stable Makefile

Admin

Capability Status Evidence
Every model registered with derived columns, filters and search; no admin.py stable urls.py
Row scope from the roles' grants: a staff user sees and edits only the rows a grant allows experimental Karni's own tests
Field visibility, change history and the import and export screens experimental Karni's own tests
A permission matrix of every role against every model and action experimental Karni's own tests

UI components and hooks

Capability Status Evidence
ResourceTable, ResourceForm and ResourceDetail driven by the OpenAPI schema stable events.tsx, event-form.tsx, event-detail.tsx
Sign-in, session restore and role checks (AuthProvider, LoginForm, usePermission) stable login.tsx
Data hooks with shared caching (useResourceList, useResourceItem) stable LeadKanban.tsx
Import screen with column mapping and preview (ResourceImport) stable leads.tsx
Page building blocks: Page, Layout, Stat, Modal, Toaster, EmptyState stable event-dashboard.tsx
ChangeHistory, DataChart and FileUpload experimental Karni's own tests

Permissions

Capability Status Evidence
One access declaration per viewset: 404 outside the read scope, 403 for read-but-not-write stable viewsets.py
Roles as data (KARNI_ROLES) with the all and own scopes stable settings.py
The related:<key> scope (class Karni: scope_paths) and custom: scopes experimental Karni's own tests
Membership through a path: owner_field = "workspace__members" stable models.py
Scope paths that do not resolve fail karni check (E214) instead of answering 500 stable models.py
Field visibility per role (class Karni: field_roles) experimental Karni's own tests
Row scope in the admin, from the same grants as the API experimental Karni's own tests
Change history: who changed which field, when, from what to what stable models.py
Sensitive fields recorded in the history without their values experimental Karni's own tests
Maker-checker approvals and safe impersonation (the approvals and impersonation modules) experimental Karni's own tests
Tenancy: a tenant scope applied to every model planned documented

Agent integration

Capability Status Evidence
karni context, karni rules and karni check in the project workflow stable Makefile, Makefile
Adapter files for every supported agent tool from one rule set (karni rules sync) stable AGENTS.md, new-model.md
Slash commands for the routine tasks (/new-model, /new-page, …) stable new-model.md
karni explain <code>: why an error happens and the fix, offline experimental Karni's own tests
MCP server exposing context, rules and check as tools experimental Karni's own tests

Testing

Capability Status Evidence
assert_crud and assert_isolated: the whole API surface and owner isolation in two calls stable test_booking.py
Generated tests from karni make model … --with test stable test_lead.py
OpenAPI snapshot test (assert_schema_snapshot) stable test_api_schema.py
Clients per role (client_as) stable test_booking_flow.py
query_budget and user_with_roles helpers experimental Karni's own tests
Architecture rules that fail the build (karni check, the ESLint preset) stable Makefile

Commands

karni <command> --help prints the same. Generated from the CLI's own help text (docs/cli.md has the same entries).

Project

karni new

Scaffold a project from templates/project

karni new [OPTIONS] NAME
Argument / option Default Description
NAME (required) Project name (slug is derived from it)
--output-dir, -o PATH .
--domain TEXT example.com Production domain for Caddy

karni add

Install an optional module and wire it into settings

karni add MODULE
Argument / option Default Description
MODULE (required) Module short name, e.g. "payments"

karni upgrade

Upgrade across Karni versions: codemods, diff, manual-change report

karni upgrade [OPTIONS]
Argument / option Default Description
--to TEXT Target Karni version (default: the newest release in the changelog)
--from TEXT The project's current Karni version, instead of asking its interpreter (use it when the new karni is already installed)
--dry-run Print a unified diff of every change and write nothing
--allow-dirty Write even with uncommitted changes, or outside a git work tree
--list List every recorded breaking change and exit
--docker/--no-docker Where to ask for the installed karni version
--service TEXT django Env: KARNI_COMPOSE_SERVICE.

karni doctor

Report the environment: project, interpreter, Node, adapters, history

karni doctor [OPTIONS]
Argument / option Default Description
--service TEXT django Env: KARNI_COMPOSE_SERVICE.
--quiet, -q Do not print the optional tip.

karni config

Read and write Karni settings: project .karni/config.toml (committed) or --global ~/.config/karni/config.toml; the project wins.

karni config COMMAND [ARGS]...
karni config get

Print the effective value of KEY.

karni config get [OPTIONS] KEY
Argument / option Default Description
KEY (required) Setting name (see karni config list).
--global Use the global file ($XDG_CONFIG_HOME/karni/config.toml) only.
karni config set

Set KEY to VALUE in the project file (or --global).

karni config set [OPTIONS] KEY VALUE
Argument / option Default Description
KEY (required) Setting name (see karni config list).
VALUE (required) New value (true/false).
--global Use the global file ($XDG_CONFIG_HOME/karni/config.toml) only.
karni config list

Every setting with its effective value and source.

karni config list

Agents and checks

karni context

Print a compact machine-readable project inventory

karni context [OPTIONS] [PATH]
Argument / option Default Description
PATH File to brief, relative to the cwd (needs --with-history).
--with-history Append what a history provider (e.g. Gigai Chronicle) recorded about PATH under a HISTORY: heading — without PATH, the project's recorded decisions, from providers that offer them. Capped at ~300 tokens (~1200 characters), cut at line boundaries with a last line naming the command for the full brief. With no history store detected, prints the plain inventory and nothing else.
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.
--manifest PATH Explicit manifest.json path

karni rules

Karni's conventions for humans and AI agents (no Django needed).

karni rules [OPTIONS] COMMAND [ARGS]...
Argument / option Default Description
--check PATH (repeatable) Statically check FILE (repeatable); exit 1 on violations.
--explain TEXT Show one rule in full.
--procedure TEXT Show a procedure's steps.
--json Machine-readable output.
karni rules sync

Regenerate the agent adapter files from Karni's rule registry.

karni rules sync [OPTIONS]
Argument / option Default Description
--check Only compare; exit 1 if any adapter is stale (for CI).
--force Overwrite hand-written files (backed up under .karni/backup/).

karni check

Run architecture and system checks

karni check [OPTIONS]
Argument / option Default Description
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.
--frontend/--no-frontend on
--quiet, -q Do not print the optional tip.

karni explain

Explain an error code (E104, C101, PAY004, K106): why and the fix

karni explain [OPTIONS] CODE
Argument / option Default Description
CODE (required) An error code such as E104, W101, C101, PAY004 or K106.
--json Machine-readable output.

karni types

Generate TS types and typed hooks from the backend OpenAPI

karni types [OPTIONS]
Argument / option Default Description
--schema-out PATH frontend/src/api/schema.yml
--out PATH frontend/src/api
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.

karni mcp

Serve context, rules and check as MCP tools (needs karni-cli[mcp])

karni mcp [OPTIONS]
Argument / option Default Description
--project PATH Project root (default: found from the working directory).

Runners

karni serve

Start the dev stack — wraps docker compose up (or manage.py runserver with --local).

karni serve [OPTIONS]
Argument / option Default Description
--local Run manage.py runserver through the project .venv instead.
--host TEXT --local only. Default 127.0.0.1.
--port INTEGER --local only. Default 8000.
--build compose only: rebuild images first.
--detach, -d compose only: run in background.

karni migrate

Apply migrations — wraps manage.py migrate [ARGS].

karni migrate [OPTIONS] [ARGS...]
Argument / option Default Description
ARGS... Passed to manage.py migrate: [app_label] [migration_name] --plan --fake …
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.

karni test

Run the test suite — wraps python -m pytest [ARGS].

karni test [OPTIONS] [ARGS...]
Argument / option Default Description
ARGS... Passed to pytest: paths, -k EXPR, -x, --lf … (use -- before --help).
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.

karni shell

Open a Django shell — wraps manage.py shell [ARGS].

karni shell [OPTIONS] [ARGS...]
Argument / option Default Description
ARGS... Passed to manage.py shell: -c CODE, -i ipython …
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.

karni routes

List every URL: methods, name, view, model, access — wraps manage.py karni_routes.

karni routes [OPTIONS]
Argument / option Default Description
--json Print the routes as a JSON list.
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.

karni worker

Run a Celery worker — wraps python -m celery -A config worker -l info [ARGS].

A host worker on macOS gets --pool=solo (the prefork pool cannot run tasks there).

karni worker [OPTIONS] [ARGS...]
Argument / option Default Description
ARGS... Passed to celery worker: -Q QUEUES, --concurrency N …
--app, -A TEXT config Celery app module. Env: KARNI_CELERY_APP.
--loglevel, -l TEXT info
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.

karni beat

Run Celery beat — wraps python -m celery -A config beat -l info [ARGS].

karni beat [OPTIONS] [ARGS...]
Argument / option Default Description
ARGS... Passed to celery beat.
--app, -A TEXT config Celery app module. Env: KARNI_CELERY_APP.
--loglevel, -l TEXT info
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.

karni schedule

Celery beat schedules — wraps manage.py karni_schedules.

karni schedule COMMAND [ARGS]...
karni schedule list

List every schedule: name, task, timing, source — wraps manage.py karni_schedules.

karni schedule list [OPTIONS]
Argument / option Default Description
--json Print the schedules as a JSON list.
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.
karni schedule run

Run one schedule's task now, synchronously — wraps manage.py karni_schedules --run NAME.

karni schedule run [OPTIONS] NAME
Argument / option Default Description
NAME (required) The schedule name (see karni schedule list).
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.

karni seed

Fill every model with realistic demo data — wraps manage.py karni_seed [ARGS].

Values come from field names and types (an email gets an email, price money, starts_at a near-future date); relations point at seeded rows; the same --seed gives the same rows. Prints a demo login (demo@example.com / karni-password). Per-model overrides live in <app>/seeds.py (karni make seeder <Model>).

Example:

karni seed --count 20 --seed 42 --reset
karni seed [OPTIONS] [ARGS...]
Argument / option Default Description
ARGS... Passed to manage.py karni_seed: --count N, --seed S, --reset, --model app.Model, --json, --force.
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.

karni sync-roles

Create or update the roles KARNI_ROLES declares — wraps manage.py karni_sync_roles [ARGS].

Expands each role's grant patterns against every model and action into Role, Permission and RolePermission rows: creates what is missing, updates what changed, reports roles the database holds that the declaration no longer names, and never deletes. The same sync runs after every karni migrate (KARNI_ROLES_AUTOSYNC).

Example:

karni sync-roles --dry-run
karni sync-roles [OPTIONS] [ARGS...]
Argument / option Default Description
ARGS... Passed to manage.py karni_sync_roles: --dry-run, --json.
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.

karni createsuperuser

Create an admin user — wraps manage.py createsuperuser [ARGS].

karni createsuperuser [OPTIONS] [ARGS...]
Argument / option Default Description
ARGS... Passed to manage.py createsuperuser: --email E, --noinput …
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.

karni manage

Run any management command — wraps manage.py ARGS (raw passthrough).

karni manage [OPTIONS] [ARGS...]
Argument / option Default Description
ARGS... The manage.py command and its arguments, e.g. showmigrations events.
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.

Generators

karni make

Generate project code: apps, models, viewsets, tasks, schedules, listeners, pages ... (karni make --list). Never overwrites without --force; --dry-run previews.

karni make [OPTIONS] COMMAND [ARGS]...
Argument / option Default Description
--list Print every generator with a one-line summary.

Every generator follows the same rules:

  • It writes project code only — never framework logic.
  • It never overwrites: an existing target is an error naming the file and --force (which replaces it).
  • --dry-run prints the files and the diff it would write, and writes nothing.
  • It prints every created and modified file, then any notes (the chosen access, the route, the next command).
  • The target app is --app <label>, else the project's only local app (local apps are the # karni:apps entries of config/settings.py).
  • Existing files change only through marker comments (# karni:apps, # karni:middleware, // karni:page-imports, // karni:routes) or AST-located appends; an unfamiliar layout is an error that names the manual step. Generated Python is ruff-clean (isort sections, ruff format layout).
  • It never imports Django: karni make migration (and make model --migrate) run manage.py makemigrations through the project interpreter.
  • Every generator is also available as a hidden Laravel-style alias: karni make:<type> (e.g. karni make:model Event title:str).

karni make --list:

app         Create a local Django app (models.py, viewsets.py) wired into # karni:apps.
model       Add a model on BaseModel from a field DSL; --with viewset,page,test or -a chains them.
viewset     Add a BaseViewSet for a model, with its access declaration (--access).
serializer  Add an explicit serializer (--reason required) and set serializer_class.
migration   Create migrations: wraps manage.py makemigrations [app] [--name] [--empty].
test        Add <app>/tests/test_<model>.py: assert_crud + the isolation check for a resource.
factory     Add an explicit test factory to <app>/factories.py (overrides the generated one).
seeder      Add a Seeder to <app>/seeds.py: per-model rows and values for karni seed.
task        Add a Celery task (@shared_task) to <app>/tasks.py; --retry N.
schedule    Add a beat schedule (every/cron) to <app>/schedules.py, creating the task if needed.
event       Add a custom event (a Django Signal) to <app>/events.py.
listener    Add a listener to <app>/listeners.py: --on Model.created|updated|deleted or an event.
command     Add a management command (<app>/management/commands/<name>.py).
service     Add a service module (<app>/services/<name>.py) — the home of HTTP clients.
middleware  Add a middleware class to <app>/middleware.py, wired via # karni:middleware.
page        Add a frontend page (<= 50 lines) composing @karni/ui, with its route.
component   Add a React component that claims a role (registerComponentRole).
module      Create a reusable module skeleton: modules/karni-<name>/ (docs/modules.md).

Field DSL for karni make model NAME [FIELD...]:

Field Generates
title:str[:len] CharField(max_length=len) — 255 when omitted
body:text TextField()
count:int IntegerField()
ratio:float FloatField()
price:decimal[:max:places] DecimalField(max_digits=max, decimal_places=places) — 10, 2
done:bool BooleanField(default=False)
day:date / at:datetime DateField() / DateTimeField()
email:email / site:url / slug:slug EmailField() / URLField() / SlugField() (:len optional)
meta:json JSONField(default=dict)
doc:file / photo:image FileField / ImageField(upload_to="<app>/<model>/") — image needs Pillow
ref:uuid UUIDField(default=uuid.uuid4)
status:choice:draft\|live a TextChoices class + CharField(choices=..., default=first)
owner:fk:User ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
venue:fk:Venue / fk:shop.Venue ForeignKey("Venue") in the same app, or "app.Model"
tags:m2m:Tag / profile:o2o:User ManyToManyField / OneToOneField
name:type? optional: null=True, blank=True (strings and files: blank=True)
karni make app

Create a local app — replaces manage.py startapp.

Writes NAME/{init,apps,models,viewsets}.py, NAME/migrations/ and NAME/tests/, and adds NAME to the # karni:apps block of config/settings.py. No admin.py, serializers.py or urls.py: Karni derives them. An existing app is never truncated: --force only fills in missing files.

karni make app [OPTIONS] NAME
Argument / option Default Description
NAME (required) App label, snake_case (e.g. events).
--force Create only the missing files of an existing app (keeps the rest).
--dry-run Print the files and diff it would write; write nothing.
karni make model

Add class NAME(BaseModel) with FIELDS to APP/models.py.

Example:

karni make model Event title:str:200 starts_at:datetime "status:choice:draft|live" organizer:fk:User 'venue:fk:Venue?' -a --access owned:organizer
karni make model [OPTIONS] NAME [FIELDS...]
Argument / option Default Description
NAME (required) Model class name, PascalCase (e.g. Event).
FIELDS... Fields as name:type[:arg][?] — str[:len] text int float decimal[:max:places] bool date datetime email url slug json file image uuid choice:a|b fk:Model m2m:Model o2o:Model; trailing ? = optional.
--with TEXT (repeatable) Also generate: viewset, page, test, factory, seeder (comma-separated or repeated).
--all, -a Also generate the viewset, the pages and the API test.
--access TEXT With viewset: granted|authenticated|public-read|owned:<field>|staff|role:<name>.
--search TEXT (repeatable) With viewset: search_fields (comma-separated).
--migrate Run karni make migration for the app afterwards.
--app TEXT Target app label (default: the project's only local app).
--force Overwrite / replace existing targets.
--dry-run Print the files and diff it would write; write nothing.
karni make viewset

Add <Model>ViewSet(BaseViewSet) to APP/viewsets.py — CRUD at /api/<models>/.

Declares model = <Model> and the mandatory access (ADR 0001): granted (default: whatever KARNI_ROLES grants for the model, row-scoped), authenticated, public-read (anonymous reads), owned:<field> (rows whose <field> is the user), staff (role "staff"), role:<name>.

karni make viewset [OPTIONS] MODEL
Argument / option Default Description
MODEL (required) Model class name (e.g. Event).
--access TEXT Who reads/writes: granted|authenticated|public-read|owned:<field>|staff|role:<name>. Default: granted (the roles' grants).
--search TEXT (repeatable) search_fields, comma-separated or repeated (e.g. title,venue).
--app TEXT Target app label (default: the project's only local app).
--force Overwrite / replace existing targets.
--dry-run Print the files and diff it would write; write nothing.
karni make serializer

Add <Model>Serializer to APP/serializers.py and set the viewset's serializer_class.

BaseViewSet generates the serializer and APIMeta steers it, so an explicit one needs --reason; it is recorded as the # karni: allow(redundant-serializer): <reason> marker.

karni make serializer [OPTIONS] MODEL
Argument / option Default Description
MODEL (required) Model class name (e.g. Event).
--reason TEXT Why the generated serializer is not enough (required).
--app TEXT Target app label (default: the project's only local app).
--force Overwrite / replace existing targets.
--dry-run Print the files and diff it would write; write nothing.
karni make migration

Create migrations — wraps manage.py makemigrations [APP] [--name NAME] [--empty].

Without --app every app with model changes gets a migration; --empty (a data migration) targets one app: --app, else the only local app. --dry-run is makemigrations' own (prints, writes nothing).

karni make migration [OPTIONS] [NAME]
Argument / option Default Description
NAME Migration name (makemigrations --name), e.g. add_event_capacity.
--empty An empty migration to fill in (data migrations); needs one app.
--app TEXT Target app label (default: the project's only local app).
--dry-run Print the files and diff it would write; write nothing.
--docker/--no-docker
--service TEXT django Env: KARNI_COMPOSE_SERVICE.
karni make test

Add APP/tests/test_<model>.py — the resource's whole API surface in a few lines.

One APITestCase: assert_crud checks list, retrieve, create, update, delete, the denials the access declaration promises, pagination, filtering and the OpenAPI contract; assert_isolated (when reads are Owned, or Granted() with an own/related grant) checks another scope's rows are out of reach on every surface. Run it with karni test.

karni make test [OPTIONS] MODEL
Argument / option Default Description
MODEL (required) Model class name (e.g. Event); it needs a viewset.
--role TEXT The role the test acts as. Default: the first HasRole the viewset declares; for Granted() the first KARNI_ROLES role whose grant reaches the model (own/related first); else any signed-in user.
--app TEXT Target app label (default: the project's only local app).
--force Overwrite / replace existing targets.
--dry-run Print the files and diff it would write; write nothing.
karni make factory

Add <Model>Factory(ModelFactory) to APP/factories.py.

Only needed to change generated values: an explicit factory replaces the generated one everywhere (tests, related rows, karni seed), and fields it does not declare keep their generated, realistic values.

karni make factory [OPTIONS] MODEL
Argument / option Default Description
MODEL (required) Model class name (e.g. Event).
--app TEXT Target app label (default: the project's only local app).
--force Overwrite / replace existing targets.
--dry-run Print the files and diff it would write; write nothing.
karni make seeder

Add <Model>Seeder(Seeder) to APP/seeds.py, read by karni seed.

Only needed to steer a model's demo data: Meta.count pins its rows per run, and a class attribute overrides a field (a value or a factory-boy declaration); every other field stays inferred from its name and type.

karni make seeder [OPTIONS] MODEL
Argument / option Default Description
MODEL (required) Model class name (e.g. Event).
--count INTEGER Rows per karni seed run for this model (0 skips it).
--app TEXT Target app label (default: the project's only local app).
--force Overwrite / replace existing targets.
--dry-run Print the files and diff it would write; write nothing.
karni make task

Add a Celery task NAME to APP/tasks.py (registered as APP.tasks.NAME).

karni make task [OPTIONS] NAME
Argument / option Default Description
NAME (required) Task function name, snake_case (e.g. send_digest).
--retry INTEGER Retry up to N times on any exception (exponential backoff).
--app TEXT Target app label (default: the project's only local app).
--force Overwrite / replace existing targets.
--dry-run Print the files and diff it would write; write nothing.
karni make schedule

Add every(...)/cron(...) to APP/schedules.py — merged into Celery beat at startup.

Without --task the schedule runs APP.tasks.<name> (dashes become underscores), which is created when missing.

karni make schedule [OPTIONS] NAME
Argument / option Default Description
NAME (required) Schedule name, unique in the project (e.g. nightly-report).
--every TEXT Interval: 30s, 15m, 1h, 1h30m, 1d.
--cron TEXT Five-field crontab, e.g. "0 3 * * *" (03:00 daily).
--task TEXT Dotted task to run (default: APP.tasks.<name>, created if missing).
--app TEXT Target app label (default: the project's only local app).
--force Overwrite / replace existing targets.
--dry-run Print the files and diff it would write; write nothing.
karni make event

Add NAME = Signal() to APP/events.py (autodiscovered with the listeners).

karni make event [OPTIONS] NAME
Argument / option Default Description
NAME (required) Event name, snake_case (e.g. order_shipped).
--app TEXT Target app label (default: the project's only local app).
--force Overwrite / replace existing targets.
--dry-run Print the files and diff it would write; write nothing.
karni make listener

Add a listener to APP/listeners.py (autodiscovered — no AppConfig.ready wiring).

--on Event.created|updated|deleted uses karni.events (soft deletes count as deleted); --on order_shipped receives a custom event from events.py.

karni make listener [OPTIONS] NAME
Argument / option Default Description
NAME (required) Listener function name, snake_case.
--on TEXT (required) Model.created|Model.updated|Model.deleted (app_label.Model.event for an installed app's model, e.g. karni_payments.Payment.updated), or a custom event (order_shipped).
--queued Run the work in a Celery worker after the transaction commits.
--app TEXT Target app label (default: the project's only local app).
--force Overwrite / replace existing targets.
--dry-run Print the files and diff it would write; write nothing.
karni make command

Add a management command NAME to APP (run it with karni manage NAME).

karni make command [OPTIONS] NAME
Argument / option Default Description
NAME (required) Command name, snake_case (e.g. send_reminders).
--app TEXT Target app label (default: the project's only local app).
--force Overwrite / replace existing targets.
--dry-run Print the files and diff it would write; write nothing.
karni make service

Add APP/services/NAME.py — the one place HTTP clients are allowed (K105).

karni make service [OPTIONS] NAME
Argument / option Default Description
NAME (required) Service module name, snake_case (e.g. stripe_client).
--app TEXT Target app label (default: the project's only local app).
--force Overwrite / replace existing targets.
--dry-run Print the files and diff it would write; write nothing.
karni make middleware

Add a middleware class to APP/middleware.py and wire it into MIDDLEWARE.

Wiring goes through the # karni:middleware block of config/settings.py (appended after Karni's middleware); without that block the command prints the line to add.

karni make middleware [OPTIONS] NAME
Argument / option Default Description
NAME (required) Class name, PascalCase (Middleware suffix added).
--app TEXT Target app label (default: the project's only local app).
--force Overwrite / replace existing targets.
--dry-run Print the files and diff it would write; write nothing.
karni make page

Add frontend/src/pages/NAME.tsx (<= 50 lines) and register its route.

--resource R --kind list|detail|form composes ResourceTable / ResourceDetail / ResourceForm for the API resource R; without --resource the page is a bare <Page>. Routes: list <base>, detail <base>/$id, form <base>/new and <base>/$id/edit, base = --path or /<resource>.

karni make page [OPTIONS] NAME
Argument / option Default Description
NAME (required) Page file name, kebab-case (e.g. events).
--resource TEXT API resource (router prefix, e.g. events).
--kind TEXT With --resource: list (default), detail or form; login needs no resource.
--path TEXT Route path (resource pages: the base, default /<resource>).
--title TEXT Page title (default: from the name).
--no-route Do not register the route in frontend/src/routes.tsx.
--force Overwrite / replace existing targets.
--dry-run Print the files and diff it would write; write nothing.
karni make component

Add frontend/src/components/NAME.tsx, registered with registerComponentRole.

A role already claimed by @karni/ui (its manifest) or by another project component is refused — compose that component instead, or pass --replace-reason to replace it on purpose.

karni make component [OPTIONS] NAME
Argument / option Default Description
NAME (required) Component name, PascalCase (e.g. StatusBadge).
--role TEXT Semantic role it claims (default: kebab-case of NAME).
--replace-reason TEXT Take over a role another component claims, saying why.
--force Overwrite / replace existing targets.
--dry-run Print the files and diff it would write; write nothing.
karni make module

Create modules/karni-NAME/ — a reusable module per docs/modules.md.

The karni-NAME distribution with the karni_NAME app, a range-pinned karni dependency, the karni.settings fragment, AGENTS.fragment.md and a standalone test suite. Works outside a project too.

karni make module [OPTIONS] NAME
Argument / option Default Description
NAME (required) Module short name (e.g. payments, live-chat).
--dir PATH Parent directory (default: modules/ in the project, else the cwd).
--force Create only the missing files of an existing module (keeps the rest).
--dry-run Print the files and diff it would write; write nothing.

Base classes

The public names of the four packages a project builds on, from their __all__. What you get is read from the class itself: the fields and managers a model inherits, the attributes a viewset declares and the endpoints it serves, the admin hooks it overrides, a function's signature.

karni.models

Name Kind Purpose What you get
AuditedModel model created_by / updated_by, stamped from the current user on save. fields created_by, updated_by; managers objects (ManagerFromAuditedQuerySet)
BaseModel model The base every Karni project model inherits: all four mixins. fields id, created_at, updated_at, deleted_at, created_by, updated_by; managers objects (BaseManager), all_objects
SoftDeleteManager manager Default manager of karni.models.SoftDeleteModel.
SoftDeleteModel model Rows are hidden by delete() instead of removed. fields deleted_at; managers objects (SoftDeleteManager), all_objects
SoftDeleteQuerySet queryset QuerySet whose delete() soft-deletes by stamping deleted_at. methods delete, hard_delete, restore, alive, dead
TimestampedModel model created_at (indexed) and updated_at, maintained by Django. fields created_at, updated_at; managers objects (ManagerFromTimestampedQuerySet)
UUIDModel model Random UUID primary key — safe to expose in URLs, no enumeration. fields id

karni.api

Name Kind Purpose What you get
Access class A viewset's access declaration: access = Access(read=..., write=..., create=...). Access(*, read: Predicate, write: Predicate, create: Predicate \| None = None) -> None
Authenticated class Any authenticated user; every row. methods allows, rows, describe
BaseViewSet viewset Model-driven ModelViewSet: declare model and access, get a full API. declare model, access, expand_depth, expandable_fields, auto_register, route, search_fields, optimize, select_related, prefetch_related, query_budget, pagination, cache, scope_exempt, scope_exempt_reason, write_or_create_actions, unbudgeted_actions; endpoints list, create, retrieve, update, partial_update, destroy, export, history, import, import/<job>, import/<job>/commit, import/<job>/preview, import/template
Budget class query_budget = Budget(list=25, reason="...") — a viewset's own query budgets. Budget(*, reason: str, default: Any = …, **actions: int \| None) -> None
Cache class cache = Cache(ttl=60, depends_on=[Price]) — tune caching for one viewset. Cache(*, ttl: int \| None = None, depends_on: tuple[type[Model] \| str, ...] = ()) -> None
Can class The requesting user holds a grant for any of the named permissions (ADR 0002). Can(*keys: str) -> None
Granted class Whatever the roles say: Can for the viewset's own model and current action. methods bind, allows, rows, describe
HasRole class The requesting user holds any of the named roles (Django groups). HasRole(*names: str) -> None
Owned class Rows whose path points at the requesting user. Owned(path: str \| None = None, *, bypass: Predicate \| None = None) -> None
Public class Everyone, anonymous included; every row. The explicit public form. methods allows, rows, describe
Uncached class cache = Uncached(reason="...") — never cache this viewset's responses. Uncached(*, reason: str) -> None
Unpaginated class pagination = Unpaginated(max_rows=N, reason="...") — a viewset that returns lists. Unpaginated(*, max_rows: int, reason: str) -> None
Unscoped class access = Unscoped(reason="..."): turn Karni's access layer off for one viewset. Unscoped(*, reason: str) -> None
Where class Arbitrary logic: Where(lambda request: Q(...)). Where(fn: Callable[[Any], Q \| bool]) -> None

karni.admin

Name Kind Purpose What you get
DEFAULT_ADMIN_EXCLUDE constant DEFAULT_ADMIN_EXCLUDE: sessions, contenttypes, admin, auth.Permission, django_celery_results.ChordCounter
KarniAdminMixin admin class Every Karni admin behaviour, as cooperative ModelAdmin overrides. overrides get_queryset, has_view_permission, has_add_permission, has_change_permission, has_delete_permission, get_actions, get_list_display, get_list_filter, get_search_fields, get_sortable_by, get_fields, get_fieldsets, get_readonly_fields, lookup_allowed, history_view, formfield_for_manytomany, get_exclude, get_form, formfield_for_foreignkey, save_model, save_related, delete_model, delete_queryset, response_add, response_change, response_delete
KarniModelAdmin admin class KarniAdminMixin (and the import/export mixin) on the KARNI_ADMIN_BASE class.
SoftDeleteAdminMixin admin class Admin behaviour for soft-delete models. overrides get_queryset, get_paginator, get_changelist, get_list_filter, get_search_results
SoftDeleteListFilter admin class Changelist filter over deleted_at: alive (default) / deleted / all.
autodiscover function Register a derived admin for every unregistered, non-excluded model. autodiscover(site: AdminSite \| None = None) -> None

karni.testing

Name Kind Purpose What you get
APITestCase class DRF's APITestCase with clients per role and the one-call CRUD/isolation checks. methods user_as, client_as, assert_crud, assert_isolated
assert_crud function Assert the full CRUD surface of resource for role (see the module docstring). assert_crud(resource: Any, role: str \| None = None, *, user: Any = None, values: Mapping[str, Any] \| None = None, payload: Mapping[str, Any] \| None = None, expect: Mapping[str, int] \| None = None, skip: Iterable[str] = ()) -> CrudReport
assert_isolated function Assert role cannot reach another owner's (or scope's) rows of resource. assert_isolated(resource: Any, role: str \| None = None, *, by: str \| tuple[str, ...] \| None = None, owner: str \| None = None, values: Mapping[str, Any] \| None = None) -> IsolationReport
assert_schema_snapshot function Fail when the generated OpenAPI schema differs from the committed snapshot. assert_schema_snapshot(path: str \| Path \| None = None, *, update: bool \| None = None, urlconf: str \| None = None, hint: str = '') -> None
client_as function A client authenticated as a new user holding roles (None alone: anonymous). client_as(*roles: str \| None, user: Any = None) -> KarniAPIClient
query_budget function Fail with the grouped duplicate report if the block runs more than limit queries. query_budget(limit: int, *, label: str = 'block', using: str \| None = None) -> Iterator[QueryRecorder]
user_with_roles function A new user (generated factory, or the explicit one) holding roles (Django groups). user_with_roles(*roles: str, **fields: Any) -> Any

Model metadata (class Karni)

A model declares what Karni's data-driven layers need to know in a nested class Karni, next to Meta. Every key is optional. Modules add their own keys; the From column says which package reads it.

class Lead(BaseModel):
    class Karni:
        owner_field = "workspace__members"
        audit = True
Key From Default What it controls Example
owner_field karni created_by The field or path (through foreign keys and many-to-manys) that leads to the user who owns a row; the own scope filters on it. owner_field = "workspace__members"
scope_paths karni {} Named paths a role's related:<key> scope compares with the same key on the user. scope_paths = {"branch": "branch", "region": "branch__region"}
actions karni () Permission actions besides view, add, change and delete; roles grant them like any other. actions = ["approve", "export"]
field_roles karni {} Which roles see and change a field; the API, the admin and exports hide it from the rest. field_roles = {"salary": {"read": ["hr", "manager"], "write": ["hr"]}}
audit karni False Record field-level change history (who, when, old and new value): True for every field, or a list of field names. audit = True
sensitive karni () Fields the change history records as changed but never with their values, on top of names matching KARNI_AUDIT_REDACT. sensitive = ["iban"]
natural_key karni first unique slug, char or email field, else the primary key The field that names a row to people: imports refer to related rows by it and exports write relations with it. natural_key = "sku"
import_key karni natural_key The column an import matches existing rows on, so a row with a known key is updated instead of created. import_key = "number"
import_lookup karni {} For a relation field, the field of the related model that its import column carries. import_lookup = {"customer": "email"}
import_transform karni None A callable, or its dotted path, that rewrites each imported row before validation. import_transform = "shop.imports.clean_order_row"
after_import karni None A callable, or its dotted path, run once an import commits, with the result. after_import = "shop.imports.reindex"
requires_approval karni-approvals unset: never held Hold changes for a second person's approval: a list of guarded fields, or True for any change, create and delete included. requires_approval = ["amount", "status"]
approvals_required karni-approvals KARNI_APPROVALS_REQUIRED (1) Approvals a pending change needs before it is applied. approvals_required = 2
approver_roles karni-approvals KARNI_APPROVALS_APPROVER_ROLES Roles that may approve, on top of holding the approve permission. approver_roles = ["finance"]
approval_timeout karni-approvals KARNI_APPROVALS_TIMEOUT_HOURS (0: never) How long a change may wait: a timedelta, or hours as an integer. approval_timeout = timedelta(hours=48)
approval_on_timeout karni-approvals KARNI_APPROVALS_ON_TIMEOUT (expire) What a timeout does: expire the change, or escalate it to the escalation roles. approval_on_timeout = "escalate"
approval_escalation_roles karni-approvals KARNI_APPROVALS_ESCALATION_ROLES Roles notified when a change escalates. approval_escalation_roles = ["cfo"]

Settings

karni_settings(globals()) in config/settings.py defines every setting below; anything assigned after the call wins. Each is read from the environment variable in the Env column when one is named. The Django and DRF settings Karni derives are described in settings.

Accounts & auth

Setting Env Default What it controls
KARNI_AUTH_MODE KARNI_AUTH_MODE "jwt" jwt (SimpleJWT token pairs) or session (cookie auth + CSRF bootstrap). See accounts.
KARNI_2FA KARNI_2FA False Enable TOTP two-factor auth (django-otp; needs karni[2fa]).
KARNI_SOCIAL_AUTH KARNI_SOCIAL_AUTH False Enable social login through django-allauth (karni[social]; session mode only).
KARNI_SOCIAL_PROVIDERS KARNI_SOCIAL_PROVIDERS [] Comma-separated allauth providers to enable, e.g. google,github.
KARNI_FRONTEND_URL KARNI_FRONTEND_URL "http://localhost:5173" The SPA's origin: used in account emails and, in session mode, added to CSRF_TRUSTED_ORIGINS — the browser's origin must match it.
KARNI_ACCOUNTS_EMAIL_BACKEND — "karni.accounts.emails.DjangoEmailBackend" Dotted path of the AccountsEmailBackend that sends verification and reset emails.
KARNI_ACCOUNTS_AUTO_LOGIN — False Sign a user in right after registration (skips email verification as a gate).
KARNI_ACCOUNTS_DEFAULT_ROLES KARNI_ACCOUNTS_DEFAULT_ROLES [] Roles (Django groups) every self-registered user starts with, e.g. attendee.
KARNI_ACCOUNTS_HONEYPOT_FIELD KARNI_ACCOUNTS_HONEYPOT_FIELD "website" Name of the hidden registration field no person fills in; a request that fills it is accepted (202) without creating a user. "" turns the honeypot off. See security.
KARNI_ROLES — { "staff": {"name": "Staff", "system": True, "grants": {"*": "all"}}, "user": {"name": "User", "grants": {"*": "own"}}, "customer": {"name": "Customer", "grants": {"*": "own"}}} The project's roles and grants: {slug: {name, description, system, grants: {"app.model:action": scope}}}; * wildcards, scopes all / own / related:<key> / custom:<name>. See roles.
KARNI_ROLES_AUTOSYNC KARNI_ROLES_AUTOSYNC True Reconcile the role tables with KARNI_ROLES after every migrate (karni sync-roles does the same by hand).
KARNI_ACCOUNTS_TOKEN_TTL — { "verify_email": 86400, "reset_password": 3600, "change_email": 86400} Lifetime per one-time token purpose (verify_email, reset_password, ...), as timedeltas.
KARNI_ACCOUNTS_LINKS — { "verify_email": "{frontend}/auth/verify-email?token={token}", "reset_password": "{frontend}/auth/reset-password?token={token}", "change_email": "{frontend}/auth/confirm-email-change?token={token}"} Frontend URL template per token purpose, with {frontend} and {token} placeholders.
KARNI_AUTH_THROTTLE_RATES — computed (_auth_throttle_rates(env)) Per-scope auth throttle overrides as a JSON object; null disables a scope.

Database & cache

Setting Env Default What it controls
KARNI_CACHE KARNI_CACHE "redis" redis (the default cache at REDIS_URL) or locmem, an in-process cache for a host run without Redis (development only: throttle counters are then per process).

Files & storage

Setting Env Default What it controls
KARNI_STORAGE KARNI_STORAGE "local" local (filesystem) or s3 (any S3-compatible service; needs karni[s3]).

API (karni.api)

Setting Env Default What it controls
KARNI_SEARCH_PROVIDER KARNI_SEARCH_PROVIDER "karni.api.search.postgres.PostgresSearchProvider", Dotted path of the SearchProvider behind ?search=; the PostgreSQL provider falls back to portable search on other databases.
KARNI_API_MAX_EXPAND_DEPTH KARNI_API_MAX_EXPAND_DEPTH 2 Deepest ?expand= path allowed (levels of nesting).
KARNI_API_ROUTER KARNI_API_ROUTER "karni.api.routers.get_default_router" Zero-argument factory returning the router karni.api.urls exposes.

Performance (ADR 0001 Decision 3)

Setting Env Default What it controls
KARNI_MAX_PAGE_SIZE KARNI_MAX_PAGE_SIZE 100 Hard cap on ?page_size= for every Karni paginator.
KARNI_MAX_PAGE_SIZE_FLOOR KARNI_MAX_PAGE_SIZE_FLOOR 25 The lowest value KARNI_MAX_PAGE_SIZE may take (the frontend's page size).
KARNI_QUERY_BUDGET — { "list": 8, "retrieve": 8, "create": 12, "update": 12, "partial_update": 12, "destroy": 10, "*": 15} Queries allowed per BaseViewSet action ("*" for any other); merged over the defaults. Per viewset: query_budget = Budget(...).
KARNI_QUERY_BUDGET_MODE KARNI_QUERY_BUDGET_MODE "off" off, warn (log an exceeded budget) or raise (fail the request; what the pytest plugin sets).
KARNI_QUERY_LOG KARNI_QUERY_LOG False Log each request's query count, duplicates and SQL time (QueryLogMiddleware).
KARNI_SEED_LOGINS KARNI_SEED_LOGINS {} Extra karni seed logins with their roles, as JSON: {"organizer@example.com": ["organizer"]}.
KARNI_API_CACHE KARNI_API_CACHE False Cache BaseViewSet list and retrieve responses (karni.api.caching).
KARNI_API_CACHE_TTL KARNI_API_CACHE_TTL 300 Seconds a cached response lives.
KARNI_API_CACHE_ALIAS KARNI_API_CACHE_ALIAS "default" The Django cache alias responses are stored in.

Admin (karni.admin + django-unfold)

Setting Env Default What it controls
KARNI_ADMIN_AUTODISCOVER KARNI_ADMIN_AUTODISCOVER True Register every unregistered model in the admin at startup (karni.admin).
KARNI_ADMIN_LOGO KARNI_ADMIN_LOGO env("KARNI_ADMIN_LOGO", default="") or None Logo for the admin header and login: karni for Karni's mark, or {"light": <static path>, "dark": ..., "icon": ...}; unset leaves the text header. See admin.
KARNI_ADMIN_PATH KARNI_ADMIN_PATH "admin/" URL path the scaffold mounts the Django admin at (trailing slash); set it to something unguessable in production.
KARNI_ADMIN_EXCLUDE KARNI_ADMIN_EXCLUDE [ "sessions", "contenttypes", "admin", "auth.Permission", "django_celery_results.ChordCounter"], App labels or app_label.ModelName entries the admin leaves out.
KARNI_ADMIN_LIST_DISPLAY_MAX — 6 Columns a derived admin changelist shows at most.
KARNI_ADMIN_LIST_FILTER_MAX — 6 Sidebar filters a derived admin shows at most.
KARNI_ADMIN_SEARCH_MAX — 5 Fields a derived admin searches at most.
KARNI_ADMIN_SITE — "django.contrib.admin.site" Dotted path of the admin site models are registered on.
KARNI_ADMIN_BASE — "unfold.admin.ModelAdmin" Dotted path of the ModelAdmin every derived admin is built on.
KARNI_MATRIX_EXCLUDE KARNI_MATRIX_EXCLUDE [ "admin", "auth", "contenttypes", "sessions", "sites", "django_celery_beat", "django_celery_results", "token_blacklist", "django_otp", "otp_totp", "otp_static", "account", "socialaccount", "karni_audit", "karni_transfer", "karni_accounts.OneTimeToken", "karni_accounts.Permission"], App labels or app_label.ModelName entries the permission matrix leaves out (framework internals by default).

Karni knobs

Setting Env Default What it controls
KARNI_HEALTH_CHECKS — [ "karni.health.checks.check_database", "karni.health.checks.check_cache", "karni.health.checks.check_celery"] Dotted paths of the checks /health/ready/ runs (database, cache, Celery by default).
KARNI_HEALTH_CELERY_TIMEOUT — 1.0 Seconds the readiness check waits for a Celery worker to answer.
KARNI_THUMBNAILER — "karni.storage.thumbnails.make_thumbnail" Dotted path of the function karni.storage.thumbnails renders thumbnails with.
KARNI_VALIDATE — True Validate the merged settings at startup (karni.conf.validation); False skips it.

Import and export (karni.transfer)

Setting Env Default What it controls
KARNI_IMPORT_INLINE_ROWS KARNI_IMPORT_INLINE_ROWS 500 Files with at most this many rows import inside the request; larger ones run in Celery with progress.
KARNI_IMPORT_MAX_ROWS KARNI_IMPORT_MAX_ROWS 100_000 An upload with more rows than this is refused (E803); split the file.
KARNI_EXPORT_MAX_ROWS KARNI_EXPORT_MAX_ROWS 100_000 An export with more rows than this is refused (E803); narrow the filters.

Change history (karni.audit)

Setting Env Default What it controls
KARNI_AUDIT_ASYNC KARNI_AUDIT_ASYNC False Write change-history entries from a Celery task instead of inside the request (history becomes eventually consistent). See change history.
KARNI_AUDIT_RETENTION_DAYS KARNI_AUDIT_RETENTION_DAYS None Days of change history to keep; None (the default) keeps everything and schedules no pruning.
KARNI_AUDIT_REDACT KARNI_AUDIT_REDACT ["password", "token", "secret", "key"] Field-name substrings whose values are never recorded in change history, only that they changed (default password,token,secret,key).

Events & schedules (karni.events / karni.schedule)

Setting Env Default What it controls
KARNI_EVENTS_AUTODISCOVER KARNI_EVENTS_AUTODISCOVER True Import every app's events.py and listeners.py at startup.
KARNI_SCHEDULES_AUTODISCOVER KARNI_SCHEDULES_AUTODISCOVER True Import every app's schedules.py at startup.
KARNI_ARCHITECTURE — { "skip_apps": [], "sql_allowed": ["*/sql/*", "*/migrations/*"], "http_allowed": ["*/services/*", "*/providers/*", "*/tests/*", "conftest.py"], "dup": {"min_statements": 8, "exclude": ["*/migrations/*"]}} Configuration of the architecture rules: skip_apps, sql_allowed, http_allowed, dup. See enforcement.

Module: karni-approvals

Setting Env Default What it controls
KARNI_APPROVALS_REQUIRED KARNI_APPROVALS_REQUIRED 1 Approvals a pending change needs before it is applied (per model: approvals_required).
KARNI_APPROVALS_APPROVER_ROLES KARNI_APPROVALS_APPROVER_ROLES [] Roles that may approve, on top of holding the approve permission; [] = any holder of karni_approvals.pendingchange:approve (per model: approver_roles).
KARNI_APPROVALS_TIMEOUT_HOURS KARNI_APPROVALS_TIMEOUT_HOURS 0 Hours before a pending change times out; 0 = never (per model: approval_timeout).
KARNI_APPROVALS_ON_TIMEOUT KARNI_APPROVALS_ON_TIMEOUT "expire" What a timeout does: "expire" the change, or "escalate" (notify the escalation roles and extend by the same timeout). Per model: approval_on_timeout.
KARNI_APPROVALS_ESCALATION_ROLES KARNI_APPROVALS_ESCALATION_ROLES [] Roles notified on escalation (per model: approval_escalation_roles).
KARNI_APPROVALS_NOTIFY KARNI_APPROVALS_NOTIFY "karni_approvals.notify.email_checkers" Dotted path called as fn(pending_change) from the Celery task to notify the checkers; the default emails them. "" turns notification off.
KARNI_APPROVALS_FROM_EMAIL KARNI_APPROVALS_FROM_EMAIL "" Sender address for the default notifier; "" uses DEFAULT_FROM_EMAIL.

Module: karni-impersonation

Setting Env Default What it controls
KARNI_IMPERSONATION_MAX_MINUTES KARNI_IMPERSONATION_MAX_MINUTES 60 Hard limit on an impersonated session; it ends by itself after this.
KARNI_IMPERSONATION_BLOCKED KARNI_IMPERSONATION_BLOCKED DEFAULT_BLOCKED Actions, models, permissions and paths blocked while impersonating.
KARNI_IMPERSONATION_REQUIRE_REASON KARNI_IMPERSONATION_REQUIRE_REASON True A reason string is required to start (default on: this is customer data).
KARNI_IMPERSONATION_NOTIFY_TARGET KARNI_IMPERSONATION_NOTIFY_TARGET True Email the impersonated user when a session starts (default on).
KARNI_IMPERSONATION_FROM_EMAIL KARNI_IMPERSONATION_FROM_EMAIL "" Sender address for that email; "" uses DEFAULT_FROM_EMAIL.
KARNI_IMPERSONATION_BANNER KARNI_IMPERSONATION_BANNER True Inject the banner into every HTML page served to an impersonated session.
KARNI_IMPERSONATION_LANDING KARNI_IMPERSONATION_LANDING "/" Where the admin's Impersonate button sends the impersonator afterwards.

Module: karni-payments

Setting Env Default What it controls
KARNI_PAYMENTS_PROVIDER KARNI_PAYMENTS_PROVIDER "stripe" Active provider: "stripe", "razorpay", "test" or any dotted path to a karni_payments.providers.base.PaymentProvider subclass (the escape hatch for custom gateways).
KARNI_PAYMENTS_CURRENCY KARNI_PAYMENTS_CURRENCY "usd" Default ISO currency code used when create_checkout gets none.
KARNI_PAYMENTS_CHECKOUT_VALIDATOR — "karni_payments.services.default_checkout_validator" Dotted path called with (user=, amount=, currency=, metadata=) BEFORE a Payment is created — the server-side pricing authority. The default is a pass-through; production projects should replace it (never trust client amounts).
KARNI_PAYMENTS_EVENT_HANDLER — "karni_payments.services.apply_event" Dotted path of the webhook state machine applied by the Celery task.
KARNI_PAYMENTS_ALLOWED_REDIRECTS KARNI_PAYMENTS_ALLOWED_REDIRECTS [] Extra URLs whose origins are allowed as checkout success/cancel redirects, on top of KARNI_FRONTEND_URL's origin.
STRIPE_SECRET_KEY STRIPE_SECRET_KEY "" Provider credentials — blank by default, validated lazily at use.
STRIPE_WEBHOOK_SECRET STRIPE_WEBHOOK_SECRET "" Provider credentials — blank by default, validated lazily at use.
RAZORPAY_KEY_ID RAZORPAY_KEY_ID "" Provider credentials — blank by default, validated lazily at use.
RAZORPAY_KEY_SECRET RAZORPAY_KEY_SECRET "" Provider credentials — blank by default, validated lazily at use.
RAZORPAY_WEBHOOK_SECRET RAZORPAY_WEBHOOK_SECRET "" Provider credentials — blank by default, validated lazily at use.

Components and hooks

Every value @karni/ui exports, read from its barrel (src/index.ts) with the TypeScript compiler. Required props are the props without a default; every component takes more (UI guide).

Components

Component Required props What it renders Source
Button — Themed button. button.tsx
Input — Themed text input; every native <input> prop passes through. input.tsx
Textarea — Themed multi-line text input; every native <textarea> prop passes through. textarea.tsx
Select value: string \| null \| undefined, options: readonly SelectOption[] Styled select for a list of options, with null for no selection. select.tsx
SelectRoot — Radix Select root, for composing a custom select from the styled parts. select.tsx
SelectGroup — Groups related options inside SelectContent. select.tsx
SelectValue — Shows the selected option's label inside SelectTrigger. select.tsx
SelectTrigger — The styled button that opens the select. select.tsx
SelectContent — The styled dropdown panel that lists the options. select.tsx
SelectLabel — A heading inside a SelectGroup. select.tsx
SelectItem value: string One selectable option; value is what the select reports. select.tsx
SelectSeparator — A divider line between option groups. select.tsx
Checkbox — Themed checkbox (Radix Checkbox) with a check mark indicator. checkbox.tsx
Switch — Themed on/off toggle (Radix Switch). switch.tsx
Label — Themed form label (Radix Label) that dims when its field is disabled. label.tsx
Tabs — Tab set root (Radix Tabs); holds the selected value. tabs.tsx
TabsList — The row of tab buttons. tabs.tsx
TabsTrigger value: string One tab button; value names the panel it opens. tabs.tsx
TabsContent value: string The panel shown while its value is the selected tab. tabs.tsx
DropdownMenu — Dropdown menu root (Radix DropdownMenu). dropdown-menu.tsx
DropdownMenuTrigger — The element that opens the menu. dropdown-menu.tsx
DropdownMenuGroup — Groups related menu items. dropdown-menu.tsx
DropdownMenuPortal — Renders the menu into document.body. dropdown-menu.tsx
DropdownMenuContent — The styled menu panel. dropdown-menu.tsx
DropdownMenuItem — One menu item; onSelect runs the action. dropdown-menu.tsx
DropdownMenuCheckboxItem — A menu item with a check mark that toggles checked. dropdown-menu.tsx
DropdownMenuLabel — A non-interactive heading inside the menu. dropdown-menu.tsx
DropdownMenuSeparator — A divider line between menu items. dropdown-menu.tsx
Skeleton — Loading placeholder block; size it with width/height utility classes. skeleton.tsx
ResourceTable resource: string Schema-driven data table for one resource: columns, sorting, filters, search, pagination, row actions and bulk select, all derived from the OpenAPI schema. ResourceTable.tsx
ResourceForm resource: string Schema-driven create and edit form for one resource: inputs, validation and server errors derived from the OpenAPI schema. ResourceForm.tsx
ResourceDetail resource: string, id: string \| number Schema-driven read view of one row: every readable field, labelled and formatted. ResourceDetail.tsx
ResourceSelect resource: string, value: string \| number \| readonly (string \| number)[] \| null, onChange: (value: string \| number \| (string \| number)[] \| null) => void Searchable picker for a related row, querying the related resource as you type. ResourceSelect.tsx
ChangeHistory resource: string, id: string \| number The field-level change history of one row: when, by whom, what changed from what to what. ChangeHistory.tsx
ResourceImport resource: string Import a CSV or XLSX file into a resource: upload, map columns to fields, preview (the mandatory dry run — N created / updated / skipped with every row's errors), then commit. ResourceImport.tsx
KarniProvider client: KarniClient Provides the Karni client and a TanStack QueryClient to everything inside it; render it once at the app root. provider.tsx
AuthProvider — Holds the signed-in user and attaches credentials (JWT or session) to every request; render it inside KarniProvider. provider.tsx
AuthGuard — Renders its children only for a signed-in user, optionally with one of the given roles; otherwise its fallback. AuthGuard.tsx
LoginForm — Email and password sign-in form with inline errors and a callback for the two-factor step. LoginForm.tsx
Page title: React.ReactNode Standard page chrome: an <h1> title row with an actions slot, then the content. page.tsx
Layout — App shell: sidebar navigation + header slot + content area. layout.tsx
KarniMark — The Karni mark, inline SVG — no asset to load or bundle. karni-mark.tsx
Modal title: React.ReactNode Themed Radix Dialog. modal.tsx
ModalClose — Escape hatch for footer buttons: any element wrapped in <ModalClose asChild> closes the modal on click (re-exported Radix Dialog.Close). modal.tsx
Toaster — Renders the toasts pushed through useToast(). toaster.tsx
EmptyState title: React.ReactNode Centered placeholder for empty collections: icon, title, description and an action slot. empty-state.tsx
FileUpload — Drag-and-drop or click-to-browse file picker that shows the current file and the pending ones. FileUpload.tsx
Stat label: React.ReactNode, value: React.ReactNode KPI tile: label, value, optional delta and icon. stat.tsx
DataChart kind: DataChartKind, title: string Minimal line/bar chart driven by {label, value} points or named series. data-chart.tsx

Hooks

Hook Parameters What it does Source
useChangeHistory resource: string, id: string \| number, params?: ChangeHistoryParams \| undefined, options?: Omit<UseQueryOptions<ChangeHistoryPage, Error, ChangeHistoryPage, readonly unknown[]>, "queryKey" \| "queryFn"> \| undefined Fetches one row's field-level change history (GET /<resource>/<id>/history/) for models with audit = True. useChangeHistory.ts
useImportDescribe resource: string, options?: Omit<UseQueryOptions<ImportDescription, Error, ImportDescription, readonly unknown[]>, "queryKey" \| "queryFn"> \| undefined Fetches what a resource's import accepts: its columns, required fields and allowed values. useResourceImport.ts
useImportJob resource: string, jobId: string \| undefined, options?: UseImportJobOptions \| undefined Fetches one import job and polls it while it runs. useResourceImport.ts
useImportActions resource: string Upload, preview and commit mutations for a resource's import jobs. useResourceImport.ts
useResourceList resource: string, params?: ResourceListParams \| undefined, options?: Omit<UseQueryOptions<Paginated<T>, Error, Paginated<T>, readonly unknown[]>, "queryKey" \| "queryFn"> \| undefined Fetches one page of a resource's list (GET /<resource>/) with filters, search, ordering and ?expand=. useResourceList.ts
useResourceItem resource: string, id: string \| number, params?: ResourceItemParams \| undefined, options?: Omit<UseQueryOptions<T, Error, T, readonly unknown[]>, "queryKey" \| "queryFn"> \| undefined Fetches one row of a resource (GET /<resource>/<id>/), optionally expanded. useResourceItem.ts
useResourceMutations resource: string, options?: ResourceMutationsOptions \| undefined Create, update and remove mutations for a resource; each success refreshes the resource's cached lists and items. useResourceMutations.ts
useKarniClient — Returns the Karni client from KarniProvider; throws a descriptive error outside one. provider.tsx
useSchema resource: string Returns one resource's parsed schema (fields, types, relations, operations) from the backend's OpenAPI document, fetched once per app. useSchema.ts
useAuth — Returns the signed-in user, auth status, login and logout; throws a descriptive error outside AuthProvider. provider.tsx
usePermission — isAuthenticated and hasRole(...names) for the signed-in user. usePermission.ts
useToast — Imperative toast API + the live toast list. use-toast.ts

Functions and constants

Name Kind What it is Source
cn function Merges class names: clsx semantics for conditionals plus tailwind-merge conflict resolution, so consumer-supplied className always wins over a component's own Tailwind classes. cn.ts
registerComponentRole function Claims a UI role (such as data-table) for a component, so a second component with the same role fails the build. roles.ts
getRoleRegistry function Snapshot of the current registry ({ role: component }) for tooling. roles.ts
variants function Builds a typed class-name function from variant definitions, the way every themed component declares its looks. variants.ts
buttonVariants function Exported for composition (escape hatch): render any element as a button lookalike with className={buttonVariants({ variant: "outline" })}. button.tsx
KARNI_SELECT_EMPTY constant The value that stands for "nothing selected" inside Radix Select, which reserves the empty string. select.tsx
changeHistoryKey function The query key of one row's change history. useChangeHistory.ts
downloadResourceExport function Fetches GET /<resource>/export/?format=… with the list's query and saves it through a temporary object URL. useResourceImport.ts
downloadImportTemplate function Fetches GET /<resource>/import/template/?format=… and saves it (the headers, an example, allowed values). useResourceImport.ts
importDescribeKey function The query key of a resource's import description. useResourceImport.ts
importJobKey function The query key of one import job. useResourceImport.ts
isImportInFlight function True while an import job is previewing or committing (the states worth polling). useResourceImport.ts
displayLabel function Returns the first non-empty string among the name → title → label → email → str keys, falling back to String(id) and finally "". displayLabel.ts
AUDIT_FIELDS constant The audit field names (created_at, created_by, …) that tables hide and detail views group last. audit-fields.ts
karniKeys constant TanStack Query keys for the schema, a resource, its lists and its items; invalidate karniKeys.resource(name) to refresh every view of a resource. keys.ts
createKarniClient function Creates the fetch client every hook in this package uses: base URL, JSON and multipart bodies, DRF errors as KarniApiError, and auth hooks. client.ts
KarniApiError class Thrown for non-2xx responses; data carries the parsed DRF error body. client.ts
zodFor function Builds a Zod validator for a resource's create or update payload from its schema. zod.ts
createLocalStorageTokenStorage function Keeps the refresh token in localStorage; the default storage. storage.ts
createMemoryTokenStorage function In-memory adapter: most secure (nothing persisted anywhere), but a page reload logs the user out. storage.ts
toastVariants function Per-variant toast styling — colors resolve through --karni-* tokens only (success leans on primary, error on destructive, info on muted). toaster.tsx

Modules

Optional apps for anything with a RAM cost or a separate service. karni add <name> adds the dependency to pyproject.toml, wires the app into INSTALLED_APPS and records the module's agent guidance (modules).

Module Status Install UI package Scope
karni-approvals experimental karni add approvals @karni/approvals-ui A second person approves high-value changes before they are written, with a queue, an API, notifications and a full audit trail.
karni-impersonation experimental karni add impersonation @karni/impersonation-ui 'Log in as user' for support, done safely — an explicit permission, no privilege escalation, a banner, a session record, both identities in the audit trail, blocked actions and a time limit.
karni-payments stable karni add payments @karni/payments-ui Hosted checkout, refunds and signature-verified webhooks for Stripe and Razorpay, with a test provider for CI.
karni-tenancy planned — — documented

Error codes

Every code Karni, its modules, the CLI, the agent rules and the ESLint plugin can report. The error page has why each happens and the fix; karni explain <code> prints the same offline.

Karni (E, W)

Code Kind Meaning
E001 check A soft-delete model's default manager shows deleted rows
W001 warning A registered scope is switched off by KARNI_SCOPES
E002 check A scope or the KARNI_SCOPES setting is invalid
E003 exception objects.without() names no scope, or an unknown one
E004 exception objects.without() needs a reason
E005 exception without() was called on a queryset
E006 exception A scope's context is not bound
E007 exception Karni.audit or Karni.sensitive is malformed
E101 check A viewset declares no access
W101 warning A hand-written serializer duplicates the one Karni generates
E102 exception access is not an Access(...) or Unscoped(...) declaration
E103 exception A predicate was expected but something else was given
E104 exception Owned() does not name a path to the user
E105 exception HasRole() names no roles
E106 exception A Where() function is invalid
E107 exception Unscoped() needs a reason
E109 exception Access predicates were combined with and / or / not
E110 exception A viewset has neither model nor queryset
E111 exception Pagination was switched off without Unpaginated(...)
E112 exception pagination_class is not a Karni paginator
E113 exception Unpaginated(...) is invalid
E114 exception Both pagination and pagination_class are declared
E115 exception A query Budget(...) is invalid
E116 exception query_budget names unknown actions
E117 exception A request ran more SQL queries than its budget
E118 exception KARNI_QUERY_BUDGET_MODE is invalid
E119 exception Cache(...) or Uncached(...) is invalid
E120 exception Query hints are invalid
E121 exception optimize must be True or False
E122 exception APIMeta declares both fields and exclude
E123 exception APIMeta names fields the serializer does not have
E124 exception A field is both read-only and write-only
E125 exception An expandable field is not a relation
E126 api The ?expand= parameter names an unknown or invalid path
E127 api The ?expand= path is too deep
E128 check KARNI_API_ROUTER cannot be imported
E129 test List or retrieve queries grow with the number of rows (N+1)
E130 test An opt-out marker has no reason
E131 check search_fields names a write-only field
E201 exception KARNI_ACCOUNTS_EMAIL_BACKEND is not an accounts email backend
E202 exception No KARNI_ACCOUNTS_LINKS entry for a token purpose
E203 exception create_user() or create_superuser() was called with invalid arguments
E204 exception Unknown one-time token purpose
E205 exception KARNI_ROLES is malformed
E206 exception A grant in KARNI_ROLES cannot be resolved
E207 exception user.can() was given an action without a model
E208 api The request lacks the permission @requires names
E209 exception Can() was given a bad permission, or one for another model
E210 exception scope_exempt = True without a reason
E211 exception Granted() has no permission for this action or viewset
E212 check Karni.field_roles is malformed, names a missing field or an undeclared role
E213 api The permission matrix refused a change
E214 check A role's scope names a path the model does not have
E301 exception KARNI_ADMIN_BASE is not a ModelAdmin class
E302 exception AdminMeta.inline_on is invalid
E303 exception KARNI_ADMIN_LOGO is not a logo declaration
E401 exception A queued listener is not a module-level function
E402 exception A listener registration is invalid
E403 exception A Celery task cannot be found
E404 exception A schedule declaration is invalid
E405 exception A schedule's interval or cron expression is invalid
E406 check Two schedules share a name
E407 exception No schedule has that name
E408 exception A scheduled task failed when run by hand
E501 exception An optional dependency is not installed
W501 warning The throttle cache is private to each process
E502 exception Unknown settings profile
E503 exception A module's settings fragment is invalid
E504 exception Karni settings failed validation
E505 check The welcome page is reachable with DEBUG off
E506 exception A setting names a dotted path that cannot be imported
E507 exception KARNI_CACHE names an unknown cache backend
E601 test The resource under test is not routed
E602 test The API does not behave as its access declaration promises
E603 exception assert_crud(skip=...) names unknown checks
E604 test A request raised an exception during assert_crud
E605 exception assert_isolated cannot isolate this resource
E606 test Rows leak to a user who must not see them
E607 test There is no OpenAPI schema snapshot yet
E608 test The OpenAPI schema changed
E609 exception No factory can be built for this model
E610 exception A factory cannot generate a value for a field
E611 exception Required relations form a loop
E612 exception Seeding keeps hitting a database constraint
E613 exception Seeding a row failed
E614 exception karni seed was given invalid arguments
E615 exception karni seed refuses to run with DEBUG = False
E701 exception karni.rules.py is invalid
E702 exception karni check found violations
E703 exception A source file cannot be parsed
E704 exception Karni's changelog is invalid
E801 api An import cannot proceed as asked
E802 api The file format is not supported
E803 api Too many rows for one import or export

Modules

Code Kind Meaning
APR001 api The change was sent for approval instead of being written
APR002 api The maker cannot be the checker
APR003 api This user may not decide this pending change
APR004 api This checker has already decided this change
APR005 api The pending change is no longer pending
APR006 api The change could not be applied: the row changed since it was proposed
APR007 check The model's approval policy is malformed
IMP001 api Impersonation needs the explicit impersonate permission
IMP002 api The target holds something the impersonator does not
IMP003 api A reason is required to impersonate
IMP004 api This user cannot be impersonated now
IMP005 api This request is not part of an impersonated session
IMP006 api This action is blocked while impersonating
IMP007 check karni-impersonation is installed but not wired
IMP008 check KARNI_IMPERSONATION_BLOCKED is malformed
PAY001 exception KARNI_PAYMENTS_PROVIDER cannot be loaded
PAY002 exception No payment provider has that name
PAY003 exception The payment provider's SDK is not installed
PAY004 exception The payment provider's API credentials are not set
PAY005 exception The webhook signing secret is not set
PAY006 exception A webhook failed signature verification
PAY007 exception The currency or amount cannot be charged exactly
PAY008 api A checkout redirect URL points at an origin that is not allowed
PAY009 exception A webhook event matches no local payment

The karni command (C)

Code Kind Meaning
C001 cli Not inside a Karni project
C002 cli No way to run the project's Python
C003 cli Docker is not installed
C004 cli These options cannot be combined
C005 cli This karni-cli install is incomplete
C006 cli An optional tool is not installed
C007 cli A configuration value is invalid
C008 cli A file cannot be written
C009 cli A command in the project failed
C010 cli The path is not a file inside the project
C011 cli pytest is not installed in the project interpreter
C101 cli The file karni make would write already exists
C102 cli Which app? (none, unknown, or several)
C103 cli The name is not valid here
C104 cli A field spec is invalid
C105 cli Something the generator builds on does not exist yet
C106 cli An option value is invalid
C107 cli The new code would clash with existing code
C108 cli A karni marker is missing from a project file
C109 cli An explicit serializer needs a reason
C110 cli A project file cannot be parsed
C199 cli Internal error in karni
C201 cli karni upgrade needs a clean git work tree
C202 cli karni upgrade does not downgrade
C203 cli That is not a version number
C204 cli No such rule or procedure
C205 cli The agent adapter configuration is invalid
C206 cli The OpenAPI schema cannot be turned into types
C207 cli karni add cannot edit pyproject.toml

Agent rules (K)

Code Kind Meaning
K001 rule karni context first; trust it.
K002 rule Rules beat other frameworks' habits.
K003 rule Model changed: make migrate; karni types.
K004 rule karni rules --check FILE; karni check pre-commit.
K005 rule Opt-outs need an inline reason.
K006 rule Procedures: karni rules --procedure NAME.
K007 rule Commands: karni --help.
K009 rule Scaffold with karni make.
K101 rule Models inherit BaseModel or compose its mixins.
K102 rule Viewsets inherit BaseViewSet.
K103 rule No ModelAdmin; use AdminMeta.
K104 rule Raw SQL only under /sql/.
K105 rule HTTP clients only in /services/.
K106 rule No Meta-only ModelSerializer.
K107 rule No duplicated code.
K108 rule Off-request writes: current_user(u).
K109 rule Viewsets declare access.
K110 rule Row rules: in access.
K111 rule Paginate lists; no N+1.
K113 rule Audited models: no update()/bulk_update().
K201 rule Compose @karni/ui; no raw table/form/input.
K202 rule Pages: max 50 lines.
K203 rule Import only the @karni/ui barrel.
K204 rule Data only via Karni hooks; no fetch/axios.
K205 rule No duplicated frontend code.
K301 rule Domain code only; extend Karni.
K302 rule Never write auth/users, admin, serializers, routers, settings, infra, tables, forms.
K303 rule Extend via props/methods; never fork.
K304 rule No extra state/form/table/HTTP/CSS library.
K305 rule Per entity: models.py and viewsets.py.
K306 rule Pages: frontend/src/pages/.
K307 rule Cross-project code: a Karni module.
K308 rule Modules: configure via settings.

ESLint plugin (karni/…)

Code Kind Meaning
karni/no-raw-elements lint Raw <table>/<form>/<input>/<select>/<textarea> in project code
karni/max-page-lines lint A page file is longer than its cap (50 lines by default)
karni/no-deep-imports lint An import reaches into @karni/ui internals
karni/no-direct-http lint fetch / axios / XMLHttpRequest / WebSocket outside the api client

Make targets

The Makefile karni new writes. Every target is a thin alias for a karni command.

Target Runs
make up karni serve -d
make down docker compose down
make serve karni serve
make migrate karni make migration; karni migrate
make types karni types
make check karni check; karni rules sync --check
make rules karni rules
make agents karni rules sync
make context karni context
make test karni test
make shell karni shell
make routes karni routes
make schedules karni schedule list
make superuser karni createsuperuser
make logs docker compose logs -f django worker beat

Slash commands

One per routine task, written for each agent tool by karni new and karni rules sync; karni rules --procedure <name> prints the same steps.

Command Arguments What it does
/new-model <ModelName> [in <app>] [fields...] Add a domain model the Karni way (model + viewset, nothing else)
/new-page <page purpose> [resource] Add a frontend page composing @karni/ui (50 lines or fewer)
/add-module <module> (e.g. payments) Install an optional Karni module and wire it into settings
/new-module <app_name> Create a new local Django app following Karni conventions
Tool Written to
Claude Code .claude/commands/<name>.md
Cursor .cursor/commands/<name>.md
Windsurf .windsurf/workflows/<name>.md
GitHub Copilot (VS Code prompt files) .github/prompts/<name>.prompt.md