Skip to content

ENGINEERING CHARTER

github-actions[bot] edited this page Jul 6, 2026 · 3 revisions

doc: ENGINEERING-CHARTER repo: praycalc master-source: ../../.claude/docs/ENGINEERING-CHARTER.md read-order: § 22 (When in Doubt) → § 21 (Common AI-Agent Mistakes) → relevant section → .claude/memory/decisions.md master-sha256: 59e2e36ad8f8896d4f23ff09e8e23c7623c6e19c06d6866ec8f959332fa6d522 derivation: scripts/sync-charter.mjs edit-policy: read-only — edit the master, then run sync-charter.mjs

Wiki copy of the Ummeco Engineering Charter. Authoritative master lives at .claude/docs/ENGINEERING-CHARTER.md in the ummeco PPI. This page is regenerated by scripts/sync-charter.mjs — do not hand-edit.

Ummeco Engineering Charter

AI AGENTS: READ THIS FIRST. This is THE canonical project engineering charter. Every Wave 4 and Wave 5 standard is consolidated here. If you find a contradiction between this charter and any sprint file, planning doc, or per-repo PRI, THIS CHARTER WINS — surface the contradiction to the user as drift.

The PPI (~/Sites/ummeco/.claude/CLAUDE.md) covers project identity / domains / infra. This charter covers engineering execution standards.


1. Project Identity

Mission

Build technology that helps Muslims live their deen, connect with their ummah, and make Islamic knowledge accessible to the world. Free, authentic, community-first.

Products in Scope

  • praycalc/ — standalone prayer time calculator (public)
  • islamwiki/ — Islamic knowledge base (public)
  • chatislam/ — AI chat dawah + Q&A (public)
  • ummat/ — private monorepo: app/, pro/, dev/, chat/, backend/
  • flock/ — family wellness app (private; transferred from unyeco 2026-04-13)

Team Posture: Principal / Senior at All Times

Per D-P7-13 (Path α): every artifact — code, schema, API, doc, ticket, commit — is authored to the standard a Principal/Senior engineer would defend in design review. There is no "we'll fix it later" tier of work. If a pattern is documented in this charter, it is mandatory the first time the surface is touched.

AI-Agent Doctrine

This charter exists primarily to prevent AI agents from making repeated, costly mistakes. Each section ends with the most common AI-agent failure mode for that surface and the canonical fix. Section 21 is the consolidated mistake catalog.


2. Stack Invariants (NEVER violate)

# Invariant Decision
1 nSelf-only backend; no Supabase/Neon/PlanetScale/managed DB PPI hard rule
2 Hasura GraphQL only at runtime; Drizzle for migrations only PPI hard rule
3 pnpm only; never npm/yarn/bun install GCI hard rule
4 Cloudflare Turnstile is canonical CAPTCHA D-P3-20
5 Umami is canonical analytics D-P3-21
6 nChat is canonical chat backbone D-P3-23
7 No Firebase backend (FCM token registration only) D-P3-41
8 Migration target for AI = nself-ai (D-P3-44) D-P3-44
9 Tier enum: Basic / Standard / Premium / Enterprise (NEVER Starter/Growth/etc.) D-P7-12
10 Brand colors via @ummat/brand tokens; never hardcode hex Wave 4 design system
11 All multi-tenant tables carry org_id NOT NULL Wave 5 database
12 All money fields stored in *_cents BIGINT Wave 5 database
13 UUID v7 PK on every table Wave 5 database
14 RLS via Hasura JWT claims, never via subqueries D-P7-05, Wave 5
15 TypeScript strict + noUncheckedIndexedAccess Wave 4 types
16 zod validation at every API and webhook boundary Wave 5 api-design
17 Conventional Commits via commitlint Wave 4 cicd
18 WCAG 2.2 AA baseline D-P3-11
19 Local dev HTTPS port = 8543 (not 443) PPI
20 Flock prefix = fl_ per current PPI (note: D-P3-10 mentioned flo_; PPI rule is canonical) PPI § Database prefixes

AI-NOTE: If a PR proposes adding any tech that contradicts these invariants, reject the PR and surface to user. These are hard locks.


3. Architectural Decisions Reference

