Configuration overview
cartapel is configured by a directory of HCL files. The directory is a self-contained bundle — config, roles, dashboards, and any custom widget/page code all live inside it — so the whole panel is one portable, versionable folder you point --config at.
There is an in-app visual builder that edits this same config, but it writes the identical HCL. The files are the source of truth.
Layout: config/ + screens/
The layout of the config directory is the layout of the sidebar. Globals live in the reserved config/ folder; everything you navigate to — tables and pages — lives under screens/, one folder per navigation group:
admin/
├── config/ # reserved — globals + shared assets, never a group
│ ├── cartapel.hcl # brand, theme, the `main` source, defaults
│ ├── auth.hcl # roles & permissions
│ ├── dashboard.hcl # dashboard widgets
│ └── widgets/ # shared custom-widget JS (served at /static)
│ └── sparkline.js
└── screens/ # every table and page lives here
├── customers/ # a sidebar group (it has a _group.hcl)
│ ├── _group.hcl # its label, icon, order, table order
│ ├── customers/ # one folder per table — the folder name is the table
│ │ └── screen.hcl # list/fields/actions (empty = introspected defaults)
│ └── subscriptions/
│ └── screen.hcl
└── overview/
├── _group.hcl
└── summary/ # a scripted page instead of a table
├── screen.hcl # module = "summary.tsx"
├── summary.tsx # the page module (co-located)
└── queries.hcl # named read-only queries the page callsConfig files are discovered recursively; load order is deterministic (files sorted by path). The rules the diagram doesn't show:
- A table is a folder holding a
screen.hcl; the folder name is the table name. An emptyscreen.hclrenders the table from introspected defaults. - A
_group.hclmakes its folder a sidebar group and sets its label, icon and order. Tables in a folder without one land in a trailing "Ungrouped" group. See Groups & navigation. - A page is the same shape — a folder whose
screen.hclsetsmodule = "<name>.tsx"(scripted) or holdspanel { }blocks (declarative), never both.page.hclis an accepted synonym for such a file. See Pages & queries. - A
queries.hclin any folder contributes named read-only queries;variables.hclandsources.hclwork the same way for template variables and extra data sources. Names must be unique across the bundle. - The file set under
screens/is closed —screen.hcl,page.hcl,_group.hcl,queries.hcl,variables.hcl,sources.hcl. Any other.hclname there is a load error.
The reserved config/ folder
config/ is special: it is never a sidebar group and is never scanned for tables. It holds exactly three global files plus a widgets/ asset folder:
| File | Contents |
|---|---|
config/cartapel.hcl | Brand, logo, locale, per_page, the secret key, theme { }, and the source "…" { } blocks. |
config/auth.hcl | role "…" { } blocks — the permission model. See Roles & permissions. |
config/dashboard.hcl | The home dashboard's panel { } blocks. See Dashboard. |
config/widgets/*.js | Shared custom-widget web components, served at /static/config/widgets/. See Pages & queries. |
Putting anything else in config/ is a loud load error. Folders whose name starts with an underscore are never treated as sidebar groups.
config/cartapel.hcl — globals
brand = "Acme Admin"
brand_logo = "https://acme.example/logo.png"
per_page = 100
locale = "en"
# The signing secret. Prefer env interpolation over a literal.
secret_key = "env:CARTAPEL_SECRET_KEY"
theme {
preset = "cartapel" # "cartapel" (default) | "django"
accent = "hsl(33 100% 50%)"
mode = "auto" # "light" | "dark" | "auto"
}
# The database cartapel reads. Exactly one postgres source must be `primary`.
source "main" {
type = "postgres"
url = "env:CARTAPEL_DB" # or a literal postgres:// url
schemas = ["public"]
primary = true
}Top-level [cartapel] keys:
| Key | Type | Description |
|---|---|---|
brand | string | Panel name, shown in the header. Defaults to cartapel. |
brand_logo | string | Logo URL, data URL, or a bundle asset filename served under /static/. |
locale | string | The instance language — picks the UI dictionary, date/number formatting and per-locale labels overrides. See Localization. |
strings | map | Override individual UI strings ({ "key" = "value" }). |
per_page | number | Default list page size — 100 when unset. A table's list.per_page overrides it. |
group_nav | string | Default sidebar mode for groups: expanded (default — every table is its own entry) or page (one entry per group; sibling tables become tabs). A group's own nav in _group.hcl overrides it. |
secret_key | string | Session-signing root. Supports env:/${}. Overridden by CARTAPEL_SECRET_KEY. Required somewhere. |
theme { } | block | Theme preset, accent, per-mode CSS token overrides, logos — see Theming. |
source "…" { } | block | A named data source. The primary postgres one is the database cartapel introspects. |
disable_sql_preview | bool | Hardening: disable the dashboard builder's ad-hoc SQL preview (admin-supplied SELECTs). Blocks arbitrary read-SQL even for admins. Default false. |
disable_webhooks | bool | Hardening: disable outbound webhook actions (an SSRF surface). Default false. |
theme { }
| Key | Description |
|---|---|
preset | Named base theme: cartapel (default) or django. |
accent / accent_btn | Shorthand accent overrides (win over the preset). |
light / dark | Per-mode maps of CSS token → value. Keys are cartapel token names without the -- prefix (page, surface, ink, accent, good, critical, …). |
mode | Force light, dark, or auto (default). |
logo_light / logo_dark | Per-mode brand logo, overriding brand_logo for that mode. |
source "…" { }
The database is declared as a named source. Define at least one postgres source and mark it primary — that is what cartapel introspects and serves.
| Key | Description |
|---|---|
type | "postgres" for the database, or "http" for a read-only JSON source a custom page can call. |
url | Connection URL (postgres) or endpoint (http). Supports env:NAME / ${NAME}. |
schemas | List of schemas to introspect (postgres). Defaults to ["public"]. |
primary | Marks the postgres source cartapel introspects. With a single postgres source it is implied; declare it explicitly when you define several. |
token_env / header | For http sources: attach a secret from this env var under header (default x-admin-token). The secret never reaches the browser. |
roles | Restrict a source to these roles (non-admins need an explicit match). |
--db postgres://… / CARTAPEL_DB overrides the primary source's URL, so the same bundle can run against dev, staging or prod by swapping one env var.
Environment interpolation
secret_key and every source's url accept env:NAME or ${NAME}, replaced at load time with the environment variable NAME. (An http source's token_env names an env var directly, no prefix.) Use it to keep secrets out of committed config:
source "main" {
type = "postgres"
url = "env:CARTAPEL_DB"
primary = true
}
secret_key = "${CARTAPEL_SECRET_KEY}"Validation & hot-reload
Config is validated as it loads, and again on every in-app edit:
- Unknown keys are rejected. Every block uses strict parsing, so a typo like
filterz = [...]is a load error, not a silent no-op. - Duplicate labeled blocks are rejected — two
field "x"blocks, or two configs for the same table, fail loudly rather than silently merging. formatandcolorare validated against their allowed vocabularies (see Fields & widgets).- Named queries must be unique across the whole bundle.
Config edits hot-reload with no restart — both through the in-app builder and by editing the files on disk (cartapel watches the config directory, debounced). A bad edit can never replace the running config: builder writes are trial-parsed first with a failed reload restoring the previous state, and a broken on-disk edit keeps the last good config and logs the error.
Round-tripping through the visual builder drops comments
The builder regenerates canonical HCL from the parsed model. If you keep comments or bespoke formatting in a file, edit it as raw HCL (in your repo or the raw editor), not through the visual form.
Next
- Tables — every block in a
screen.hcl. - Groups & navigation —
_group.hclin detail.