Skip to content

Adding an API Feature

The expected path for a new feature that starts in the dashboard and ends in the database:

text
Frontend
   ↓  typed wrapper + page state
API Route
   ↓  parse, validate, gate
Service
   ↓  domain rules, shared operation
Database Access / RPC
   ↓  authoritative mutation, one transaction
Audit / Events
   ↓  canonical record in the same transaction
Response

Work through the layers in that order, but decide the data contract first: what the mutation must do atomically, what identity/authorization it needs, and what its audit record is. That decision determines where the logic lives.

Layer by layer

Frontend

  • Add a typed wrapper in src/lib/api/<domain>.ts (method, path, params, response type in types.ts).
  • Call it from the page; on success update from the response and/or re-fetch; on ApiError surface the message and re-fetch on conflicts.
  • Sanitize any URL values before rendering; gate controls by the existing permission flags.

API route (app.py)

Keep the handler a thin adapter:

  1. Parse and validate input (required fields, enums, URLs, bounds — see Input validation).
  2. Authenticate (verify_api_key or active_agent_from_session) and authorize (authorize_dashboard, require_admin, or a training gate).
  3. Perform row-level checks (supervisor_report_denied, is_assigned_agent).
  4. Delegate to the service operation.
  5. Map RPC error tags to HTTP responses; return the serialized payload.

Do not put business rules in the route. If the operation is multi-step or race-prone, do not implement it with several table writes in the route — see the RPC step.

Service

  • Put the domain operation in the owning service module (reports_service, punishment_service, …) or a new service if none owns it.
  • The service owns the pre-flight rules and the call to the authoritative mutation; routes stay thin.
  • Reuse existing shared operations instead of duplicating their logic — e.g. do not add a second path for a report action when perform_report_action exists.

Database access / RPC

  • For a simple single-row read/write that is not race-prone and needs no audit, direct table access through db_access helpers is fine.
  • For a multi-write, race-prone, or audited operation, add a named RPC in a new migration:
    • one transaction for all writes;
    • SELECT … FOR UPDATE (or SKIP LOCKED) on the contended row;
    • in-function re-validation of whitelists/statuses;
    • revoke from public, anon, authenticated + grant execute to service_role (migration 016 convention);
    • machine-readable error tags, not free-form text.
  • Do not introduce an RPC when the existing API/database architecture already solves the problem (project rule). Prefer reusing an existing authoritative mutation.

Audit / events

  • If the mutation's integrity requires a record, write it inside the same transaction (via rpc_write_event or admin_write_audit from the RPC).
  • Application-level telemetry that is not integrity-critical may use event_service.record_event (best-effort by design).
  • Record canonical actor_id (Discord ID from the session) and a display actor_name; never encode identity inside display text (migration 034 invariant).
  • Add the new event type to event_service.EVENT_TYPES and the Event types reference.

Decision guide

QuestionAnswer → layer
Does the operation write multiple rows that must commit together?RPC
Can two requests race on the same row?RPC with a row lock
Must the mutation produce an audit/event record?RPC (same transaction) or event_service (best-effort)
Is it a simple read or single-row update with no race?db_access helpers in the route/service
Does a protected operation already exist for this?Use it — do not duplicate

Verification

  • API contract suite: add/extend *_tests.py for the route (request parsing, gates, RPC error mapping, response shape) — run with the repo's Python environment.
  • SQL: apply the migration to a scratch project; exercise happy path, error tags, and privileges.
  • Frontend: npm run build (typecheck), manual demo-environment pass.
  • Update the API reference, RPC reference, schema, and the Changelog with the change.