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
| 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
| Argument / option |
Default |
Description |
MODULE (required) |
|
Module short name, e.g. "payments" |
karni upgrade
Upgrade across Karni versions: codemods, diff, manual-change report
| 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
| 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.
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
| 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
| 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])
| 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).
| 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.
| 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 |
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. |
| 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/…)
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 |