Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions docs/adr/0007-deploy-security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# ADR 0007 — Deployment and security posture

## Status
Accepted

## Context
Evincta needs to be publicly accessible (a live demo a reviewer can use)
while (a) not costing money per visitor, (b) not exposing secrets, and
(c) not being abusable. The API is stateful: it holds a long-lived MCP
host (four subprocesses) and a SQLite checkpointer on the filesystem for
the human-in-the-loop pause.

## Decision

**Hosting split.** Frontend (static React) → Vercel. API (stateful
container) → Render free tier as a Docker web service. We do NOT use
serverless functions (e.g. Lambda) for the API: it holds a long-lived
MCP host and a filesystem checkpointer, which serverless — with its
ephemeral filesystem and no persistent process between requests — cannot
support without re-spawning the MCP servers per call (the slow pattern
Phase 6a removed) and losing paused-claim state.

**Secrets.** All secrets (model/provider keys, the API key, allowed
origins) are injected at runtime as environment variables by the host
(Render / Vercel). They are never copied into the Docker image;
`.dockerignore` excludes `.env`, and `.env` is gitignored. No secret is
ever committed to the repository.

**Auth.** Reads (list/detail) are open so the demo queue loads without
friction. Writes (`POST /claims`, which spends model tokens, and
`POST /claims/{id}/decision`, which mutates the audit) require an API key
via an `X-API-Key` header, checked by a FastAPI dependency. The frontend
supplies the key for the demo; a one-click "demo adjuster" login gates
the UI and flows the adjuster identity through as the audit `approver`.

**CORS.** Locked via an `ALLOWED_ORIGINS` env var to the exact Vercel
origin (methods restricted to GET/POST; headers to Content-Type and
X-API-Key). No wildcard in production.

**Cost safety.** Demo claims are pre-seeded with recommendations computed
once and cached; reads never invoke a model, so browsing/deciding costs
nothing. Only the key-gated `POST /claims` spends. A key-gated reset
returns the demo queue to a pending state for the next visitor.

## Consequences
+ Live, usable demo at zero per-visitor cost; secrets never exposed.
+ The container matches the app's stateful nature; the env-var seam lets
the same code run locally and in production by changing configuration.
+ Write endpoints are protected against casual abuse and budget drain.
- Render free tier sleeps after ~15 min idle (~30–60s cold start) —
acceptable for a portfolio; documented in the README.
- API-key auth embedded in the frontend protects against casual abuse,
not a determined attacker (the key ships in the built JS). Described
honestly as such; real SSO would slot in at the same dependency seam.
- Single-instance state (SQLite + JSON index) — horizontal scale is a
documented roadmap item (see ADR 0008).

## Alternatives rejected
- Serverless API (Lambda): incompatible with the long-lived MCP host and
filesystem checkpointer.
- No auth: leaves paid endpoints open to abuse and budget drain.
- Wildcard CORS: any origin could drive the API from a browser.
- Live per-view model calls: every visitor would cost money and risk
hitting rate/usage limits mid-demo.
Binary file added docs/media/console.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/trace.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading