# Wedding Invitation & RSVP Automation

Guests receive a personal link, open an animated invitation, tap Accept, and fill a short
form. The host watches live headcounts per event and exports a caterer list. Automated
reminders go out 15, 7 and 2 days before each event, each carrying a one-tap cancel link
that updates the headcount immediately.

Three events are tracked independently: **Mehedi**, **Marriage Ceremony**, and **Walima**.
A guest may be invited to one, two, or all three.

Planning lives in [`openspec/`](openspec/changes/add-rsvp-v1/) — the proposal, ten
capability specs, the design decisions, and the task list. Read `design.md` before making
architectural changes; it records why things are the way they are.

## Architecture

| Layer | Choice |
|---|---|
| Frontend | Next.js 15 (App Router), TypeScript, Tailwind — rendering only |
| Backend | FastAPI + Pydantic v2 — all business logic, data access, and authorization |
| Database | PostgreSQL 16 + SQLAlchemy 2.0 async + Alembic |
| Queue | PostgreSQL (`message_job` table, claimed with `FOR UPDATE SKIP LOCKED`) — no Redis |
| Worker | APScheduler: hourly reminder planner + 30s send poll |
| Admin auth | Google OAuth only, against an email allowlist — no passwords |
| Messaging | Email only in v1; WhatsApp and SMS are v2 behind the same interface |
| Proxy | Caddy routes `/api/*` to FastAPI and everything else to Next.js — one origin, no CORS |

The browser never reaches the database. Every authorization decision is made by FastAPI on
its own authority, not inherited from the frontend.

## Prerequisites

- **Docker Desktop** (with Compose v2)
- **make** — not installed on Windows by default:
  ```bash
  scoop install make
  ```
  or `choco install make`. Run the targets from Git Bash.

Node and Python are only needed if you want to run tooling outside containers.

## Getting started

```bash
cp .env.example .env.dev
```

Fill in `SEED_SUPER_ADMIN_EMAIL` with your Google account — without it nobody can sign in
to the admin area. Then:

```bash
make dev
```

```bash
make migrate
```

```bash
make seed
```

The app is at `http://localhost`, API docs at `http://localhost/api/docs`.

For how to actually run a wedding with it — adding guests, sending invitations, what the
guest sees, reminders — see the [User Guide](docs/USER-GUIDE.md).

## Common tasks

Run `make help` for the full list.

| Command | What it does |
|---|---|
| `make dev` | Start the dev stack (hot reload, `DRY_RUN=true`) |
| `make prod` | Build, migrate and start production — the whole deployment, in one command |
| `make migrate` | Apply migrations |
| `make migration name="..."` | Autogenerate a migration |
| `make seed` | Load demo data (refuses to run against prod) |
| `make test` | API test suite |
| `make lint` | ruff, mypy, and TypeScript checks |
| `make client` | Regenerate the TypeScript API client from OpenAPI |
| `make logs S=api` | Tail one service |
| `make backup` | Timestamped database dump into `backups/` |

Most targets accept `ENV=prod` to act on production instead of dev, for example
`make migrate ENV=prod`.

## Two environments

Compose overlays are always named explicitly; `docker-compose.override.yml` is deliberately
unused so a production command can never silently inherit development settings.

| | dev | prod |
|---|---|---|
| Email | `DRY_RUN=true` — rendered and logged, never sent | live provider |
| Source | mounted, hot reload (`WATCHPACK_POLLING=true` — Windows bind mounts deliver no inotify events, so without it `next dev` never sees an edit) | baked into the image |
| Postgres port | published on 5432 | not published |
| TLS | plain HTTP on localhost | Caddy with the real domain |
| Restart policy | none | `unless-stopped` |

### Public values the frontend renders with

The Turnstile site key and the Google OAuth client id are public, but they are **not** named
`NEXT_PUBLIC_*`. Next.js replaces that prefix with a literal at build time, and the
production web image is built by `docker build` with none of them set — so a `NEXT_PUBLIC_`
read compiles to `undefined` and the container's environment is ignored entirely. It works
in `next dev` and breaks only in production, which is the worst possible place to find out.

They are read at request time by [`web/lib/public-config.ts`](web/lib/public-config.ts) and
passed to client components as props. An ESLint rule fails the build on the prefixed form.

## Deploying

`make prod` is the entire deployment, and it is the same command for the first deploy and
for every update afterwards. It is safe to re-run.

```bash
make prod
```

It builds the images, brings the database up **alone**, applies migrations against it while
nothing is serving, and only then starts the API, worker, web and Caddy — waiting for each
to report healthy before it returns. Migrating after the stack is already up would leave the
API answering requests against a schema it does not match for as long as the upgrade takes.

The one prerequisite is `.env.prod`: copy `.env.example`, fill in every value, and keep it
off the machine's backups. The stack refuses to start without it, and
[`assert_production_ready()`](api/app/config.py) refuses to start on any value still left at
its development placeholder — so a half-filled file fails loudly at boot rather than signing
real session cookies with a secret published in this repository.

On a Linux host, `media/` must belong to uid 1001, the unprivileged user the API image runs
as. `make prod` does this for you; if it reports that it could not, run
`sudo chown -R 1001:1001 media` once. Card uploads fail with a permission error otherwise —
at the moment an admin publishes a card, not at boot.

Two things are stateful: Postgres and `media/`. `make backup` writes both with a matching
timestamp, and a database-only backup restores card records pointing at artwork that no
longer exists.

## Before launch

External setup items that are launch-blocking and have lead times, so start them early.
**None of them are needed for development** — dev runs on published test keys, and `make
prod` refuses to start on any of them, so a placeholder cannot reach production by accident.

1. **Domain + email deliverability.** SPF, DKIM and DMARC on the sending domain. Since v1
   is email-only, deliverability is the single biggest delivery risk — invitations that
   land in spam are invitations nobody sees.
2. **Google sign-in client.** Sign-in uses the Google Identity Services **ID-token** flow —
   the browser obtains a token and posts it to `/api/auth/google`, which verifies it against
   Google's public keys. There is no callback route, so register **Authorized JavaScript
   origins**, not redirect URIs: `http://localhost` for development (include the port if you
   move Caddy off 80 — `http://localhost` and `http://localhost:3005` are different origins
   to Google) and `https://your-domain.com` for production. The client **secret** is not
   used by this flow and nothing reads it.

   Sign-in also requires your address to be an active row in `admin_user`; there is no
   auto-provisioning. `make seed` creates it from `SEED_SUPER_ADMIN_EMAIL`.
3. **Cloudflare Turnstile.** Free, needs the domain, takes about two minutes. Put the pair
   in `.env.prod` as `TURNSTILE_SITE_KEY` and `TURNSTILE_SECRET_KEY`. Dev uses Cloudflare's
   documented always-pass test keys (`1x0000…`), which need no account and skip the
   verification call entirely — the production guard rejects them, so this is the one item
   you will notice immediately if you skip it.
4. **Event dates and venues.** Needed before reminders can be enabled, since every wave is
   computed backwards from `starts_at`.

## Contributing

Work is tracked as OpenSpec tasks. To pick up the next piece:

```bash
openspec status --change add-rsvp-v1
```
