208 lines
9.9 KiB
Markdown
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
|