Canonical decision lock list. Every entry below is FROZEN — if you find code that contradicts a D-* decision, that is drift; surface to user immediately with the file path and decision ID.

Decision Subject Lock
D-P3-19 Flock prefix fl_ PPI
D-P3-20 Cloudflare Turnstile canonical CAPTCHA LOCKED
D-P3-21 Umami canonical analytics LOCKED
D-P3-23 nChat canonical chat backbone LOCKED
D-P3-41 No Firebase backend LOCKED
D-P3-44 nself-ai migration target for AI LOCKED
D-P5-14 (see decisions.md) LOCKED
D-P5-15 (see decisions.md) LOCKED
D-P6-01 (see decisions.md) LOCKED
D-P7-01 P7 scope = Option C honest scope LOCKED
D-P7-02 up_org is canonical tenant table LOCKED
D-P7-03 uuid up_sites + template/template_data columns LOCKED
D-P7-04 up_iqama_schedule daily-row model canonical LOCKED
D-P7-05 RBAC dual-mode: x-hasura-user-id (hub) + x-hasura-org-id (entity) LOCKED
D-P7-06 Signage devices V2 canonical; delete V1 folder LOCKED
D-P7-07 i18n localePrefix: 'as-needed' (V1) LOCKED
D-P7-08 Next.js (authed) route group LOCKED
D-P7-09 Template-specific tables: up_sites_{imam,charity,masjid} LOCKED
D-P7-10 Retire ummat.charity apex post charity.ummat.pro LOCKED
D-P7-11 Plural table names: up_donations (NOT up_donation) LOCKED
D-P7-12 Tier enum: Basic / Standard / Premium / Enterprise LOCKED
D-P7-13 Path α — full-quality, no quality debt deferred LOCKED

Full text of every decision: .claude/memory/decisions.md (search for the ID).


4. Naming Conventions Master Reference

Files / Folders

  • TypeScript: kebab-case.ts for utilities, PascalCase.tsx for components
  • Tests adjacent: Foo.tsxFoo.test.tsx (NOT __tests__/)
  • Stories adjacent: Foo.stories.tsx
  • Folders: kebab-case
  • Routes (Next.js App Router): kebab-case segments, [param] dynamic, (group) groupings
  • Server Actions: actions.ts colocated with route
  • Migrations: NNNNNN_<scope>_<verb>_<subject>.sql (zero-padded, monotonic)

Components

  • File and default export: PascalCase
  • Props interface: <ComponentName>Props
  • Compound: <Card.Header>, <Tabs.Trigger>

Hooks

  • use<Thing> — must be a function, must obey React rules of hooks
  • Server-only hooks: use<Thing>Server (RSC) — but prefer plain async functions for RSC

Types

  • Branded IDs: OrgId, UserId, DonationId (see § 5)
  • Generic params: T, U, K extends keyof T (single letter when shape is obvious; descriptive otherwise — TItem, TError)
  • Discriminated unions over string literals when state matters
  • interface for object shapes and props; type for unions, intersections, mapped

GraphQL Operations

  • Queries: Get<Subject> or List<Subject>s (e.g., GetMasjid, ListDonations)
  • Mutations: <Verb><Subject> (e.g., CreateDonation, UpdateOrgSettings)
  • Subscriptions: On<Subject><Event> (e.g., OnDonationCreated)
  • Fragments: <Subject>Fields or <Subject>SummaryFields
  • File names: <operation-name>.graphql (kebab) in src/graphql/

Database — Tables (plural per D-P7-11) and Columns

Prefix Domain
up_* Ummat Pro core (multi-tenant entity-scoped)
ua_* Ummat App (consumer)
uc_* Ummat Chat
pc_* PrayCalc
iw_* Islam.wiki
ci_* ChatIslam
fl_* Flock
jz_* Jobs / async work
nk_* nChat backbone tables
tr_* Translations / i18n
sa_* Super-admin / platform-level
lg_* Logs / append-only
sh_* Shared lookups (countries, currencies)
sc_* Scheduling primitives
cron_* Cron / scheduler state
obs_* Observability (metrics, traces)

