Skip to main content
Restates the forms already decided in 0132 (permission keys), 0141 (event names), 0137 (machine codes) and 0135 (public ids), and builds on 0006 and 0128. The always-on rule is .agents/rules/naming-and-casing.md.
Amended by 0264: A module key is one snake_case segment (decision 1), and model keys are snake_case (decision 2). Dotted module keys are not used. Token constants for interface ports and defineConfig values take the type’s name (0263 decision 3); SCREAMING_SNAKE applies to other module-level constants.

Context

Casing was decided piece by piece: files in ADR 0006, code shapes in 0128, permission keys in 0132, event names in 0141, machine codes in 0137, public ids in 0135. On 2026-10-20 (night) the owner agreed one convention table, recorded in the decision file as “Naming convention FINAL”. This ADR is that table.

Decision

  1. Code style (TypeScript). Whole words; acronyms are cased as words (userId, ApiClient, loadHttpUrl). No I prefix on interfaces. Abbreviations are limited to an allow-list: id, url, api, db, i18n, ui. Booleans read is, has or can. Event handlers are on or handle. Hooks start with use. Module-level constants are SCREAMING_SNAKE.
  2. The table.
  1. Namespace lookups use ::. Content owned by a module is looked up with <module>::<path>: i18n keys, assets (ADR 0234), templates and stubs. Wire identifiers keep dots: permissions, events, action keys and DI token keys.
  2. Environment prefixes.
    • Public values (never secrets; a lint and a validator check enforce this): VITE_, EXPO_PUBLIC_, RENDERER_VITE_.
    • Server values: STACKRA_, FIGENTRA_, MAIN_VITE_.
    • Secrets are named <PROVIDER>_<PURPOSE>, for example STRIPE_SECRET_KEY.
    • Reserved, owned by tools: ENCORE_, TURBO_, NODE_, CI, GITHUB_, INFISICAL_, EAS_.
  3. DI tokens. A token is a constant made with defineToken, with a dotted key of the form <module>.<TypeName>: export const INVOICE_REPOSITORY = defineToken<InvoiceRepository>("billing.InvoiceRepository"). Code injects the constant. String injection is banned. A concrete @Injectable class is its own token.

Consequences

  • One table answers a naming question. The always-on rule carries the same table, so agents see it in every session.
  • The secret example in .agents/rules/secrets.md now uses STRIPE_SECRET_KEY, which matches decision 4.
  • Text elsewhere that uses another form is stale and is fixed when it is next touched.

Enforcement

planned — the oxlint unicorn/filename-case rule (kebab-case files), the house lint rules of 0129 for code style and DI tokens, the public-prefix lint for VITE_, EXPO_PUBLIC_ and RENDERER_VITE_, and the validator checks for machine codes, event names, permission keys and namespace lookups.

Alternatives

  • Acronyms in full caps (userID, loadHTTPURL): rejected; acronyms are cased as words.
  • Dots for every identifier, including asset and i18n keys: rejected; :: separates the module from its content.

Supersedes and sources

Supersedes: none. Restates the forms of 0132, 0141, 0137 and 0135. Sources: the owner’s “Naming convention FINAL” (stackra-decisions-2026-10-19.md, end of file, 2026-10-20); A10 (L627); C27 and the module-key confirmation (L855, L930); ADR 0006; ADR 0128; the code-conventions proposal (stackra-notes/code-conventions-proposal.md, section 2).