Skip to content

Architecture

Data flow

text
Dashboard / Discord / Integrations

          Flask API

        Domain Services

      Database / RPC Layer

       PostgreSQL / Supabase

       Data + Events + Audit

Layer 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_id in an HttpOnly, Secure, SameSite=Lax signed cookie. Each request resolves the liveagents row for that ID; only active and onboarding agents hold a session.
  • Bot/integration. The Authorization header must equal the environment's API_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_POLICY in authz.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_users table) 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_action locks 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 receives STATUS_CONFLICT → HTTP 409.
  • Queue pickup. rpc_claim_action and rpc_discipline_claim use FOR UPDATE SKIP LOCKED, so two bot ticks can never grab the same action.
  • Action completion. rpc_complete_action locks the row and guards status; a second completion returns ACTION_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 on training_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.