Standard columns on every table:

  • id UUID PRIMARY KEY DEFAULT gen_uuid_v7()
  • created_at TIMESTAMPTZ NOT NULL DEFAULT now()
  • updated_at TIMESTAMPTZ NOT NULL DEFAULT now() (trigger-maintained)
  • deleted_at TIMESTAMPTZ (soft delete; nullable)
  • org_id UUID NOT NULL REFERENCES up_org(id) (multi-tenant tables only)
  • Money: amount_cents BIGINT NOT NULL, currency CHAR(3) NOT NULL

Tier Enum (D-P7-12 — CANONICAL)

export const TIER = ['Basic', 'Standard', 'Premium', 'Enterprise'] as const;
export type Tier = typeof TIER[number];
// PRICES (USD): Basic=$0 (Free trial), Standard=$299, Premium=$599, Enterprise=$2499

RETIRED names (NEVER use): Starter, Growth, Established, Pro, Free, Plus, Lite.

CSS / Tailwind

  • Use logical properties: ms-* / me-* (margin-inline-start/end), NOT ml-* / mr-* (RTL safety)
  • Brand tokens via @ummat/brand Tailwind config plugin — NEVER hex literals in class strings
  • Component-level: prefer className composition via cn() helper (clsx + tailwind-merge)

Env Vars

  • UPPER_SNAKE_CASE
  • Public (browser-exposed): NEXT_PUBLIC_* prefix only when truly safe
  • Per-app namespacing: UMMAT_PRO_*, PRAYCALC_*, CHATISLAM_*, etc.
  • Documented in each app's .env.example with one-line comment

Routes

  • Public: /, /about, /donate
  • Authed: under (authed) group (D-P7-08) — same URLs, different layout
  • API: /api/<verb>-<resource> (Next.js route handlers); prefer Server Actions or Hasura

5. Type Safety Bar

Compiler Flags (mandatory in every tsconfig.json)

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": true,
    "verbatimModuleSyntax": true,
    "moduleResolution": "bundler",
    "target": "ES2023"
  }
}

Branded IDs

export type OrgId = string & { readonly __brand: 'OrgId' };
export type UserId = string & { readonly __brand: 'UserId' };
export type DonationId = string & { readonly __brand: 'DonationId' };

export const orgId = (s: string): OrgId => s as OrgId;

AI-NOTE: never pass a raw string where an OrgId is expected. The compiler catches this only if you maintain the brand discipline.

Discriminated Unions for State

type SubscriptionState =
  | { status: 'pending'; createdAt: Date }
  | { status: 'active'; activatedAt: Date; renewsAt: Date }
  | { status: 'past_due'; lastFailureAt: Date; retryCount: number }
  | { status: 'comped'; grantedBy: UserId; reason: string }
  | { status: 'canceled'; canceledAt: Date };

Forbidden

  • any — banned. Use unknown and narrow.
  • as any — banned (rule: @typescript-eslint/no-explicit-any).
  • // @ts-ignore without // JUSTIFY: <reason; ticket #> on the line above — banned.
  • // @ts-expect-error allowed only when paired with // JUSTIFY: line.

Boundary Validation

Every API route, Server Action, webhook handler, message queue consumer, and worker job MUST validate input via zod at the entry point. No exceptions. Cast-to- type at the boundary is forbidden.

GraphQL Codegen

  • Mandatory: graphql-codegen runs in CI with --check flag
  • Generated files in src/graphql/__generated__/ — NEVER hand-edit
  • CI gate: pnpm graphql:check must pass on every PR

6. Documentation Bar

File Header (5-Field Block — Required on Every TS / Dart / SQL File)

/**
 * FILE: <path>
 * PURPOSE: <one sentence: what this file is responsible for>
 * INVARIANTS:
 *   - <hidden truth caller must know>
 *   - <state guarantee>
 * RELATED:
 *   - <other file paths>
 *   - <ADR or doc>
 * DO NOT:
 *   - <common mistake to avoid>
 *   - <pattern that broke production>
 */

TSDoc (Every Exported Symbol)

