Hero is now a data-native 0pt artifact (no photos, keeps grain/reveal/float); header is the 0pt wordmark with Swedish nav; layout is sv + loads JetBrains Mono + 0pt metadata; globals wires the loaded fonts. Adds CLAUDE.md + docs/01-09 as the build contract. next build green (5 routes). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.2 KiB
5.2 KiB
08 · Infrastructure
Repo structure (monorepo)
0pt/
├─ CLAUDE.md
├─ docs/ # this documentation set
├─ apps/
│ └─ web/ # Next.js (Vercel): marketing, catalog, portal, admin
├─ services/
│ └─ api/ # FastAPI orchestrator + workers (existing src/* migrates here)
│ ├─ orchestrator/ # main.py + routers (docs/04-api.md)
│ ├─ workers/ # adaptive_router, fetch_worker, ocr_worker, pipeline, db
│ └─ sources/ # sources.yaml + connectors (docs/06)
├─ packages/
│ └─ shared/ # shared TS types (mirror API contract), design tokens
├─ sql/ (or services/api/alembic/) # migrations (docs/03)
├─ infra/
│ ├─ docker-compose.yml # VPS always-on services
│ └─ Caddyfile # reverse proxy + TLS
└─ .github/workflows/ # CI/CD
Hosting topology
- Vercel —
apps/web→0pt.app. Free Hobby until paying customers. - One VPS (Hetzner ~€5/mo) —
docker-compose: FastAPI, workers, NocoDB, Metabase, Postgres (or Neon), Redis (if Celery), Caddy. Caddy terminates TLS and proxiesapi.0pt.app,data.0pt.app(NocoDB),dash.0pt.app(Metabase). - Cloudflare R2 — files.
- Postgres — on VPS (
pgvector/pgvector:pg16) or Neon managed.
docker-compose (infra/docker-compose.yml — sketch)
services:
postgres:
image: pgvector/pgvector:pg16
environment: { POSTGRES_USER: opt, POSTGRES_PASSWORD: ${PG_PASSWORD}, POSTGRES_DB: opt }
volumes: ["pgdata:/var/lib/postgresql/data"]
redis:
image: redis:7
api:
build: ../services/api
command: uvicorn orchestrator.main:app --host 0.0.0.0 --port 8000
environment: [DATABASE_URL, R2_*, STRIPE_*, RESEND_API_KEY, INTERNAL_TOKEN]
depends_on: [postgres, redis]
worker:
build: ../services/api
command: python -m workers.run # or celery -A workers worker
environment: [DATABASE_URL, R2_*]
depends_on: [postgres, redis]
nocodb:
image: nocodb/nocodb
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
environment: { MB_DB_TYPE: postgres, MB_DB_HOST: postgres, 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"]
volumes: { pgdata: {}, caddydata: {} }
Environment variables (full inventory)
# Postgres
DATABASE_URL=postgres://opt:***@host:5432/opt
PG_PASSWORD=***
# Internal auth (Next ↔ FastAPI)
INTERNAL_TOKEN=*** # sent as X-Internal-Token
# Auth.js
AUTH_SECRET=***
AUTH_URL=https://0pt.app
# Stripe
STRIPE_SECRET_KEY=sk_***
STRIPE_WEBHOOK_SECRET=whsec_***
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_***
# Cloudflare R2 (S3-compatible)
R2_ACCOUNT_ID=*** R2_ACCESS_KEY_ID=*** R2_SECRET_ACCESS_KEY=*** R2_BUCKET=opt-files R2_ENDPOINT=https://<acct>.r2.cloudflarestorage.com
# Email
RESEND_API_KEY=re_***
# Embeds (signed URLs)
METABASE_SITE_URL=https://dash.0pt.app METABASE_SECRET_KEY=***
NOCODB_BASE_URL=https://data.0pt.app NOCODB_API_TOKEN=***
# Encryption for db_grants
GRANT_ENC_KEY=*** # 32-byte key, AES-GCM
# App
NEXT_PUBLIC_API_URL=https://api.0pt.app
Never hardcode. Vercel env for apps/web; VPS .env (git-ignored) for compose. Provide .env.example with empty values.
Security & compliance (build in, not bolt on)
- Customer DB grants: customer creates a read-only, table-scoped, time-boxed DB role; we store the connection string AES-GCM encrypted (
GRANT_ENC_KEY), validate it's read-only, and delete after the job. IP-allowlist; prefer a replica. Log access inaudit_log. - File delivery: short-lived presigned R2 URLs; never public buckets.
- Isolation: API filters by
org_id; Postgres RLS where practical; NocoDB/Metabase exposed only via signed embed URLs scoped per customer/order (no separate login surfaced). - PII (contacts/UBO): lawful basis recorded per delivery; honor deletion; minimize retention.
- DPA: ship a Data Processing Agreement; provenance log + audit log are part of the offering.
- Secrets: rotate; least privilege; no secrets in client bundles (only
NEXT_PUBLIC_*).
Embedding NocoDB / Metabase (so it feels like one app)
- Generate a signed embed URL server-side (Metabase signed embedding with
METABASE_SECRET_KEY; NocoDB shared/locked view + token) scoped to the customer's data, and render it in an iframe inside the mono chrome. The customer never sees a NocoDB/Metabase login.
CI/CD (.github/workflows)
- web: lint + typecheck + build → deploy to Vercel on push to
main(Vercel Git integration). - api: lint (ruff) + tests (pytest) → build Docker image → deploy to VPS (SSH
docker compose pull && up -d, or a small runner). - Run DB migrations as a deploy step.
Monitoring (cheap)
- Vercel Analytics + Speed Insights (web).
- Sentry free tier (web + api).
- Uptime ping on
/health. - Worker/job health visible in admin (reads
jobs).