Appearance
Architecture
Data flow
text
Dashboard / Discord / Integrations
↓
Flask API
↓
Domain Services
↓
Database / RPC Layer
↓
PostgreSQL / Supabase
↓
Data + Events + AuditLayer responsibilities
Dashboard (React/Vite) — ecc-dps-dashboard/ The browser application. Renders state fetched from the API, collects input, and issues credentialed requests. It holds no business authority: every mutation it performs goes through the API, and the API re-checks identity, clearance, rank, and assignment on every route. The Training Center is the same bundle booted under the training.eccdps.org hostname.
Flask API — dps-code-api/app.py The composition and routing layer. Registers routes, resolves the request's environment, parses and validates input, applies authentication and authorization gates, delegates to services, and maps results/errors to HTTP responses. app.py deliberately contains no business logic that has a home in a service module; the route handlers are thin adapters over shared operations.
Domain Services — dps-code-api/*_service.py, authz.py, db_access.py Where domain rules live: report action state machines (reports_service), punishment contracts (punishment_service), the training engine (training_engine), training session runtime (training_runtime), the canonical event service (event_service), queue helpers (action_queue, discipline_queue), agent contracts (agents_service), and administrative helpers (admin_service, health_service, oauth_service, contact_threads). Services never touch the Flask app; they depend on db_access (the low-level data-access and serialization boundary) and authz (the gating boundary).
Database / RPC Layer — dps-code-api/migrations/*.sql Named PostgreSQL functions own the authoritative, atomic mutations. A single RPC call performs the row lock, status/state guard, all writes (state, timeline, event, queue insert), and the audit record in one transaction. RPCs validate their own inputs, so the database remains authoritative even if a route bug skips a check. Least-privilege grants (service_role only) and row-level security restrict who may execute them.
PostgreSQL / Supabase The single source of truth. All application state, the append-only events and admin_audit_log streams, and the training tables live here. Supabase Realtime is used only as a best-effort synchronization signal for training sessions.
Request lifecycle
Every meaningful request follows the same path:
text
Request
→ Authentication (browser session cookie or API key)
→ Authorization (clearance, rank, allowlist, assignment)
→ Validation (fields, URLs, bounds)
→ Service (domain rules, shared operations)
→ Authoritative DB Operation (named RPC or gated table write)
→ Audit/Event (timeline + canonical event in the same transaction)
→ Response (serialized, identifier-scrubbed payload)Authentication
- Browser sessions. Discord OAuth stores only the agent's
discord_idin an HttpOnly, Secure, SameSite=Lax signed cookie. Each request resolves the liveagentsrow for that ID; onlyactiveandonboardingagents hold a session. - Bot/integration. The
Authorizationheader must equal the environment'sAPI_KEY(constant-time comparison). The bot key bypasses clearance only where a route explicitly allows it (report creation, queue work); training and punishment routes reject the key outright.
Authorization
- Clearance level (1–5) gates dashboard capabilities via
CLEARANCE_POLICYinauthz.py. - Rank gates supervisor access (Senior Agent+), reassignment (Head Investigator+), and Director-only admin capabilities (Director / Department Director).
- Row-level gates re-check assignment after a report is loaded: only the assigned agent (or supervisor rank) may update, note, evidence, action, or contact a case.
- The admin allowlist (
admin_userstable) gates the whole System Administration area on top of clearance; admin mutations are audited with reasons and before/after state.
Concurrency model
The system is designed around single-writer races resolved in the database:
- Report actions.
rpc_report_actionlocks the report row (SELECT ... FOR UPDATE) and re-checks the allowed status set before mutating. Two agents racing to validate the same case: the first wins, the second receivesSTATUS_CONFLICT→ HTTP 409. - Queue pickup.
rpc_claim_actionandrpc_discipline_claimuseFOR UPDATE SKIP LOCKED, so two bot ticks can never grab the same action. - Action completion.
rpc_complete_actionlocks the row and guards status; a second completion returnsACTION_ALREADY_COMPLETED→ 409. - Training. The API is the only writer of training tables; the frontend never mutates training state directly. Duplicate client actions are idempotent via the
(session_id, client_id)unique index ontraining_events.
Realtime (training only)
Training state is persisted by the API first; a best-effort Supabase Realtime broadcast then notifies connected browsers. Realtime is never the source of truth: clients reconcile by re-fetching authoritative state after connecting and after events. Broadcast failure is logged and does not fail the request that produced the state change.
Isolation guarantees
- Environment isolation. One Flask codebase serves production (
api.eccdps.org) and demo (demo-api.eccdps.org). The environment is resolved from the trusted ingress hostname per request; sessions carry an environment claim and are signed with environment-specific secrets. Demo configuration never falls back to production credentials. - Role isolation. Browser sessions and the bot key are distinct identities. Training and punishment routes reject the bot key; admin routes require a browser session and the allowlist.
- Frontend is not a boundary. UI gating mirrors server rules for usability, but every protected route re-checks authorization server-side.