/**
 * Grants a complimentary subscription to an organization.
 *
 * @param input - validated by `grantCompSchema`; `orgId` must exist
 * @returns Subscription in `comped` state with audit row written
 * @throws {OrgNotFoundError} when `orgId` does not match a row
 * @throws {InvalidGrantorError} when `grantedBy` lacks `comp_grant` permission
 *
 * @example
 * await grantComp({ orgId, grantedBy: actor.id, reason: 'pilot launch', tier: 'Premium' });
 *
 * AI-NOTE: this is a privileged action. The caller MUST be inside an audit
 * transaction created by `withAudit(actor, 'comp_grant', () => ...)`.
 * Calling this directly without that wrapper will trip the audit-required
 * trigger and rollback.
 */

README per Package

Required sections: Purpose · Install · Usage · API · Testing · Maintainers · Related. Stub READMEs are a charter violation.

ADR per Architectural Decision

Path: .claude/docs/adr/NNNN-<slug>.md. Template: Context, Decision, Status, Consequences, Alternatives Considered, Related D-* IDs.

Migration Header (Every .sql Migration)

-- PURPOSE: <one line>
-- ROLLBACK: <down.sql path | "NOT REVERSIBLE because <reason>">
-- DEPENDS: <prior migration IDs>
-- CONSUMERS: <packages or services that read/write the changed tables>
-- DATA-TOUCH: <NONE | READ | WRITE | DESTRUCTIVE>
-- LOCK-HEAVY: <YES | NO> (if YES, requires off-peak window note)

Inline Comments — WHY ONLY

Code shows WHAT. Comments must explain WHY. See § 20 for the seven approved comment patterns and the forbidden list.


7. Module Boundary Rules

From To Allowed?
apps/* packages/@ummat/* YES
apps/* apps/* NO — extract to package
packages/* apps/* NO — packages cannot depend on apps
packages/* packages/* YES if no cycle

Leaf Packages (no internal dependencies)

  • @ummat/types — branded IDs, discriminated unions, shared type aliases
  • @ummat/schema — zod schemas (depends only on zod + @ummat/types)
  • @ummat/brand — design tokens (no app code, no React)

Enforcement

  • dependency-cruiser runs in CI; any cycle or forbidden edge fails the build
  • ESLint import/no-cycle at file level
  • Custom rule: no-deep-relative-imports../../../foo is banned across package boundaries; must go through the package's public entry

8. Component Patterns

Composition Principles

  • Slots over prop explosion. A Card accepts header, body, footer slots rather than 12 boolean props.
  • Compound for tightly-coupled. Tabs.Root / Tabs.List / Tabs.Trigger / Tabs.Content.
  • Polymorphism via asChild (Radix-style) when the same visual ships as different elements.
  • forwardRef on every leaf interactive component. Required for focus management, popovers, menus, tooltips. Non-negotiable.

7 UI States (Canonical per Dynamic Block)

Every block that fetches or accepts user input must implement all 7 — no exceptions:

  1. Empty — no data yet, with CTA where actionable
  2. Loading — skeleton matching final layout (no spinner-only)
  3. Partial — some data, some still loading
  4. Loaded — happy path
  5. Error — recoverable; with retry affordance
  6. Offline — explicit network-down state
  7. Forbidden — authorized user lacks permission; explain why

Accessibility (WCAG 2.2 AA — D-P3-11)

  • Every interactive element: keyboard reachable, visible focus, aria-label if no text
  • Every form field: associated <label>, aria-describedby for help/error
  • Color contrast: 4.5:1 text, 3:1 UI components
  • Target size 24×24 CSS px minimum (D-P3-11)
  • Focus not obscured (D-P3-11 — sticky headers must offset focus)
  • Drag alternatives where dragging is the only path (D-P3-11)
  • axe runs in every Playwright test; zero violations gate

RTL

  • Logical properties only (ms-* / me-* / ps-* / pe-*)
  • dir="rtl" flips at <html> based on locale
  • Icons that have direction (arrows, chevrons): mirror via [dir=rtl]:scale-x-[-1]

Brand Tokens

import { brand } from '@ummat/brand';
// CORRECT
className={`bg-[${brand.green.mid}]`}  // ❌ STILL WRONG — hex in class
className="bg-brand-green-mid"          // ✅ Tailwind plugin maps token

Hex literals in any source file under apps/*/src/** or packages/@ummat/ui/src/** are a charter violation. Lint: @ummat/no-hardcoded-brand-color.

Storybook

  • One story file per component
  • Required stories: Default, AllVariants, AllStates (the 7 above), AllSizes, RTL, Dark
  • Visual regression via Chromatic or Percy on every PR

9. Data Fetching + State

Default: React Server Components

  • Pages and layouts are RSC by default
  • Drop to Client ('use client') only for interactivity, browser APIs, or stateful UI

Server-State on Client

  • TanStack Query (@tanstack/react-query) — never fetch in useEffect
  • Cache key convention: [<resource>, <id?>, <params?>]
  • Suspense + ErrorBoundary at route boundary

Forms

  • React Hook Form + zodResolver
  • Server Actions for submission (Next.js); never bare fetch from form
  • Optimistic updates via useOptimistic where safe

URL State

  • nuqs for query-string-backed state (filters, pagination, tabs)
  • Never store in useState what should survive a refresh

Persistent Client State

  • Zustand for cross-tree state (UI prefs, locale override, theme)
  • NEVER store PII in client state — use Zustand persist middleware only for non-PII
  • Tokens in HttpOnly cookies — never localStorage

Realtime

  • Hasura subscriptions for live data (donations feed, signage status)
  • Bound subscription scope by org_id filter at the query level
  • Tear down on unmount; never leak

10. Backend Service Layer

Layered Architecture

[Hasura Action / Webhook / Cron] → [Use Case] → [Domain] → [Infra (Repository / HTTP / Queue)]
                                       ↑
                                  Pure functions
  • Use case: orchestration, transactions, audit, idempotency
  • Domain: pure business rules, no I/O
  • Infra: repository implementations, HTTP clients, queue producers

Repository Pattern per Aggregate

  • One repository class per aggregate root
  • Returns domain types, never DB rows
  • Lives in packages/@ummat/repos/

Errors: Sealed Hierarchy + Result Type

export class AppError extends Error {
  constructor(public code: string, message: string, public details?: unknown) {
    super(message);
  }
}
export class NotFoundError extends AppError { /* code: 'not_found' */ }
export class ForbiddenError extends AppError { /* code: 'forbidden' */ }
// ...

