9.9 KiB
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 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_dumpviadocker run --rm postgres:17-alpineso the dump tool always matches the server version and the host needs nopostgres-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 viarclone. 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 downmust 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_dumpcovers 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 —
inventoryLimitis 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.
- Magic-link sign-in, sessions in Postgres, no passwords anywhere
- Admin rights via the
isAdminflag or theADMIN_EMAILSallowlist - A signed-in non-admin is refused by every
/api/admin/*route. Note that the/admindocument 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.
- Campaign edit form (
/admin); creation deliberately not offered - 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.examplecurrently 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 composeprocedure - 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-*.pngalready exist and are simply not wired in. The scaffold default is still in place robots.tsandsitemap.ts— the whole rendering strategy is built around sharing and search; shipping without a sitemap undercuts it- Hungarian
not-found.tsxanderror.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ésekandHozzászólásokrender 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 (
.nvmrcandengines) - Prettier, so formatting does not drift with a second contributor
- Hosted CI once a remote exists —
web/scripts/ci.shalready 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,
.invalidemails - Docker packaging: multi-stage build (Go binary + Vite bundle), 49 MB runner image
compose.yamlwith 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/healthreadiness 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