BACKLOG P1-1: siax.repo.json/siax.config.json rewritten to be schema-valid against sax3l/siax-standard's actual v2.0 schemas (lifecycle, operationalState, gitHost, domains-as-objects, consumedBy, children, contracts -- validated with jsonschema, not just eyeballed). config.json restructured to the real deploy-descriptor shape (framework/build/start/ port/healthcheck/environments) instead of duplicating repo.json's identity fields, which the schema doesn't allow. BACKLOG P1-2: 3 ADRs for decisions already made and evidenced elsewhere in this repo (Coolify-not-Vercel, Zitadel-not-NextAuth, Iceberg-format pointer to n0d ADR-0030) -- retroactive documentation, not new decisions. The Stripe/Revolut and services/scrapers/* questions are explicitly NOT written as ADRs here; both are account-owner-gated or actively under review elsewhere (see docs/architecture/decisions/README.md). BACKLOG P1-3: CHANGELOG.md from real git log history (no tags/releases exist yet -- this doesn't invent version numbers that were never cut). BACKLOG P1-19: `pnpm siax:doctor` (scripts/siax-doctor.mjs, AGENT-04) -- zero-dependency, checks required files + manifest shape, prints control-IDs. .devcontainer/devcontainer.json (DX-03). compose.dev.yaml (DX-02 -- UNVERIFIED, no Docker available in this environment to smoke-test, flagged in the file itself). All 4 prompts + the index rewritten to AGENT-03 shape (goal/context/prerequisites/scope/contract/acceptance-criteria/ control-ID/forbidden-actions). BACKLOG P1-20: docs/architecture/ARCHITECTURE.md migrated to the v2.0 9-heading template -- new §5 "Ytor och paritet" (capability x API/CLI/MCP/console coverage table, grounded in the same registry facts docs/02-architecture.md already established), §6-9 renumbered from the old §5-8 (all internal cross-references fixed, CLAUDE.md's own §-numbers left untouched). docs/api/ added (points at contracts/openapi.yaml as the one SSOT instead of duplicating it as drift-prone prose). BACKLOG P1-21: packages/schemas now has a real package.json (was loose JSON files with no package -- pnpm-workspace.yaml's packages/* glob matched an empty package). Verified `pnpm install --frozen-lockfile` and the full typecheck/test/build suite still pass with 2 real workspace projects. apps/ restructuring explicitly NOT attempted (BACKLOG's own effort estimate is "L" / large -- moving the whole app tree is a bigger, riskier change than belongs in this pass). Import-boundary check added via dependency-cruiser (ARK-14) -- installed for real, config tested against this codebase (179 modules, 310 deps, 0 violations), wired as `pnpm run deps:check` (advisory, not yet a CI gate). BACKLOG P2-5: docs/02-architecture.md's "Hosting" section marked SUPERSEDED (the Vercel plan was superseded by the Coolify deploy.target flip, 2026-08-09 -- ADR-0001). README.md/.env.example were checked and are already accurately framed (live-on-Vercel vs. config-target-is-Coolify) -- not touched. BACKLOG P3-5: 2 stale github.com/sax3l/0pt/pull/3 links removed (the PR merged long ago, .env.example is real and current) -- docs/09-build-plan.md, docs/08-infrastructure.md. The "two diverged remotes, unresolved which is canonical" language elsewhere (CLAUDE.md, AGENTS.md, SECURITY.md, etc.) is deliberately NOT touched -- that's a live, correctly-flagged, account-owner- owned open question per the Gitea-exclusivity mandate, not a stale link. BACKLOG P6 (19 items): concrete artifacts for the ones that are actually a file (.editorconfig, dependency-cruiser, deprecation policy, K8s reference manifest, .actrc, Makefile, ONBOARDING.md); docs/guides/P6-GUIDES.md for the rest (CLI-13, DOC-18, UI-30, IAM-19, PAY-11, AI-18, OBS-21, TEST-20, NET-19, SCALE-13, OSS-10, AGENT-13) -- each grounded in this repo's actual code/gaps, not generic advice. NOT attempted (out of this task's scope, noted for the record): - P2-1 (cl0ud app-registry.ts mismatch) -- lives in a different repo (sax3l/cl0ud), not cloned/authorized for this task's single-repo PR flow. - The CI "doctor step" half of P1-15 -- deliberately left out of this PR's .gitea/workflows/ci.yml to avoid a 3-way merge conflict with ci/security-headers-and-scanning, which already restructured that file; add it as a follow-up once that PR merges. This branch was authored independently of, but shares docs/architecture/ ARCHITECTURE.md and docs/slo.md with, fix/honest-health-check and ci/security-headers-and-scanning -- expect a small, normal merge conflict if this is rebased after either of those merge first (both already rebased cleanly against each other and against main as of this commit).
20 KiB
08 · Infrastructure
Verified against actual repo state 2026-07-19 (branch
main, plus.env.exampleread fromfeature/0pt-env-example). Where this doc previously described a target that doesn't match what's committed, that's called out explicitly rather than silently corrected — see the repo's honesty convention indocs/ICEBERG_WRITE_BOUNDARY.md.
Repo structure (actual, not the original apps/web sketch)
The monorepo did not converge on the apps/web/ + packages/shared/ layout this doc originally
sketched. What's actually on main today:
0pt/
├─ CLAUDE.md
├─ README.md · SECURITY.md
├─ docs/ # this documentation set
├─ app/ # Next.js App Router (Vercel) — the "edge" plane, AT THE REPO ROOT
│ ├─ api/ # route handlers: orders, auth, admin, catalog, connections,
│ │ # vehicles, subscriptions, sources, estimates, health, uploads,
│ │ # payments, 0pt/*, screen, webhooks, ai
│ ├─ bestall/ katalog/ kallor/ order/ portal/ admin/ tjanster/ # pages
├─ lib/ # edge-side helpers: db.ts, engine.ts, auth.ts, stripe.ts,
│ │ # orders-store.ts, subscriptions-store.ts, data.ts, services.ts
├─ src/ # SIAX-platform-facing TS layer (NOT under app/ or lib/)
│ ├─ clients/ # cl0ud-, n0d-, aud0-, st0re-, st0re-lakehouse-client.ts
│ ├─ services/ # dataset-, source-, source-query-, iceberg-write-,
│ │ # document-capture-, document-extraction-, external-writeback-,
│ │ # source-rate-policy-, dataset-iceberg-mapping-service.ts
│ ├─ adapters/ # local-blob-store.ts, odoo-bills-adapter.ts, graph-mailbox-adapter, ...
│ └─ types/
├─ components/ hooks/ styles/ # design system in code (docs/05) — data-artifact.tsx,
│ │ # provenance-badge.tsx, embed.tsx, use-reveal.ts, globals.css
├─ packages/schemas/ # JSON Schemas for 0pt.* event/entity contracts (not a pnpm workspace
│ │ # package today — pnpm-workspace.yaml has no `packages:` list)
├─ services/api/ # FastAPI orchestrator + workers (the "engine" plane, on a VPS)
│ ├─ orchestrator/main.py
│ ├─ workers/ # adaptive_router.py, pipeline.py, resolve.py, queue.py, run.py
│ ├─ sources/ # sources.yaml + connectors.py (docs/06)
│ └─ Dockerfile · requirements.txt
├─ sql/ # migrations — see "Schema state" below
├─ infra/
│ ├─ docker-compose.yml # real, committed (not a sketch — see below)
│ └─ Caddyfile # real, committed
├─ tests/p0/ # vitest, TS-side (services/clients/adapters)
└─ (no .github/workflows/ directory exists — see "CI/CD" below)
Known open question (not resolved this pass, Simon's call): CLAUDE.md and README.md both
now note that github.com/sax3l/0pt (this checkout's origin) and git.siax.io/sax3l/0pt (Gitea)
are two remotes that share part of the same history but have independently diverged — Gitea's main
picked up a WAF-bypass/proxy-rotation scraper stack (since removed via a Gitea PR) that never touched
GitHub's main. Which remote is authoritative/deployed, and whether they should be kept in sync, is
undecided. This doc describes what's on this checkout's main only.
Schema state (sql/)
Only two migration files exist in the repo's history, on any branch: sql/001_init.sql (the core
schema — dim_company, dim_contact, fact_financials, orders, etc., per docs/03-data-model.md)
and sql/004_iceberg_write_boundary.sql (opt_dataset_iceberg_mappings + source-rate-policy tables,
per docs/ICEBERG_WRITE_BOUNDARY.md). There is no sql/002_* or sql/003_* anywhere in this
repo's git history (git log --all for those paths returns zero commits) — despite
docs/ICEBERG_WRITE_BOUNDARY.md, docs/11-lakehouse-write-plan.md, and docs/P0_QUERY_SOURCE_FLOW.md
citing sql/002_p0_dataset_quality.sql and a sql/003_* as if they were already-applied migrations.
They are not real. See "Security & compliance" below for what this means for QA/quality-check claims.
Hosting topology
- Vercel —
app/(this repo's root, not a separateapps/web) →0pt.app. Verified this pass (network check performed):https://0pt.appreturns HTTP 200, serves a Next.js/Turbopack app titled "0pt — your data" with SIAX branding, andGET https://0pt.app/api/healthreturns{"ok":true,"service":"0pt-web","db":"not_configured","queue":"not_configured",...}— an exact shape match for this repo's ownapp/api/health/route.ts. So README's "Live: https://0pt.app" is confirmed accurate, and the live deployment confirmsDATABASE_URL/REDIS_URLare not set in production — i.e. the live site is running on the in-memory order/subscription stores described throughout this doc, not against a real Postgres. This does not confirm which git remote/commit is deployed, or that every route behaves as documented — only that the deployment exists and matches this repo's shape. - One VPS (Hetzner, per
docs/02) —infra/docker-compose.yml(see below) runs FastAPI, worker, NocoDB, Metabase, Postgres, Redis, Caddy. This compose file and itsCaddyfileare real, committed files — not a sketch — but this pass found no evidence (no CI/CD, no deploy log, no accessible host) that they have ever actually been run against a live VPS. Treat as "written, unexecuted." - Cloudflare R2 — documented target for file storage.
app/api/uploads/route.tschecks forR2_ACCESS_KEY_ID/R2_BUCKET/R2_ENDPOINTand returns501 storage_not_configuredwhen absent; when "present" it still returns a hardcodedput_url: null— there is no actual presigned-URL generation implemented yet. R2 upload is an env-guarded stub, not a working integration. - Postgres —
lib/db.tsresolvesDATABASE_URL || POSTGRES_URL || POSTGRES_PRISMA_URL(Vercel Postgres integration sets the latter two automatically) and falls back tonull(in-memory stores) when none is set. This part is real and wired.
docker-compose (infra/docker-compose.yml — real file, reproduced here for reference)
services:
postgres:
image: pgvector/pgvector:pg16
environment: { POSTGRES_USER: opt, POSTGRES_PASSWORD: ${PG_PASSWORD}, POSTGRES_DB: opt }
volumes: ["pgdata:/var/lib/postgresql/data", "../sql:/docker-entrypoint-initdb.d:ro"]
redis:
image: redis:7
api:
build: ../services/api
command: uvicorn orchestrator.main:app --host 0.0.0.0 --port 8000
environment: { DATABASE_URL: "postgres://opt:${PG_PASSWORD}@postgres:5432/opt", COMPANIES_HOUSE_API_KEY: "${COMPANIES_HOUSE_API_KEY:-}", INTERNAL_TOKEN: "${INTERNAL_TOKEN}" }
depends_on: [postgres, redis]
worker:
build: ../services/api
command: python -m workers.run
environment: { DATABASE_URL: "postgres://opt:${PG_PASSWORD}@postgres:5432/opt", COMPANIES_HOUSE_API_KEY: "${COMPANIES_HOUSE_API_KEY:-}" }
depends_on: [postgres, redis]
nocodb:
image: nocodb/nocodb:latest
environment: { NC_DB: "pg://postgres:5432?u=opt&p=${PG_PASSWORD}&d=opt", NC_PUBLIC_URL: "https://data.0pt.app" }
depends_on: [postgres]
metabase:
image: metabase/metabase:latest
environment: { MB_DB_TYPE: postgres, MB_DB_HOST: postgres, MB_DB_PORT: "5432", MB_DB_USER: opt, MB_DB_PASS: "${PG_PASSWORD}", MB_DB_DBNAME: opt }
depends_on: [postgres]
caddy:
image: caddy:2
ports: ["80:80", "443:443"]
volumes: ["./Caddyfile:/etc/caddy/Caddyfile", "caddydata:/data"]
depends_on: [api, nocodb, metabase]
volumes: { pgdata: {}, caddydata: {} }
Note vs. an earlier draft of this doc: the api/worker containers here only receive
DATABASE_URL, COMPANIES_HOUSE_API_KEY, and INTERNAL_TOKEN — not STRIPE_*, RESEND_API_KEY,
or R2_*. That's correct as written: Stripe, R2, and (a documented but unbuilt) Resend integration
are all called from app/api/* route handlers, which run on Vercel, not inside this compose stack.
Caddyfile (real, committed):
api.0pt.app { reverse_proxy api:8000 }
data.0pt.app { reverse_proxy nocodb:8080 }
dash.0pt.app { reverse_proxy metabase:3000 }
Environment variables
.env.example now exists and is merged to main (BACKLOG P3-5: the stale
link to the unmerged github.com/sax3l/0pt/pull/3 that used to sit here has
been removed — that PR landed a while ago). It was previously impossible to
commit: .gitignore's blanket .env* rule silently matched .env.example
too, so it was never added despite docs/09-build-plan.md Phase 0 requiring
it since day one; .gitignore now has !.env.example to fix that. README's
"Env inventory: .env.example" link is a real, current reference.
The table below is grep-verified against main this pass, not transcribed from .env.example's own
comments — in one case (ST0RE_URL, see note) that turned up a discrepancy worth flagging.
| Var | Status on main |
Where |
|---|---|---|
DATABASE_URL / POSTGRES_URL / POSTGRES_PRISMA_URL |
Wired — priority-ordered fallback | lib/db.ts |
ENGINE_URL |
Wired — edge defers to FastAPI engine when set, else computes locally | lib/engine.ts |
INTERNAL_TOKEN |
Wired — sent as X-Internal-Token on every engineFetch() call |
lib/engine.ts |
AUTH_SECRET |
Wired, but only for a lightweight HMAC session scaffold — not Auth.js. lib/auth.ts's own comment: "Production auth = Auth.js + Postgres adapter... wired once the database is provisioned" |
lib/auth.ts, app/api/auth/login/route.ts |
DEV_LOGIN_KEY |
Wired — gates the dev-only login route (403 without a match); not a real auth system | app/api/auth/login/route.ts |
AUTH_URL |
Documented target — no read found | — |
STRIPE_SECRET_KEY |
Wired — real Stripe Checkout session creation (test or live key both work; not test-mode-locked in code) | app/api/payments/[order_id]/deposit/route.ts |
STRIPE_WEBHOOK_SECRET |
Wired — real signature verification, advances order status | app/api/webhooks/stripe/route.ts |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY |
Documented target — no read found | — |
R2_ACCESS_KEY_ID, R2_BUCKET, R2_ENDPOINT |
Presence-checked only — gates a 501 vs stub 200; no actual R2 client/presign call exists yet |
app/api/uploads/route.ts |
R2_ACCOUNT_ID, R2_SECRET_ACCESS_KEY |
Documented target — no read found (even the stub above doesn't check these) | — |
OPT_LOCAL_BLOB_DIR |
Wired — local-disk fallback artifact store, used when no object storage is configured | src/adapters/local-blob-store.ts |
RESEND_API_KEY |
Documented target — no Resend call site anywhere in this repo; not "unconfigured", genuinely not implemented | — |
METABASE_SITE_URL, METABASE_SECRET_KEY, NOCODB_BASE_URL, NOCODB_API_TOKEN |
Documented target — no read found. components/embed.tsx renders an iframe from a src prop the caller must supply; no server-side signed-URL generator exists to produce that src |
— |
DOC_INTELLIGENCE_ENDPOINT, DOC_INTELLIGENCE_KEY, DOC_INTELLIGENCE_API_VERSION |
Wired | src/services/document-extraction-service.ts |
GRANT_ENC_KEY |
Presence-checked only — gates 501 vs a stub 201 that returns { grant_id, status: "pending" }. No AES-GCM encryption, no read-only validation, no actual storage happens yet despite the route's own comment describing that flow |
app/api/connections/db-grant/route.ts |
NEXT_PUBLIC_APP_URL |
Wired — used as a fallback origin for building Stripe redirect URLs | app/api/payments/[order_id]/deposit/route.ts |
NEXT_PUBLIC_API_URL |
Documented target — no read found | — |
REDIS_URL |
Presence-checked only, in /api/health — no queue code in this repo reads it (Celery/queue usage, if any, is services/api's own concern, out of this repo's TS surface) |
app/api/health/route.ts |
COMPANIES_HOUSE_API_KEY |
Real key referenced by services/api (Python engine, sources.yaml/connectors.py) and passed through by infra/docker-compose.yml — not part of the Next.js edge app's own env surface |
services/api/ |
Platform-client URLs — corrected from a stronger claim
CL0UD_URL, N0D_URL, AUD0_URL, and ST0RE_URL are all documented targets only — none of
the four is read via process.env.* anywhere in this repo's committed code, grep-verified this
pass (grep -rn "process\.env\.\(ST0RE_URL\|CL0UD_URL\|N0D_URL\|AUD0_URL\)" returns zero matches
outside comments and one it.todo(...) placeholder in tests/p0/iceberg-write-service.test.ts).
This corrects a claim that appears in .env.example itself (on feature/0pt-env-example) and was
carried into an earlier version of this doc: that comment states "ST0RE_URL IS read
(src/clients/st0re-client.ts, src/clients/st0re-lakehouse-client.ts)". That is not accurate as of
this verification. Both of those client files — like cl0ud-client.ts, n0d-client.ts, and
aud0-client.ts — only export a create*Client(baseUrl?: string) factory that falls back to an
in-memory mock when no baseUrl argument is passed. Every call site in this repo (app/api/0pt/**
routes, src/services/*.ts) either passes no baseUrl at all or forwards an opts?.st0reBaseUrl-style
constructor option that nothing upstream ever populates from process.env.ST0RE_URL. This matches
docs/ICEBERG_WRITE_BOUNDARY.md's own "Verification status" section, which lists wiring
ST0RE_URL → IcebergWriteService({ st0reBaseUrl: process.env.ST0RE_URL }) as future work, not
something already done.
Net effect: setting any of CL0UD_URL/N0D_URL/AUD0_URL/ST0RE_URL in a deployed environment
today does nothing by itself. A future pass has to wire each route/service to actually read its
corresponding env var before the value has any effect. Until then, every 0pt→platform call in this
repo runs against the deterministic in-memory mock built into each client file — real, tested TS, but
never exercised against a live CL0UD/N0D/AUD0/ST0RE instance from this codebase.
Security & compliance — corrected against actual code, not the target design
The items below were previously described as already-built behavior. Grep/read-verified this pass, several are env-guarded stubs that return a plausible-shaped response without doing the described work. Stating this plainly, per this repo's own honesty convention:
-
Customer DB grants —
app/api/connections/db-grant/route.tsreturns501whenGRANT_ENC_KEYis unset, and a stub201 { grant_id, status: "pending" }when it is set. There is no AES-GCM encryption, no read-only-role validation, and no persistence implemented yet — the route's own comment describes the target behavior ("validate read-only, AES-GCM encrypt conn_string, store, audit") but the code doesn't do it. This is a real gap, not a doc typo. -
File delivery / presigned R2 URLs — same pattern:
app/api/uploads/route.tsis a501stub with a hardcodedput_url: null. No presigned URL is ever actually generated in this repo today. -
Tenant/org isolation — OPEN SECURITY GAP, not a deliberate two-tier design. Several
app/api/0pt/*routes readtenant_id/project_iddirectly from the request body instead of from a server-bound session:app/api/0pt/lakehouse/mappings/route.tsapp/api/0pt/lakehouse/mappings/[mappingId]/writes/route.tsapp/api/0pt/sources/rate-policies/route.tsapp/api/0pt/sources/[sourceId]/query/route.tssrc/services/document-capture-service.ts
This is not a defensible internal-vs-external boundary — it's because no auth/session system exists anywhere in this repo yet.
app/api/orders/route.ts's ownGEThandler carries the literal comment// org scoping added with auth— i.e., not yet.lib/auth.tsis a lightweight HMAC session scaffold gated by a dev-only login key, unconnected to any of the0pt/*routes above. Any caller can currently claim anytenant_idfor these endpoints. Fixing this is Phase 3 ofdocs/09-build-plan.md(Auth & portal shell) — it is Simon's call how that auth design bindstenant_idserver-side; it is not built anywhere in this repo today. -
PII (contacts/UBO) / DPA — no code found in this pass implementing lawful-basis recording, deletion-on-request, or a published DPA. Treat as target design, per
CLAUDE.md§5, not shipped. -
Secrets rotation / least privilege — no rotation tooling found in this repo (expected — that's an operational practice, not application code — but noting it's unverified either way).
-
Quality/QA gate on delivered data —
src/services/quality-check-service.ts,src/services/quality-report-service.ts,src/types/quality-check.ts,src/types/quality-report.ts,src/data-platform/services/catalog-service.ts, andsql/002_p0_dataset_quality.sql/sql/003_*.sqlwere at one point referenced indocs/ICEBERG_WRITE_BOUNDARY.md,docs/11-lakehouse-write-plan.md, anddocs/P0_QUERY_SOURCE_FLOW.mdas if built and tested. None of these files has ever existed in this repo's git history, on any branch (git log --allconfirms zero commits for every one of those paths). All three of those docs have since been corrected (verified this pass) — each now carries its own "fabricated, not built" callout and none currently asserts those files as real; this section restates the underlying fact because it directly bears on the "Security & compliance" claims below: there is no lakehouse-side data-quality/QA enforcement built in this repo today. (The Python engine'sservices/api/workers/pipeline.pydoes have a real, working confidence-threshold drop —delivered = [r for r in deduped if r["confidence"] >= threshold]— but that's a separate, narrower mechanism inside the FastAPI pipeline, not the TS/lakehouse quality-check layer those three docs describe.)
Embedding NocoDB / Metabase
components/embed.tsx (EmbeddedView) is real, committed code — a sandboxed <iframe> that renders
whatever src URL its caller passes, with a placeholder state when src is null. What's missing:
the server-side code that generates a signed Metabase/NocoDB embed URL from METABASE_SECRET_KEY /
NOCODB_API_TOKEN. No such generator was found in this pass. So the embed shell exists; the
signed-URL plumbing that would make it show real customer data does not yet.
CI/CD
There is no .github/workflows/ directory anywhere in this repo. No lint/typecheck/build/test
pipeline runs in CI today. On the TS side, pnpm lint, pnpm typecheck, and pnpm test (vitest,
tests/p0/*.test.ts) exist as local scripts in package.json, but nothing invokes them automatically
on push/PR. On the Python side there is no lint/test tooling at all — grep-verified this pass:
ruff and pytest appear nowhere in services/api/requirements.txt or anywhere else in this repo
(no pyproject.toml, no pytest.ini, no test files). services/api/README.md's own "Run locally"
section only lists pip install -r requirements.txt + uvicorn/python -m workers.run — no lint or
test step. An earlier version of this doc claimed ruff/pytest "exist as local scripts" for
services/api; that was unverified and is corrected here. This is a real gap against
docs/09-build-plan.md Phase 0's "CI skeleton" deliverable — not yet built, not a doc omission.
Monitoring
- Vercel Analytics — genuinely wired:
app/layout.tsximports and renders<Analytics />from@vercel/analytics/next(package.jsondependency@vercel/analytics@1.3.1). - Sentry — not found. No
sentrypackage inpackage.json, no Sentry init/config file anywhere in the repo. This is a documented target only, not shipped. - Uptime ping on
/health—app/api/health/route.tsexists and returns{ ok, db, queue, ts }based on env presence checks; no external uptime monitor wired to it was found in this repo (that would live outside this codebase regardless, e.g. an external pinger — unverified either way). - Worker/job health in admin —
app/admin/jobs/page.tsxexists as a page; this pass did not trace whether it reads real job state or placeholder data (out of this doc's grounding scope — flagged for whoever ownsdocs/01-product.md/admin-portal docs to verify).