szabadpalya/TODO.md

208 lines
9.9 KiB
Markdown

# 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