Appearance
Services
The API's domain logic lives in service modules under dps-code-api/. Each module owns a bounded set of rules and a data contract; route handlers in app.py remain thin adapters over them. The general conventions:
- Services never import
app; they depend ondb_access(data + serialization),authz(gates), andenvironment(config) only. - Every service documents what it owns and what it must not do directly.
- Authoritative multi-step mutations are delegated to the named RPCs — services do not recreate them with sequences of table writes.
- Canonical events are recorded through
event_service(application path) or by the transactional RPCs (same-transaction path).
reports_service.py — report domain operations
- Owns: the report data contract —
REPORT_ACTIONS(validate/invalidate/ investigate/contact_reporter/claim with their required/new statuses),PATCHABLE_FIELDS,BOT_ONLY_PATCHABLE_FIELDS, the status sets, and the server-side report list (rpc_list_reports). - Owns: the shared operations
perform_report_actionandconclude_investigation— the single application paths for report state transitions. - May modify: reports, timeline, pending_actions, contact_messages, events — but only through
rpc_report_action,rpc_create_report, and friends. - Must not: implement its own status transitions with bare table writes.
- Database interaction: pre-flight reads (
db_get_report_row), then the authoritative RPC; maps RPC error tags to HTTP responses.
punishment_service.py — punishment domain contract
- Owns:
PUNISHMENT_STATUSES,PUNISHMENT_ID_RE(AA0AA0),EDITABLE_PUNISHMENT_FIELDS,PROTECTED_PUNISHMENT_FIELDS,PUNISHMENT_REQUIRED_CREATE_FIELDS,expires_atnormalization, and the server-side punishment list. - May modify: punishments and discipline_queue — through
rpc_punishment_create/update/revokeand the discipline RPCs. - Must not: accept client-supplied punishment IDs, issuer identity, or timestamps (protected fields → 400).
- Database interaction:
db_get_punishment_rowreads + RPC mutations + batched display enrichment (issuer names, deliveries, profiles).
agents_service.py — agent data contract
- Owns:
AGENT_REQUIRED_CREATE_FIELDS,AGENT_PATCHABLE_FIELDS,VALID_RANKS. Pure constants, no logic.
event_service.py — canonical event interface
- Owns: the
EVENT_TYPEStaxonomy and the application-level writersrecord_event/record_report_event(thin wrappers overrpc_write_event). - Must not: be bypassed by ad-hoc event inserts from route code — route-level event recording goes through this module or through the transactional RPCs.
- Behavior: best-effort — a failed event write is logged and never fails the request that produced the activity. (The transactional RPC path is the strict one: events inside the mutation's transaction are mandatory.)
admin_service.py — admin helpers
- Owns:
_admin_identity()(session-derived (agent_id, agent_name, correlation_id) for audit rows) and health-history recording (_record_and_load_health_history, best-effort).
health_service.py — admin health formatting
- Owns: the transformation of raw
rpc_admin_healthmetrics into the status-site payload: 10 sections (overall, database, api, actions, queue, agents, evidence, auth, audit, events), each rated ok/warn/error, overall = worst section. Pure transformation — no request context, no database access.
oauth_service.py — Discord OAuth helpers
- Owns:
_discord_token_exchange(proxy-first, direct fallback),_validated_return_to(allowlisted origins only),discord_json_request,DiscordAPIError. - Must not: be called with secrets in logs; error bodies are logged, never the client secret.
contact_threads.py — contact thread data
- Owns:
db_get_contact_messagesandserialize_contact_message. Read-only helpers; mutations go throughrpc_contact_reply/rpc_contact_respond/rpc_close_contact_thread.
action_queue.py — report action queue
- Owns: stale-action requeue (
rpc_requeue_stale_actions, 10-minute timeout, 3-attempt cap), atomic claim (rpc_claim_action), queue listing,_to_unixfor Discord timestamps. - May modify: pending_actions — through the queue RPCs.
- Must not: perform the bot's Discord-side work; it only hands work to the bot and records outcomes.
discipline_queue.py — discipline delivery queue
- Owns: the delivery queue helpers: stale requeue (10 min / 3 attempts), atomic claim with the live punishment embedded, completion error mapping, and the delivery list. Mirrors
action_queue.py. - Must not: touch punishment rows directly; enqueues are transactional with the punishment RPCs.
training_engine.py — scenario evaluation
- Owns: pure condition evaluation:
condition_met(recursive action/ objective/evidence/any/all/trainer trees),evaluate_objectives,evaluate_evidence_unlocks,default_session_state,validate_action_type. - Must not: touch the database or Flask — it is a pure function of the scenario definition and state.
training_service.py — training serialization + reads
- Owns: training serializers (
_serialize_scenario,_serialize_note_row,_serialize_injection_row,_serialize_training_event), role/elapsed-time helpers, and request-scoped single-row reads. - May modify: nothing — reads only.
training_runtime.py — training session orchestration
- Owns: the session state machine:
_update_session_row, scenario instance activation,_record_training_event(persist + broadcast),_apply_engine(persist engine outcomes),_session_payload(the 4-parallel- read detail builder),_session_list_item, and the gates (_training_login_agent,_is_trainer,_trainer_control,_training_session_or_error). - May modify: training tables (sessions, session scenarios, events) — the API is the only writer.
- Must not: trust client-supplied roles, statuses, or actor identity; the actor is always derived from the session.
- Realtime:
_rt_broadcast/_broadcast_session/_broadcast_userare best-effort; broadcast failure never fails the persisted mutation.
authz.py — authentication/authorization
- Owns: rank/clearance constants,
verify_api_key,active_agent_from_session,has_permission, supervisor/reassign/Director checks,is_assigned_agent,authorize_dashboard,supervisor_report_denied,_is_admin_user,require_admin,_admin_capabilities. - Must not: be bypassed by routes; the DB RPCs independently re-check the critical guards.
db_access.py — low-level data access + serialization
- Owns: the request-scoped
supabaseproxy, table-name constants,db_get_*readers, Discord profile normalization/upsert/read, serializers (serialize_report/note/evidence/timeline/agent/punishment/profile), batch name maps, and identifier scrubbing (_build_identifier_replacements,_scrub_user_ids). - Must not: contain business rules (status machines, clearance) — that is the services' job.
environment.py — configuration
- Owns:
EnvironmentConfig(production/demo), per-request environment resolution, environment-bound session cookies, request-scoped Supabase clients, optional ingress HMAC verification. - Must not: leak secret values; configuration is never logged.
erlc_relay.py — ER:LC integration client
- Owns: relay selection, failover ordering, the per-relay circuit breaker, and
fetch_player(the ER:LC player lookup). Routes only callfetch_playerand never see relay URLs, tokens, or connection details.
How services interact with the database
Two patterns, used deliberately:
- Transactional RPCs for operations with multiple writes or race requirements (report actions, notes, contact, queue claims/completions, deletions, agent updates, punishments, deliveries, events). One call; the database owns the transaction.
- Direct table reads/writes for simple, single-row, low-risk operations (reads everywhere; training-table writes;
updated_atbumps). When a direct write would duplicate an existing RPC, use the RPC.
The rule that keeps them consistent: application code should not recreate authoritative multi-step database operations when an atomic database operation already exists.