add-malli-schemas
Efficiently add Malli schemas to API endpoints in the Metabase codebase with proper patterns,…
Where Metabase backend code goes and how it reaches the app DB. Covers module layout, the `<module>.db` rule, `[:auto/param]` value binding, module config, and pre-handoff checks. Use when adding or moving backend namespaces, writing app-DB queries, or touching module
$ npx -y skills add metabase/metabase --skill backend-module-conventions --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/backend-module-conventionsContext preview
The summary Claude sees to decide when to auto-load this skill.
Where Metabase backend code goes and how it reaches the app DB. Covers module layout, the `<module>.db` rule, `[:auto/param]` value binding, module config, and pre-handoff checks. Use when adding or moving backend namespaces, writing app-DB queries, or touching module
name: backend-module-conventions description: Where Metabase backend code goes and how it reaches the app DB. Covers module layout, the `<module>.db` rule, `[:auto/param]` value binding, module config, and pre-handoff checks. Use when adding or moving backend namespaces, writing app-DB queries, or touching module boundaries. Preloaded by every `*-backend-expert` agent.
This skill decides where a line of backend code lives and how it reaches the app DB. Project `CLAUDE.md` covers the module config keys, nested modules, test commands, and ratchets. When the two disagree, the source tree wins. Read the linter or test that enforces a rule before you trust any prose about it, including this file.
A module is `metabase.<module>` (OSS, `src/`) or `metabase-enterprise.<module>` (EE, `enterprise/backend/src/`). Most modules split into these namespaces:
| Namespace | Holds | |---|---| | `<module>.core` | The public API. Other modules call only this (plus anything else listed in the module's `:api`). Often a `potemkin/import-vars` facade. | | `<module>.db` | Every app-DB query the module makes. See the next section. | | `<module>.models.*` | Toucan 2 model definitions: `methodical/defmethod t2/table-name`, hooks, transforms. | | `<module>.settings` | `defsetting`s. | | `<module>.init` | Requires the namespaces that register things as a side effect (settings, tasks, event handlers). `metabase.core.init` or `metabase-enterprise.core.init` requires it. | | `<module>.api`, or the nested `<module>.rest` module (`:ns-prefix "metabase.<module>-rest"`, dir `<module>_rest/`) | HTTP endpoints. Domain logic stays in the base module. The `.rest` child calls the parent's `.core`. | | `enterprise/<module>` | The EE companion of an OSS module. It nests under the OSS module, which exports it automatically. |
If a setting, task, or event handler never takes effect, look for a missing link in the `.init` chain.
Call Toucan 2 query functions (`t2/select*`, `t2/insert!`, `t2/update!`, `t2/delete!`, `t2/count`, `t2/exists?`, `t2/query`, ...) and the `metabase.app-db.core` wrappers (`mdb/query`, `mdb/update-or-insert!`, ...) only from `metabase[-enterprise].<module>.db`. Driver code uses `metabase.driver.<driver>.db`. Every other namespace calls functions from its own module's `db`. The kondo hook `hooks.metabase.toucan.db-ns` reports violations as `:metabase/t2-query-namespace`. Test files are exempt.
Write `db` functions in the style of `src/metabase/settings/db.clj`:
`metabase.app-db.value-guard` binds a value written as `[:auto/param v]` as a SQL parameter. The `:metabase/unsafe-app-db-query` lint is off globally. `.clj-kondo/config.edn` turns it on per namespace under `:config-in-ns`, one converted `db.clj` at a time. Follow these rules in every `db.clj`, whether or not the lint covers it yet:
1. Mark a value that comes from a variable when it sits in a **value slot**. Value slots are the right-hand side of `:=`, `:in`, `:like`, ..., and kv-arg values used as filters. 2. Never mark a literal. A literal cannot carry a request value. 3. Never mark a value in an **identifier slot** (`:select`, `:from`, joins, aliases, and similar). `value-guard` throws `::marker-outside-value-slot` at compile time. In `:order-by` and `:group-by` a marker compiles to `ORDER BY ?`, which is useless but does not throw. Don't write one there either. 4. Leave a possibly-empty collection unmarked. Toucan rewrites `[:in col []]` to `false` before the marker step. 5. Leave values that `t2/update!` *writes* unmarked. Model hooks such as encryption must still see them. 6. Keep kv-arg style (`:engine engine`) for a column with a Toucan type transform. If you move a keyword value into a raw `{:where ...}` map, the transform is skipped. HoneySQL then formats the keyword as an identifier, and the query returns wrong rows without failing.
The docstring of `metabase.app-db.value-guard` explains what the guard refuses without any marker.
`./bin/lint-migrations-file.sh` enforces these rules. Run it after every change.
Metabase is the easy, open-source way for everyone in your company to ask questions and learn from data.
Repo: metabase/metabase
Efficiently add Malli schemas to API endpoints in the Metabase codebase with proper patterns,…
Add OpenTelemetry tracing spans to Clojure code following Metabase tracing conventions. Use…
Add product analytics events to track user interactions in the Metabase frontend
Draft or audit the REST API changelog (docs/developers-guide/api-changelog.md) by…
Evaluate Clojure code via nREPL using clj-nrepl-eval. Use this when you need to test code,…
Review Clojure and ClojureScript code changes for compliance with Metabase coding standards,…