# Production readiness What stands between the current build and taking real money from real people. Ordered roughly by risk, not by effort. Items marked **blocker** must be done before the platform accepts a single forint; the rest can follow a soft launch. Status of the codebase is in the [Already done](#already-done) section at the bottom, so this list stays a list of *gaps*. --- ## 1. Backups and data safety — **blocker** Nothing exists today. The database will hold pledges, shipping addresses and a private donor list; losing it is worse than downtime, leaking it is worse than losing it. - [ ] `scripts/backup.sh` — `pg_dump` via `docker run --rm postgres:17-alpine` so the dump tool always matches the server version and the host needs no `postgres-client` - [ ] Encrypt at rest (age or gpg). **The key must also live somewhere other than the machine being backed up** — otherwise a dead box leaves you with ciphertext and nothing else - [ ] Ship offsite. Destination still undecided: another machine over SSH (`restic`/`rsync`), or object storage via `rclone`. Per SPEC.md's threat model, prefer somewhere outside Hungarian jurisdiction - [ ] Schedule with a **systemd timer on the host, not a compose service** — a `docker compose down` must never silently stop backups, and backup credentials must not sit in the app's environment - [ ] Retention policy (e.g. 7 daily, 4 weekly, 6 monthly) - [ ] `scripts/restore.sh` — provision a scratch database, load a dump, assert row counts. **A backup nobody has restored is a hypothesis** - [ ] CI step: prove a dump restores into a scratch database - [ ] Dead-man switch so a silently failing backup gets noticed - [ ] Provider volume snapshots as a second layer — different failure domain (`pg_dump` covers a bad migration; snapshots cover a dead disk). Pure infra, nothing in this repo ## 2. Payments — **blocker** The entire reason the platform exists. Nothing is built. - [ ] Stripe Connect: platform account owned by the egyesület, Express accounts for each approved organisation - [ ] Checkout: tier → quantity → shipping (if `requiresShipping`) → guest email or account → Payment Element → confirm - [ ] **Verify HUF handling against current Stripe docs before writing any money code** — forint does not follow the plain smallest-unit rule - [ ] Webhook endpoint: signature verification, idempotency keyed on `event.id` - [ ] The fulfillment guard: donation and digital charge immediately, physical tiers authorize and capture only if the goal is met - [ ] Inventory enforcement server-side — `inventoryLimit` is currently only a display concern and is trivially bypassed - [ ] Refund path - [ ] Tests. This is money logic and the one place tests genuinely earn their keep ## 3. Campaign close job — **blocker** Nothing schedules anything today. This is the job that decides whether physical pledges get captured or released. - [ ] Worker or systemd timer that runs at `endsAt` - [ ] Idempotent and safe to re-run — **never double-charge a pledge** - [ ] Capture or release physical pledges by goal outcome, set campaign status - [ ] Notify backers and the creator - [ ] Dead-man switch: if the job does not run, someone must find out ## 4. Auth and accounts Real passwordless auth is in (magic links, implemented in `api/internal/auth`). Admin-only for now. - [x] Magic-link sign-in, sessions in Postgres, no passwords anywhere - [x] Admin rights via the `isAdmin` flag or the `ADMIN_EMAILS` allowlist - [x] A signed-in non-admin is refused by every `/api/admin/*` route. Note that the `/admin` document itself is now public HTML carrying no data — the gate moved from the page to the API when the frontend became an SPA - [ ] **Magic links currently print to the server log** — unusable in production until email is wired up (section 7). This is what makes auth still a launch dependency - [ ] Creator accounts and organisation membership. Users and organisations are still unconnected in the schema, so every admin can edit every campaign. Needed once organisations log in for themselves — and then every server action needs an ownership check, not just the pages - [ ] Guest pledging (email only, no account) — already assumed by the schema - [ ] Rate limiting on magic-link requests (section 9) - [ ] Session security review before launch: cookie flags behind Caddy ## 5. Admin and moderation — **blocker** Campaign creation is invite-only by design — invite-only enough that campaigns are set up by hand in the database. The app only edits what is already there, and the API refuses a save that carries no campaign id. - [x] Campaign edit form (`/admin`); creation deliberately not offered - [x] Reward tiers editable, with a server-side guard that refuses to delete a tier that already has pledges - [ ] Organisation application form and approval queue - [ ] Campaign submit → review → launch approval - [ ] **Explicit political-content check in the approval flow** — no party, campaign or candidate may be funded. This is the boundary that keeps the platform outside párttörvény reporting - [ ] Backer list visible to the creator for fulfillment only, never exportable as a general mailing list ## 6. Legal and organisational — **blocker** Mostly not code, and mostly slow. Start early. - [ ] Found the egyesület (10 members, statutes, court registration — weeks to months). **Stripe live mode needs the registered entity, tax number and bank account.** This is the real launch gate, not the code - [ ] Register `szabadpalya.hu` — verify at a registrar; DNS absence is not proof it is free - [ ] ÁSZF, adatkezelési tájékoztató, impressum - [ ] Distance-selling disclosures on physical tiers: seller identity, delivery deadline, total price incl. ÁFA and shipping, 14-day withdrawal notice (45/2014. (II. 26.) Korm. rendelet) - [ ] Decide who covers the egyesület's running costs, given the 0% fee - [ ] Cookie/analytics posture — prefer something that needs no consent banner ## 7. Email — **blocker** - [ ] Transactional provider (Resend or Postmark). **Do not self-host SMTP** — receipts that land in spam are support tickets - [ ] SPF, DKIM, DMARC on the domain - [ ] Templates: pledge receipt, campaign closed (funded / not funded), physical pledge released, magic link, creator notifications ## 8. Ops - [ ] Caddy in front, automatic TLS (named in SPEC.md, not written) - [ ] Secrets on the host: documented, file permissions, not in the repo. `.env.example` currently lists one variable - [ ] Sentry or equivalent for error tracking - [ ] Uptime monitoring against `/api/health` - [ ] Log retention and rotation - [ ] Deploy story: Coolify or a documented `docker compose` procedure - [ ] Rollback procedure, including how to handle a bad migration - [ ] Staging environment, or an explicit decision to go without one ## 9. Security and privacy - [ ] Security headers and CSP — matters once card fields are on the page - [ ] Rate limiting on checkout, magic links and the webhook endpoint - [ ] GDPR: data export and deletion path for backers - [ ] Confirm no PII reaches logs or Sentry - [ ] Dependency audit in CI - [ ] Security review of the money path before launch ## 10. Media uploads - [ ] Image upload for campaign galleries — currently every campaign uses a generated SVG placeholder - [ ] Storage on a mounted volume, included in the backup set - [ ] Size and type limits, EXIF stripping ## 11. Public site polish Cheap, visible, and currently missing. - [ ] Favicon and app icons — `brand/out/icon-*.png` already exist and are simply not wired in. The scaffold default is still in place - [ ] `robots.ts` and `sitemap.ts` — the whole rendering strategy is built around sharing and search; shipping without a sitemap undercuts it - [ ] Hungarian `not-found.tsx` and `error.tsx` — currently Next's English defaults - [ ] Real content for `/hogyan-mukodik`, `/dijszabas`, `/sikersztorik` - [ ] Browse filters actually filter (they are decorative today) - [ ] Campaign tabs: `Frissítések` and `Hozzászólások` render nothing yet - [ ] Accessibility pass: keyboard paths, focus order, contrast in both themes ## 12. Engineering hygiene - [ ] Test runner and real tests, starting with the fulfillment guard - [ ] Pin the Node version (`.nvmrc` and `engines`) - [ ] Prettier, so formatting does not drift with a second contributor - [ ] Hosted CI once a remote exists — `web/scripts/ci.sh` already works unchanged as the entry point --- ## Already done - Frontend: landing, browse, campaign page; Hungarian UI, three theme states - Go-rendered document with per-campaign `og:` tags, and share images drawn in Go; the data behind the tags is inlined so the first route needs no round-trip - Postgres 17 via pgx and plain SQL migrations; raised totals and backer counts derived from pledge rows rather than stored - Seed: 4 campaigns, 550 pledges, `.invalid` emails - Docker packaging: multi-stage build (Go binary + Vite bundle), 49 MB runner image - `compose.yaml` with a separate one-shot migrate service; the server refuses to migrate at boot so it can stay up and report a degraded database - Local CI (`scripts/ci.sh`): 28 checks including a container smoke test that migrates and seeds using the image's own binaries, plus a pre-push hook - `/api/health` readiness probe, wired into Dockerfile and compose healthchecks, with CI proving it fails when the database is unreachable - Admin surface: campaign list and editor (no creation — campaigns are made by hand in the database), tier editing with a deletion guard - Passwordless magic-link auth, admin-only, with hashed single-use tokens and CI proving a signed-in non-admin cannot reach it, a used link cannot be replayed, and a cross-origin write is refused - Brand: placeholder logo, colour and type system derived from the rendszerbontó identity