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
09 · Build plan
Phased, in dependency order. Finish a phase to its acceptance criteria before the next. Each phase = small, reviewable commits. Reconcile with existing code rather than rewriting wholesale.
Status callouts added 2026-07-19. Each phase below now carries a
Status:line, grep/read-verified againstmainthis pass. These are conservative on purpose: a phase is only called Done when this pass found real, working code satisfying its Accept criteria; anything else is Partial (with the specific evidence and gap named) or Not started. No phase is upgraded to Done without a code citation. Where a referenced doc (e.g.docs/design/...) doesn't exist, that's flagged rather than silently worked around. Seedocs/08-infrastructure.mdfor the full env/security grounding these statuses draw on.
Phase 0 — Foundation & repo
- Monorepo structure (
docs/08). Migrate existingsrc/orchestrator+src/workersintoservices/api. - Postgres schema + migrations (
docs/03);pgvectorenabled..env.example. docker-composeup (postgres, redis, api, worker, nocodb, metabase, caddy)./healthgreen.- CI skeleton (lint/typecheck/build).
- Accept:
docker compose upruns the whole stack locally; migrations apply;/healthreturns ok for db + queue. - Status: Partial.
- Repo structure exists but diverged from the
apps/web/packages/sharedlayoutdocs/08originally sketched — it settled onapp/(Next.js, repo root),lib/,src/(platform clients/services), andservices/api/(FastAPI).docs/08-infrastructure.mdnow documents the structure as-built. - Migration to
services/api/orchestrator+services/api/workersis done (verified: those directories exist and containmain.py/adaptive_router.py/pipeline.py/etc.; nosrc/orchestratororsrc/workersremain at the old paths). - Postgres schema is real:
sql/001_init.sql(CREATE EXTENSION vector,CREATE EXTENSION pgcrypto, 17 tables incl.orgs,users,orders,jobs,audit_log,bridge_provenance,db_grants,payments) +sql/004_iceberg_write_boundary.sql. Nosql/002_*orsql/003_*exists anywhere in this repo's history — some other docs (docs/ICEBERG_WRITE_BOUNDARY.mdetc.) cite asql/002_p0_dataset_quality.sqlthat was never built; seedocs/08-infrastructure.md's "Schema state" section. .env.example— now exists and is merged tomain(BACKLOG P3-5: the stale link to the unmergedgithub.com/sax3l/0pt/pull/3that used to sit here has been removed — that PR has long since landed,.env.exampleis a real, current, gitignore-exempted file at the repo root today).infra/docker-compose.ymlandinfra/Caddyfileare real, committed files (not sketches) covering exactly the services listed. This pass found no evidence they've ever actually been run — no CI, no deploy log, no reachable host checked. "File exists and looks correct" ≠ "verified to run"; treat the Accept criterion ("docker compose up runs... /health returns ok") as unverified.- CI skeleton — not built. There is no
.github/workflows/directory in this repo at all.pnpm lint/typecheck/testexist as local scripts only.
- Repo structure exists but diverged from the
Phase 1 — Design system in code
- Tailwind + shadcn;
globals.css(grain + reveal keyframes) anduseRevealverbatim (docs/05). - Build DataArtifact primitive (table/map/schema/chart/grid) and ProvenanceBadge (
docs/designprompts 5.1, 2.11). - Accept: a sample page renders mono tokens, a DataArtifact animates on scroll, reduced-motion respected, focus rings visible.
- Status: Partial — real components exist; scroll-motion/reduced-motion/focus-ring behavior not
independently exercised this pass.
- Tailwind + shadcn/radix stack: confirmed real in
package.json(tailwindcss,@radix-ui/*,class-variance-authority,tailwind-merge,tailwindcss-animate). hooks/use-reveal.ts,components/data-artifact.tsx,components/provenance-badge.tsxall exist and are used acrossapp/katalog,app/kallor,app/bestall,app/order/[id], and mostcomponents/sections/*files (grep-confirmed).docs/design/0pt-v0-build-brief.mdanddocs/design/0pt-design-coverage-and-visuals.md(the "prompts 5.1, 2.11" this bullet references) do not exist anywhere in this repo — confirmed via full-repo search; there is nodocs/design/directory at all.CLAUDE.md's own doc index has already been corrected to flag this as a gap rather than list them as real. Treat any reference to specific numbered "build-brief prompts" in this build plan as pointing at a doc that was never written — not this file's scope to author that content.- Reduced-motion handling, focus-ring visibility, and actual scroll-triggered animation behavior were not executed/verified in this pass (no browser run performed) — code presence is confirmed, runtime behavior is not.
- Tailwind + shadcn/radix stack: confirmed real in
Phase 2 — Marketing + catalog (placeholder data)
- Landing components top-to-bottom (build-brief 2.1–2.10): Header, Hero, Philosophy, Catalog teaser, Engine, Visualisation gallery, Coverage, Editorial, Proof, Footer.
- Full Catalog page + Dataset detail + Sourcing-map page (coverage doc 5.4). Postgres full-text search.
- All with loading/empty/error states + mobile.
- Accept: marketing site + catalog browse work end-to-end on placeholder catalog rows; one continuous scroll reads well; DoD met per screen.
- Status: Partial.
- Landing section components are real:
components/sections/{hero,philosophy,gallery,editorial, pricing,technology,testimonials,footer,featured-products,collection}-section.tsxall exist. app/katalog/page.tsx,app/katalog/[id]/page.tsx,app/kallor/page.tsxall exist.- Postgres full-text search was not found — grepped for
to_tsvector/websearch_to_tsqueryacross the whole repo, zero matches. Catalog browsing today reads fromlib/data.tsplaceholder data, not a full-text query against Postgres. This Accept criterion's "catalog browse... on placeholder catalog rows" half is plausible; the Postgres full-text search half is not built. - Loading/empty/error states + mobile + DoD-per-screen were not independently audited screen-by-screen
this pass —
components/states.tsx(LoadingSkeleton,ErrorState) exists and is imported widely, which is evidence toward this but not a full verification.
- Landing section components are real:
Phase 3 — Auth & portal shell
- Auth.js + Postgres adapter; sign in/up/verify/forgot.
orgs/users. - Portal shell + Dashboard ("Mina beställningar") with empty state.
- Accept: a user can sign up, land in an empty dashboard, sign out; routes protected.
- Status: Not done — this is the most consequential gap in the whole repo, not a minor loose end.
- There is no Auth.js integration.
lib/auth.tsis a from-scratch, ~40-line HMAC-SHA256 session scaffold (Web Crypto, no dependency) whose own comment states: "Production auth = Auth.js + Postgres adapter + OAuth (docs/02) — wired once the database is provisioned."app/api/auth/login/route.tsis a dev-only login gated by a sharedDEV_LOGIN_KEY, not sign-up/verify/forgot. orgsanduserstables exist insql/001_init.sql(schema is ready), but nothing in the app reads/writes them for real session-bound identity today.app/portal/page.tsxand sibling portal pages exist (shell is real), but routes are not protected by any real session check tied to that schema.- Concretely, this means the tenant/org isolation several
0pt.*API routes assume does not exist yet.app/api/0pt/lakehouse/mappings/route.ts,.../writes/route.ts,app/api/0pt/sources/rate-policies/route.ts,app/api/0pt/sources/[sourceId]/query/route.ts, andsrc/services/document-capture-service.tsall readtenant_id/project_idstraight from the request body — anyone can claim any tenant.app/api/orders/route.ts's ownGEThandler carries the literal comment// org scoping added with auth. This is an open security gap, not a two-tier design choice — seedocs/08-infrastructure.md's "Security & compliance" section for the full list. Do not build any doc or feature that assumes tenant-scoped auth already exists.
- There is no Auth.js integration.
Phase 4 — Order lifecycle end-to-end (test mode)
- Configurator (2.12) wired to
POST /estimates(real estimate math in FastAPI). - Deposit checkout (2.13) → Stripe test Checkout → webhook → order
IN_PRODUCTION. - Production tracker (2.14) reading
GET /orders/{id}/events(SSE) — pipeline may be simulated at this phase. - Validation & delivery (2.15): tabs render from a sample delivery; accept →
POST /payments/release(test) →DELIVERED; revise leaves escrow untouched. - AI-assist (
POST /ai/parse) prefilling the configurator. - Accept: a full custom order can be configured, deposit paid (test), watched, validated, accepted, and "delivered" — state machine in
docs/01traversed; escrow split correct. - Status: Partial — the payment/escrow spine is genuinely real; the SSE tracker is not.
app/api/estimates/route.tsexists.- Deposit checkout is real, working code:
app/api/payments/[order_id]/deposit/route.tscalls Stripe's Checkout Sessions API directly (not a stub) andapp/api/webhooks/stripe/route.tsverifies the webhook signature and advances order status viasetStatus. app/api/payments/[order_id]/release/route.tsis also real: it enforces the escrow rule in code — balance release is rejected with409 not_acceptedunlessorder.status === "accepted"— matching the "escrow split correct" accept criterion's intent.GET /api/orders/{id}/events(SSE) does not exist. OnlyGET/PATCH /api/orders/{id}were found (app/api/orders/[id]/route.ts); no/eventssub-route, and noEventSource/text/event-stream/ReadableStreamusage anywhere inapp/(grep-verified). If a production tracker is live today, it's pollingGET /api/orders/{id}, not the SSE stream this bullet describes — or it hasn't been built. Either way, the SSE contract as written is not implemented.app/api/ai/parse/route.tsis real: a genuine (non-fabricated) keyword/regex heuristic that prefills a config object, with a documented fallback toengineFetch("/ai/parse", ...)whenENGINE_URLis set. This one holds up as described.
Phase 5 — Real data engine (Nordic slice)
- Compliant connectors first: GLEIF + Brreg + Companies House (verify access terms; record in
sources.yaml). Remove any FlareSolverr/bypass path (docs/06). - Pipeline stages produce real
dim_company+fact_financialswithbridge_provenance; QA threshold drops low-confidence rows. - Production tracker now reflects real job metrics.
- Accept: a Nordic company dataset is produced for a real order, every field has provenance, quality report generated, level-0 fields shown honestly (not fabricated).
- Status: Partial.
services/api/sources/sources.yamlandconnectors.pyare real: GLEIF, Brreg, Companies House, PRH, and CVR are all registered, each with an explicitlicence: "... — VERIFY"tag rather than an assumed-clean claim — matches the "verify access terms" instruction.services/api/workers/adaptive_router.py's own comment confirms: "There are NO bypass/stealth/ proxy-rotation tiers here, by design (CLAUDE.md §1)" — the FlareSolverr/bypass removal is done, at least on this repo'smain(seedocs/08-infrastructure.md's "Known open question" about the diverged Gitea remote, which independently added such a stack on its ownmain— unresolved which remote is canonical).services/api/workers/pipeline.pyhas a real, working confidence-threshold QA drop:delivered = [r for r in deduped if r["confidence"] >= threshold], plus anavg_confidencefigure in its output. This is a genuine, narrow QA mechanism inside the Python engine.- This is not the same thing as the "quality report" / quality-check layer once described in
docs/ICEBERG_WRITE_BOUNDARY.md,docs/11-lakehouse-write-plan.md, anddocs/P0_QUERY_SOURCE_FLOW.md. Those three docs used to citesrc/services/quality-check-service.ts,src/services/quality-report-service.ts,src/types/quality-{check,report}.ts, andsrc/data-platform/services/catalog-service.tsas already-built and tested. None of those files has ever existed in this repo's git history, on any branch — and all three docs have since been corrected to say so explicitly (verified this pass), rather than repeating the original claim. So: the narrow Python-side confidence filter is real; the broader TS/lakehouse "quality report" this Accept criterion likely also intends is not built (seedocs/P0_DATASET_QUALITY_FLOW.mdfor the honest target spec). Whetherpipeline.py'savg_confidenceoutput satisfies "quality report generated" is a judgment call for whoever runs this phase — flagging rather than deciding it here. - Whether an actual Nordic dataset has been produced end-to-end for a real order was not verified this pass (would require running the stack).
Phase 6 — Delivery surfaces & integrations
- Exports: CSV / Parquet / JSON via R2 presigned URLs.
- Embedded NocoDB ("Min data") and Metabase dashboards via signed URLs in mono chrome.
- Integrations: FiveCRM push, webhooks, Google Sheets.
- File uploads (Uppy → R2) for matching/enrichment.
- Accept: a delivered dataset is downloadable, viewable in embedded NocoDB + Metabase (no separate login), and pushable to FiveCRM; uploads work.
- Status: Not done — this phase is UI-shell-only today.
- CSV/Parquet/JSON exports via R2 presigned URLs: not found.
app/api/uploads/route.ts(the closest R2-touching code) is a501stub with a hardcodedput_url: null— no presign logic exists yet, for uploads or exports. components/embed.tsx(EmbeddedView) is real — a sandboxed iframe shell — but the server-side signed-URL generator for Metabase/NocoDB (METABASE_SECRET_KEY/NOCODB_API_TOKEN) that would feed it a realsrcwas not found anywhere in this pass.- "FiveCRM" appears only as a destination label string in
lib/data.ts,lib/services.ts,app/portal/integrationer/page.tsx(explicitly described in that file's own comment as "presentationella toggles" — i.e. presentational, not wired), and as one of several regex-matched destination names inapp/api/ai/parse/route.ts's heuristic. No actual FiveCRM API push exists. uppyis not a dependency inpackage.json(checked directly) — the Uppy → R2 upload flow this bullet describes has no corresponding package installed, let alone implemented.
- CSV/Parquet/JSON exports via R2 presigned URLs: not found.
Phase 7 — Subscriptions / feeds
subscriptions; recurring jobs (weekly/daily) delivering deltas; webhooks on change.- Freshness monitoring; Resend emails (confirmation, "din data är klar", feedback).
- Accept: an order converts to a feed; a scheduled run produces a delta + fires a webhook + email.
- Status: Partial — data layer real, scheduling and email are not.
lib/subscriptions-store.tsis real CRUD against thesubscriptionstable (with an in-memory fallback when no DB is configured) —createSubscription/listSubscriptionsgenuinely insert/read.- No recurring-job scheduler (cron, queue consumer producing deltas, etc.) was found on the TS side of
this repo.
services/api/workers/queue.pyexists on the Python side but this pass did not trace whether it implements actual delta-producing scheduled runs — flagged as unverified rather than assumed either way. - Resend is not wired at all — grepped for
resend/Resendacross the whole repo, zero matches outsideRESEND_API_KEYbeing listed as an env var. No confirmation/"din data är klar"/feedback email has been implemented.
Phase 8 — Admin, hardening, compliance
- Admin: order queue + scope/quote + escrow release; source registry & freshness monitoring; worker/job monitoring; customer + catalog management.
- Customer DB-access grant flow (encrypted, read-only, time-boxed) + revoke.
- Security pass: RLS, presigned-only files, signed embeds, secret rotation,
audit_logeverywhere; DPA published; legal pages. - Finalize all loading/empty/error states + mobile across the app.
- Accept: admin can run the business; DB-grant flow works and self-revokes; security checklist passes; DoD met app-wide.
- Status: Partial — UI shell real, the security substance is not.
- Admin pages exist:
app/admin/{page,jobs/page,sources/page,customers/page,verktyg/page}.tsx. This pass did not verify whether they read real backend data or placeholders. sql/001_init.sqldefinesaudit_log,db_grants, andpaymentstables — schema is ready.- The DB-access grant flow is a stub, not a working feature.
app/api/connections/db-grant/route.tsreturns501withoutGRANT_ENC_KEY, and with it set returns a hardcoded201 { grant_id, status: "pending" }— no AES-GCM encryption, no read-only validation, and no self-revoke logic exist in code today, despite the route's own comment describing that target behavior. Seedocs/08-infrastructure.md's "Security & compliance" section. - RLS enforcement, presigned-only files (blocked on the Phase 6 R2 gap above), signed embeds (blocked on the Phase 6 NocoDB/Metabase gap above), and a published DPA were not found as implemented in this pass. Treat the "security checklist passes" accept criterion as not met.
- Admin pages exist:
Phase 9 — Launch
- Domain
0pt.app(Vercel) + subdomains (api/data/dash via Caddy). Sentry + uptime live. - Soft launch: Protosell internally + a handful of existing dealer relationships.
- Accept: a real external customer completes an order start→delivery in production; feedback loop running.
- Status: Domain is live; soft-launch/customer criterion not verified.
- Verified this pass (network check performed):
https://0pt.appreturns HTTP 200 and serves this repo's own Next.js app (title "0pt — your data", SIAX branding);GET /api/healthreturns{"ok":true,"service":"0pt-web","db":"not_configured","queue":"not_configured",...}, an exact shape match forapp/api/health/route.ts. README's "Live: https://0pt.app" is confirmed, and the response confirms the live deployment has noDATABASE_URL/REDIS_URLset — seedocs/08-infrastructure.md's "Hosting topology" section. This does not confirm which git remote/commit is deployed (see the diverged-remotes note elsewhere in this doc), or that api/data/dash subdomains or Caddy are live — only the root Vercel deployment was checked. - Sentry is not wired — no
sentrypackage inpackage.json, no Sentry config file anywhere in the repo (grep-confirmed).@vercel/analyticsis genuinely wired (app/layout.tsxrenders<Analytics />), which is a real but much smaller piece of the "Sentry + uptime live" bullet. - No evidence either way on the soft-launch/external-customer criterion — out of this doc's ability to verify from code alone.
- Verified this pass (network check performed):
Working conventions (for Claude Code)
- Tech docs + identifiers English; UI copy Swedish.
- Conventional Commits; one concern per PR; keep diffs reviewable.
- No secrets in code; update
.env.examplewhen adding env. (.env.exampleitself currently lives onfeature/0pt-env-example/ PR #3 — merge that first, or this instruction has nothing to update yet.) - Add/extend tests for engine logic (estimate math, fusion/confidence, dedup) and API contract.
- When a source's access/licence is unclear, verify and record in
sources.yamlbefore relying; if disallowed for bulk, route to licensed/partner or mark level 0 — never bypass, never fabricate. - Touch the design tokens/motion only via
docs/05; keep red ≤ once per screen; no photography. - If anything here conflicts with existing code, flag it in the PR description and propose the reconciliation rather than guessing.
- Two git remotes claim to be this repo (
github.com/sax3l/0ptandgit.siax.io/sax3l/0pt), and they have independently diverged (seeCLAUDE.md,README.md). Don't assume the checkout you're in is the only copy, and don't silently pick one as canonical — that's Simon's decision, flag it if it becomes relevant to the phase you're working on.