export type Result<T, E = AppError> = { ok: true; value: T } | { ok: false; error: E };

Async Work

  • BullMQ for queues; one queue per work type
  • Idempotency by job id (deterministic from input)
  • Retry with exponential backoff (3 attempts, 2x growth, jitter)
  • Dead-letter queue for terminal failures

Hasura Action Handlers

Thin adapters: parse input via zod → call use case → map Result to Hasura response. No business logic in handlers.


11. Database

Mandatory Per-Table Standards

  • id UUID PRIMARY KEY DEFAULT gen_uuid_v7() — v7 for time-sortable
  • created_at TIMESTAMPTZ NOT NULL DEFAULT now()
  • updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + trigger to maintain
  • deleted_at TIMESTAMPTZ (soft delete; partial index WHERE deleted_at IS NULL)
  • org_id UUID NOT NULL REFERENCES up_org(id) on every multi-tenant table

Money

  • Always *_cents BIGINT NOT NULL
  • Always paired currency CHAR(3) NOT NULL (ISO 4217)
  • Never NUMERIC for money — precision drift across services
  • Never JS number in code paths that touch money — use bigint or string

RLS

  • Hasura JWT claims drive RLS, NOT subqueries
  • Per D-P7-05: x-hasura-user-id for hub-style multi-entity listings; x-hasura-org-id for entity-scoped CRUD
  • Custom permission predicates only when JWT-claim model insufficient — document why

Indexes

  • Every FK gets an index (Postgres does NOT auto-index FKs)
  • Partial indexes for filtered queries: WHERE deleted_at IS NULL, WHERE status = 'active'
  • Composite indexes ordered by selectivity descending
  • CREATE INDEX CONCURRENTLY post-launch (avoid table locks)

JSONB

  • Allowed for flexible config (template_data, settings, feature flags)
  • Banned for queryable content — promote to columns or join tables
  • D-P7-09 governs template-specific data: dedicated tables, not JSONB

Triggers / Aggregates

  • Trigger-maintained aggregate caches preferred over materialized views for hot paths
  • Audit triggers on every privileged-action table

12. API Layer

