Skip to content

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 calls

Config 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 empty screen.hcl renders the table from introspected defaults.
  • A _group.hcl makes 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.hcl sets module = "<name>.tsx" (scripted) or holds panel { } blocks (declarative), never both. page.hcl is an accepted synonym for such a file. See Pages & queries.
  • A queries.hcl in any folder contributes named read-only queries; variables.hcl and sources.hcl work 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 .hcl name 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:

FileContents
config/cartapel.hclBrand, logo, locale, per_page, the secret key, theme { }, and the source "…" { } blocks.
config/auth.hclrole "…" { } blocks — the permission model. See Roles & permissions.
config/dashboard.hclThe home dashboard's panel { } blocks. See Dashboard.
config/widgets/*.jsShared 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

hcl
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:

KeyTypeDescription
brandstringPanel name, shown in the header. Defaults to cartapel.
brand_logostringLogo URL, data URL, or a bundle asset filename served under /static/.
localestringThe instance language — picks the UI dictionary, date/number formatting and per-locale labels overrides. See Localization.
stringsmapOverride individual UI strings ({ "key" = "value" }).
per_pagenumberDefault list page size — 100 when unset. A table's list.per_page overrides it.
group_navstringDefault 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_keystringSession-signing root. Supports env:/${}. Overridden by CARTAPEL_SECRET_KEY. Required somewhere.
theme { }blockTheme preset, accent, per-mode CSS token overrides, logos — see Theming.
source "…" { }blockA named data source. The primary postgres one is the database cartapel introspects.
disable_sql_previewboolHardening: disable the dashboard builder's ad-hoc SQL preview (admin-supplied SELECTs). Blocks arbitrary read-SQL even for admins. Default false.
disable_webhooksboolHardening: disable outbound webhook actions (an SSRF surface). Default false.

theme { }

KeyDescription
presetNamed base theme: cartapel (default) or django.
accent / accent_btnShorthand accent overrides (win over the preset).
light / darkPer-mode maps of CSS token → value. Keys are cartapel token names without the -- prefix (page, surface, ink, accent, good, critical, …).
modeForce light, dark, or auto (default).
logo_light / logo_darkPer-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.

KeyDescription
type"postgres" for the database, or "http" for a read-only JSON source a custom page can call.
urlConnection URL (postgres) or endpoint (http). Supports env:NAME / ${NAME}.
schemasList of schemas to introspect (postgres). Defaults to ["public"].
primaryMarks the postgres source cartapel introspects. With a single postgres source it is implied; declare it explicitly when you define several.
token_env / headerFor http sources: attach a secret from this env var under header (default x-admin-token). The secret never reaches the browser.
rolesRestrict 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:

hcl
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.
  • format and color are 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

Released under the MIT License.