Appearance
Code Organization Rules
These are the project's architectural conventions. They exist because the system's security and audit properties depend on where logic lives: the authority model is only sound if mutations stay on their authoritative paths.
Keep app.py focused on composition and routing
app.py registers routes, parses requests, applies gates, and maps responses. Business rules belong in the service modules; data access belongs in db_access; gating belongs in authz / training_runtime; configuration belongs in environment.
Rule: a route handler that grows beyond request parsing + delegation is a sign the logic belongs in a service. New domain code should live in the owning module, not in app.py.
Place domain logic in services
Each domain has a service module that owns its data contract and rules:
- reports →
reports_service.py - punishments →
punishment_service.py - discipline delivery →
discipline_queue.py - action queue →
action_queue.py - agents →
agents_service.py - events →
event_service.py - training →
training_engine.py(pure evaluation),training_service.py(serialization/reads),training_runtime.py(orchestration) - admin/health/oauth →
admin_service.py,health_service.py,oauth_service.py
Rule: if two routes need the same operation, it is a shared service operation (like perform_report_action), not duplicated route code.
Avoid duplicated mutation logic
There is exactly one application path per business operation:
- report actions →
reports_service.perform_report_action - conclude →
reports_service.conclude_investigation - agent record changes →
rpc_admin_agent_update(both dashboard and bot paths delegate to it) - punishment mutations →
rpc_punishment_create/update/revoke
Rule: do not add a second path that produces different side effects for the same operation. When you need a variant, extend the shared operation.
Use authoritative RPCs for protected atomic operations
Multi-write, race-prone, or audited operations are implemented as named PostgreSQL functions and invoked by the API:
text
status/assignment + timeline + event + contact + queue insert
→ one rpc_report_action transaction, report row lockedRule: application code should not recreate authoritative multi-step database operations when an atomic database operation already exists. Do not introduce an RPC to solve a problem the existing architecture already solves.
Do not bypass authorization
- Every protected route calls
authorize_dashboard/require_admin/ training gates before touching data. - Row-level checks (
supervisor_report_denied,is_assigned_agent) run after the row is loaded. - Frontend visibility is advisory; the API is authoritative.
- The bot API key is rejected on training, punishment, and admin routes.
Rule: never add a route that skips a gate "because the frontend hides the button". If a route needs different access, change the policy deliberately and document it.
Preserve audit behavior
- Mutations whose integrity depends on a record write it in the same transaction (RPC path) or through
event_service(best-effort path) — never ad-hoc inserts. actor_idis the canonical identity from the session;actor_nameis a display snapshot. Never encode identity inside display text.- Destructive operations always carry a reason and a forensic record.
- Do not remove timeline/event dual-writing: both streams are contracts.
Rule: a change that alters what is recorded, by whom, or when must be reviewed as a security change (see Audit integrity).
Validate untrusted input
- Bound and shape-check before touching the database (see Input validation): body size, report IDs, enums, URLs, search strings, reasons.
- Re-validate the critical values inside the RPCs.
Rule: never interpolate user input into PostgREST filter expressions without sanitizing (see the admin report search) and never accept non-http(s) URLs into stored link fields.
Avoid direct database mutations when a protected operation exists
Before writing a table directly, ask: does an RPC already own this mutation? If yes, call it. Direct table writes are acceptable for:
- simple single-row reads (everywhere);
- whitelisted single-field updates that have no audit/race requirement (e.g. the report PATCH path — and even that writes timeline + event);
- training-table writes, where the API is the sole writer by design.
Rule: if you are about to write multiple tables in a route handler, stop — that is an RPC.
Database and RPC ownership
The DB/RPC layer has its own owner contracts (DB_OWNER_CONTRACTS.md in dps-code-api): do not redesign schema, functions, transaction boundaries, constraints, concurrency semantics, or authoritative mutation contracts without the DB/RPC owner. Key invariants:
- actor identity is a first-class parameter (
p_actor_id/p_requester_id); - queue/discipline completions are locked + guarded + evented;
- deletion is atomic with its audit record;
- the bot's agent updates flow through the same audited RPC as the dashboard.
Migration rules
- Additive/corrective migrations only — never modify an already-applied migration to change production behavior.
- Idempotent files (
IF NOT EXISTS/CREATE OR REPLACE/ guardedDO). - New functions: EXECUTE revoked from public/anon/authenticated, granted to service_role;
SECURITY INVOKERunlessDEFINERis justified; fixedsearch_path. - New tables: RLS enabled; narrow grants; append-only/delete-blocking triggers where retention matters.
- Apply migrations before deploying the code that calls them.