Routing Decision Tree

  1. Hasura GraphQL first. CRUD, listing, joins, subscriptions.
  2. Server Actions second. Form submissions, mutations bound to a single page.
  3. Route handlers third. Webhooks, file uploads, streaming, server-sent events.

REST Conventions (when route handlers used)

  • Status codes: 200/201/204 success; 400/401/403/404/409/422 client; 500/502/503 server
  • Error shape (always):
{ "error": { "code": "stable_machine_code", "message": "human", "details": {}, "request_id": "..." } }
  • request_id propagated via X-Request-Id header

Webhooks

  • Verify signature BEFORE parsing body
  • Idempotency by event id (store in lg_webhook_events)
  • 200 fast (within 1s) — defer work to queue
  • Stripe webhook payloads: zod-validate the event variant — NEVER cast

13. Auth + Authorization

Auth

  • Hasura Auth canonical (no custom JWT issuers)
  • Access token: 15-minute lifetime
  • Refresh token: 30-day lifetime, rotating, HttpOnly cookie

MFA Required For

  • super_admin
  • entity_owner
  • Anyone executing comp grants
  • Anyone modifying billing config

Authorization (D-P7-05)

  • RBAC at app layer + RLS at DB layer (defense in depth)
  • Permission catalog in packages/@ummat/auth/permissions.ts
  • Audit row on every privileged action — non-bypassable trigger

14. Errors + Resilience

AppError Hierarchy

  • Stable string codes for machine handling
  • Human messages for UI (i18n via @ummat/i18n keys, NEVER hardcoded English)
  • details for structured context (validation errors, etc.)

Result Type

  • Use Result<T, E> for fallible operations
  • Throw only for truly unexpected (programmer error, system fault)

Outbound Calls

  • AbortController + per-call timeout (default 5s, configurable)
  • Exponential backoff retry (jittered)
  • Circuit breaker per upstream dependency
  • Dead-letter queue for persistent failures

15. Observability

Logging (pino)

Required fields on every log line: request_id, entity_id, user_id, route, status, latency_ms. Structured JSON only. Levels: trace/debug/info/warn/error/fatal.

Metrics

  • Per-event metrics catalog in packages/@ummat/observability/metrics.ts
  • Exposed at /metrics (Prometheus format) on every service
  • SLO dashboards in Grafana

Tracing

  • OpenTelemetry SDK in every service
  • Trace ID propagation via W3C traceparent header
  • Sampled traces: 100% errors, 1% success

Errors → Sentry

  • All level >= error to Sentry
  • request_id and trace_id attached to every Sentry event

Status

  • Public status page: status.ummat.dev (Upptime per Phase 1 D)

16. Performance

Per-Route-Shape Budgets

Shape LCP FID CLS TBT JS CSS Image (hero)
Marketing 1.8s 100ms 0.1 200ms 100KB 30KB 100KB
App page 2.5s 100ms 0.1 300ms 200KB 50KB 150KB
Dashboard 3.0s 100ms 0.1 500ms 300KB 60KB n/a

Gates

  • Lighthouse CI on every PR (mobile + desktop)
  • Bundle-size diff comment on PR (@next/bundle-analyzer)
  • ISR + on-demand revalidation for marketing/content
  • KV cache (Cloudflare KV) for slug→entity_id lookups

17. Testing

Layer Tool Coverage Bar
Unit Vitest 80% global / 90% critical-path
Integration Vitest + testcontainers every use case
E2E Playwright 84-permission matrix per template
Visual regression Chromatic or Percy every PR
Mutation Stryker money paths required
Load k6 per release; SLO-bound
A11y axe in Playwright zero violations

Zero-Flake Gate

A flaky test (passes / fails on identical inputs) must be quarantined within 24h and fixed within one sprint. Three flake quarantines in a sprint = team retro.


18. CI/CD

Pre-commit (lefthook)

  • format (prettier) → lint (eslint) → typecheck (tsc) → test (changed) → secret scan (gitleaks)

Conventional Commits (commitlint)

<type>(<scope>): <subject> where type ∈ feat|fix|refactor|chore|docs|test|build|ci|perf|style|revert.

Branch Naming

<type>/<ticket-id>-<slug> — e.g., feat/T-P7-A1-15-add-up-donations-view.

PR Template

Required sections: What · Why · How · Tested · Screenshots (UI changes) · Checklist · Related ADR / D-* IDs.

Branch Protection (main)

  • ≥1 approval required
  • All checks pass (lint, typecheck, test, build, lighthouse, dep-cruiser, codegen-check)
  • No force push
  • Linear history

Releases

Gated per GCI Version & Release Lock. NO version bumps, NO tags, NO publish without a STORM-approved release plan in .claude/planning/release-{version}.md.


19. Security

Zero Secrets in Code

  • gitleaks pre-commit + CI gate
  • All secrets via ~/.claude/vault.env or per-environment .env (never committed)

Rotation Policy

Cred Rotation
OAuth secrets 90 days
Database passwords 180 days
API keys (Stripe, Anthropic) 365 days or on staff change
JWT signing key 365 days with overlap window
Webhook signing secrets on every consumer change

Dependency Hygiene

  • pnpm audit --audit-level=high gate on PR
  • Dependabot enabled with security-only auto-PRs
  • License-checker: MIT / Apache-2.0 / BSD / ISC only (GPL flagged to user)
  • SBOM via cyclonedx-npm published per release
  • Pinned (no caret) for high-risk: stripe, next, jsonwebtoken, @hasura/*

20. AI-Agent Code Patterns (the WHY-comments doctrine)

When to Write a Comment — Only These Seven Cases

  1. Invariant// INVARIANT: org_id matches actor's claimed org_id post-RLS check
  2. Contract// CONTRACT: callers must hold the audit transaction (see withAudit)
  3. Decision tree// WHY this branch: cents-only path; alt was Decimal but breaks FFI to Stripe webhook payload
  4. Workaround// WORKAROUND: Hasura issue #9012 — Remote Schema doesn't pass headers; revisit at Hasura ≥ 2.40
  5. Reference// SEE: ADR-0014 | docs/billing-state-machine.md | issue #312
  6. Surprising-behavior// CAUTION: this looks like a normal upsert but emits a domain event with side effects
  7. AI-instruction// AI: when modifying this, first read docs/RLS.md; common mistake is widening the predicate

NEVER Write These Comments

  • "// adds two numbers" (code shows that)
  • "// returns the user" (code shows that)
  • Multi-paragraph philosophy
  • TODO without ticket reference: // TODO: T-P7-XX-NN — extract X helper
  • Personal-name attributions ("// Aric: ...")
  • AI provider attribution ("// generated by ...") — banned by GCI

File Header (Canonical)

See § 6.

Function-Level TSDoc (Canonical)

See § 6.

Component Prop Docs (Canonical)

interface ButtonProps {
  /** Visual variant. `primary` triggers brand color from @ummat/brand. */
  variant: 'primary' | 'secondary' | 'destructive' | 'ghost';
  /** Whether to render as another element via Radix-style asChild. */
  asChild?: boolean;
  /** REQUIRED if no children — provides accessible name. */
  'aria-label'?: string;
}

State Machine Documentation

/**
 * Subscription state machine:
 *
 *           ┌─────────┐  grant   ┌────────┐
 *  trial ─►│ pending │ ───────► │ active │
 *           └─────────┘          └────────┘
 *                ▲                   │
 *                │ retry             │ comp_grant
 *                │                   ▼
 *           ┌─────────┐         ┌────────┐
 *           │  past   │ ◄────── │ comped │
 *           │  due    │ revoke  └────────┘
 *           └─────────┘
 *                │
 *                │ cancel
 *                ▼
 *           ┌─────────┐
 *           │canceled │
 *           └─────────┘
 *
 * Transitions enforced by trigger up_subscription_state_check (migration 00NN).
 */

21. Common AI-Agent Mistakes (read before any change)

For each: PROBLEM → SOLUTION → DETECTION.

# Problem Solution Detection
1 Hardcoding brand color hex (e.g., #79C24C) Use @ummat/brand token via Tailwind plugin CI lint @ummat/no-hardcoded-brand-color
2 Wrong tier name (Starter / Growth / Free / Plus) Use D-P7-12 enum: Basic / Standard / Premium / Enterprise CI lint @ummat/canonical-tier-name
3 Direct Postgres client from app code Always go through Hasura GraphQL ESLint no-restricted-imports for pg/postgres in apps/*
4 Stripe webhook payload cast to type zod-validate the event variant first Custom rule: @ummat/require-zod-at-route-boundary
5 Hardcoded English in JSX next-intl <Trans> / t('key') eslint-plugin-i18next/no-literal-string
6 as any cast Fix the underlying type @typescript-eslint/no-explicit-any (error level)
7 Modifying generated GraphQL files Run codegen and edit source schema CI gate pnpm graphql:check
8 Editing .claude/ via Write/Edit tool Use claude-write / claude-edit scripts PreToolUse hook auto-approve-claude-dirs.sh
9 New table without org_id (multi-tenant) Add org_id UUID NOT NULL REFERENCES up_org(id) Migration lint custom rule
10 New API route without zod parse Add zod parse at top of handler Rule: @ummat/require-zod-at-route-boundary
11 No file header Add canonical 5-field block (§ 6) Rule: @ummat/file-header-required
12 Bare TODO without ticket // TODO: T-P7-XX-NN — explanation no-restricted-syntax for bare TODO
13 Cycle in module imports Restructure or extract leaf package import/no-cycle
14 Brand color drift (the praycalc-style mistake) Charter § 8 + lint catches @ummat/no-hardcoded-brand-color
15 Migration without ROLLBACK section Write down.sql or document irreversibility @ummat/migration-header-required
16 Reaching across apps via deep relative path Extract to @ummat/<x> package @ummat/no-deep-relative-imports
17 Skipping CR-B because "small change" CR-B is GATE per Weight (Wave 4 cr-rubrics) Process discipline (PR template + reviewer assignment bot)
18 Adding deps without audit CI pnpm audit --audit-level=high Automated CI gate
19 New feature without ADR Author ADR before opening PR Required label adr-attached on feat: PRs
20 Commit without conventional format commitlint rejects Pre-commit hook + CI
21 Reintroducing the /v1/auth/* prefix on hasura-auth calls hasura-auth 0.36 at auth.ummat.dev serves bare paths only (/signin/email-password, /signup/email-password, /token, /signout, /signin/passwordless/email); the /v1/auth/* prefix has no nginx upstream and 502s in prod Live probe: curl -I https://auth.ummat.dev/v1/auth/signin/email-password must never be treated as a passing check; grep for v1/auth in client auth code
22 Pointing billing calls at api.praycalc.com api.praycalc.com is the Hasura CORS host only — it 404s on /billing. Billing lives at https://smart.praycalc.com/billing; web goes through /api/billing/* proxy routes, desktop calls smart.praycalc.com directly (Tauri CSP must allow it) grep for api.praycalc.com near billing/checkout/portal code
23 Trying to "unset" a Vercel header in a later headers() rule Vercel headers cannot be unset once set by an earlier matching rule — only overridden. To make a route iframe-able again, override with a permissive/invalid value (e.g. X-Frame-Options: ALLOWALL so browsers ignore it, with CSP frame-ancestors governing instead), never rely on a later rule removing it Manual header check: curl -I <route> and confirm no conflicting X-Frame-Options: DENY/SAMEORIGIN survives from an earlier rule

AI-NOTE: This catalog is the cheatsheet. If a code change does not appear on the right side of any of these mistakes, it is acceptable. If it appears on the left side, the lint will catch it — do not waste a CR cycle on it.


22. When in Doubt

Read in this order:

  1. This charter (you are here) — section that matches your surface
  2. .claude/memory/decisions.md — search for the related D-* ID
  3. .claude/planning/p7-wave4-*.md — Wave 4 standard for the surface
  4. .claude/planning/p7-wave5-*.md — Wave 5 deep dive (db / api)
  5. The relevant per-repo PRI (<repo>/.claude/CLAUDE.md)
  6. If still unclear: surface to user. Do NOT guess. Drift from a guess compounds across the codebase faster than asking for 30 seconds of clarification.

Charter Maintenance

  • Charter is updated only via STORM-approved planning docs
  • Each charter update bumps the authored: date in the front-matter
  • Charter sections are referenced by § N in PRs, ADRs, and tickets
  • Drift between charter and code is a TICKET, not a fix-in-passing — file it

End of Engineering Charter. Read § 21 before every code change. Read § 22 when stuck.

Clone this wiki locally