# Wedding Invitation & RSVP — User Guide

How to run a wedding invitation from an empty guest list to a final headcount, step by
step. Written for the person actually doing it, not for a developer.

Everything below describes what the app does **today**. Where something is not built yet,
it says so plainly rather than pretending — see [What is not built yet](#what-is-not-built-yet)
at the end.

**Addresses** (development):

| | |
|---|---|
| Admin | `http://localhost/admin` |
| A guest's personal invitation | `http://localhost/i/{token}` |
| The public / printed-QR page | `http://localhost/e/{event}` |

In production these become `https://your-domain.com/...`. Everything else is identical.

---

## Onboarding a customer, start to finish

The whole path, once. Each step links to the part that explains it.

| # | Step | Where |
|---|---|---|
| 1 | **Create the event** — ceremony type, name, date, venue | Admin → Events → Create event |
| 2 | **Publish it** when the details are settled | the event's Settings tab |
| 3 | **Upload the card** the designer delivered, with its images | the event's Card design tab |
| 4 | **Preview** it in the phone frame, then **publish** it | same tab |
| 5 | **Write the invitation messages** (optional) | Events → Invitation messages |
| 6 | **Add guests** to that event, or import a spreadsheet into it | the event's Guests tab |
| 7 | **Send** the invitations | Admin → Messages |
| 8 | **Watch the numbers**, then export for the caterer | Admin → Dashboard |

No engineer is involved after the card file exists.

**What the designer delivers:** one `.html` file plus every image it references, handed over
together — not a PDF, not a Photoshop file. Ask them for the **share image** at the same time:
one 1200 × 630 picture, under 300 KB, which is what appears when the link is pasted into
WhatsApp. The rules are in [docs/CARD-AUTHORING.md](CARD-AUTHORING.md). The whole design must
come to **under 600 KB**, and the upload screen warns above 500 KB; the invitation page has an
800 KB budget in total and the card is the largest thing on it.

**Repeat steps 1–7 per ceremony.** Each event has its own card, its own guest list and its
own counts. A person attending two ceremonies is added to both and gets two links.

---

## The shape of the whole thing

There are two completely separate ways a guest gets in, and it helps to hold them apart
from the start:

**1. You invite them.** You open an event, add them to *its* guest list (or import a
spreadsheet into it), the app makes a private link just for them, you email it. They open it,
their name is already on the page, and their answer is tied to their row. This is the main
path.

Guests belong to **one event each**. Someone invited to both the Mehedi and the Walima is
added to both lists and exists as two separate records with two separate links — which is
also why a total spanning events is labelled "guest records" rather than "people".

**2. They find you.** You print a QR code, put it on a card or a stand at the venue, and
whoever scans it registers themselves. Nobody knew they were coming until they said so.
This is the walk-in path.

```
   YOU INVITE THEM                          THEY FIND YOU

   open event, add guest / import          print the QR code
          |                                        |
   app makes a private link                 guest scans it
          |                                        |
   you email it  ──────────┐          ┌──── /e/{event} public page
          |                │          │            |
   guest opens /i/{token}  │          │     they fill in name + phone
          |                │          │            |
   they accept or decline  │          │     app creates their guest row
          |                │          │            |
          └────────────────┴──────────┴──────▶  headcount
                                   |
                        automatic reminders: 15, 7, 2 days before
```

Both paths end in the same place: a row you can see, count, and export for the caterer.

---

## Before you start: signing in

1. Go to `http://localhost/admin`.
2. Sign in with the Google account that is on the allowlist.

**There is no sign-up.** The app never creates an account for anyone. If an address is not
already an active row in the admin table, it is turned away no matter which Google account
is used — and the message is deliberately vague, so nobody can use the sign-in page to
find out who the administrators are. The first Super Admin is created by `make seed` from
`SEED_SUPER_ADMIN_EMAIL`.

While `AUTH_DEV_BYPASS=true` in development, a second box appears underneath for signing
in by email alone. The API refuses it unless it is also in development mode with the bypass
on, so this box alone cannot let anyone into a real deployment.

**Roles.** What you see is shaped by your role, but the menu hiding a button is *not* the
security. Every action is checked again by the API on its own, so a hidden button is a
convenience, not a lock.

---

## Part 1 — Create the event and its card

An event is a record you create: a ceremony type, a name, a date and a venue. Everything
else in the system hangs off it — its own guest list, its own invitation card, its own
counts. You can have as many as you like, including two of the same type.

### Creating one

1. Admin → **Events** → **Create event**.
2. Pick the ceremony (Mehedi, Marriage, Walima), give it a **name** in English and Bangla,
   and fill in the date, venue and address.
3. **Create event.**

It is created as a **draft**. Guests get "not found" until you publish it, so a half-filled
event is never reachable. Publish it from the event's **Settings** tab when you are ready.

The web address comes from the name — "Walima — Dhaka" becomes `/e/walima-dhaka` — and it is
allocated once and never changes, because it ends up printed on QR cards. Renaming the event
later changes what guests read, not where the link goes.

### The invitation card

The card is the artwork on the invitation. It is designed for you as a service and delivered
as a file, so there is nothing to lay out here — you upload it and publish it.

1. Open the event → **Card design** tab.
2. Choose the card file (`.html`) and, in the same step, **every image it uses**.
3. **Upload as draft.** Nothing a guest sees has changed yet.
4. Look at the phone preview beside the version list. That is the card exactly as a guest
   gets it — same fonts, same animation.
5. **Publish** when it is right.

Uploading and publishing are separate on purpose: a designer sending three attempts in an
afternoon must not be pushing each one live.

**Rolling back** is publishing an older version — the versions are all listed and never
reused, so "the one from Tuesday" stays identifiable. **Unpublish** takes the card off
entirely; the invitation falls back to a plain, still-complete page.

> **The card has the date and venue printed on it.** It is fixed artwork, so changing either
> in Settings does *not* update the card — the screen warns you when a card is live. You need
> a new version from the designer.

If your designer needs the rules, hand them [docs/CARD-AUTHORING.md](CARD-AUTHORING.md).

### How the link looks when it is shared

Paste an invitation link into WhatsApp and the chat shows a picture with the event's name,
date and venue under it, instead of a bare web address. The same card appears in the
invitation email, above your message.

That picture comes from the **share image** your designer uploads with the card. It is a
separate file from the card artwork — landscape rather than portrait, because that is the
shape chat applications use. Without one, the link still previews with the name, date and
venue, just no picture.

The preview is **the same for every guest.** It never shows a guest's name, so a guest who
forwards their link into a family group does not show that group who it was addressed to.
Their name does appear in their own email, which only they receive.

> **Changing the card does not refresh previews already shared.** WhatsApp and Facebook keep
> a copy of what they fetched for about a week, so a link someone pasted yesterday keeps its
> old picture even after you publish a new card. New shares get the new one immediately. If
> you need to force it, paste the link into Facebook's Sharing Debugger
> (`developers.facebook.com/tools/debug/`) and press **Scrape Again** — that is the platform's
> own tool, and there is nothing on our side that can clear their copy for them.

### What is on the invitation

| Section | Where it comes from |
|---|---|
| **Dear \<their name\>** | the guest's own row — only on a personal link |
| The invitation sentence under it | your message settings, by guest type and language |
| **The card** | the design published on this event |
| **Accept**, and the form behind it | the guest's current status |
| Countdown, date, time, venue, address | event record, below the Accept button |
| Dress code, notes | event record, hidden when blank |
| **Get Directions** button | event map link, hidden when blank |
| Contact number in the footer | the wedding's host contact |

The guest's name appears in the header and **nowhere else** — the card itself is identical
for everyone, which is what lets one file serve the whole guest list.

### The invitation messages

Under the guest's name is one short sentence, and you choose it. Admin → **Events** →
**Invitation messages**: two per language, one for guests invited on their own and one for
guests invited with their family.

Each is capped at 100 characters, with a live counter. The limit is not arbitrary — the
sentence sits directly above the artwork, and a paragraph there crowds the design it is
meant to introduce. Leave a field blank and the wording shown in grey is used.

### Editing an event

Open the event → **Settings**. You can change the date, venue, capacity and published state.

**Published matters.** An unpublished event returns "not found" to every guest, including
ones holding a valid link. Use it to keep an event dark until you are ready.

> **Changing the date is not a normal edit.** Read
> [Part 7 — If the date changes](#part-7--if-the-date-changes) before you touch it. The
> screen warns you before saving.

### Deleting an event

At the bottom of Settings. The confirmation tells you exactly how many guest records and
responses go with it, because they do — a guest belongs to one event, so deleting the event
deletes its list. Their links stop working. It cannot be undone.

### What the animation does

When a guest opens their link, a closed envelope appears, the flap opens, and the card rises
out. It takes under two seconds.

- There is a **Skip** button, and tapping anywhere also skips it.
- It plays **once per event per browser tab**. Reloading to check the venue does not make
  them sit through it again.
- If the guest's phone is set to reduce motion, it does not play at all — not a faster
  version of it, none of it.
- If their connection is slow or JavaScript is off, they simply get the invitation with no
  envelope. **The text is never hidden behind the animation.**

### Cover photo and music

Each event has a slot for a cover photo (used when no card is published) and a background
music track. When a track is set, a round button appears in the corner of the invitation.

**Music never plays on its own.** Every phone browser blocks audio that starts without the
guest tapping something, so a track set to autoplay would just be silently refused.

There is **no upload screen for these yet** — see [What is not built yet](#what-is-not-built-yet).

---

## Part 2 — Get guests into the system

Three ways. Use whichever fits how you have the names.

### 2a. One at a time

1. Admin → **Events** → open the event → **Guests** tab → **Add guest**.
2. Fill in:
   - **Full name**
   - **Phone** — any Bangladeshi format works (`01712345678`, `+8801712345678`,
     `017 1234 5678`). It is stored in one standard form so the same person typed two ways
     is still recognised as one person.
   - **Email** — **without this they receive nothing.** Version 1 sends email only. A guest
     with no email address can still be counted and can still respond if you hand them the
     link another way, but the app cannot reach them.
   - **Side** — bride / groom / common. Used for filtering later.
   - **Language** — English or বাংলা. Decides which language their emails are written in.
   - **Seats** — the most people they may bring, themselves included. The RSVP form will
     not let them exceed it.
   - **Tags** — comma separated, e.g. `family, office, uni`. This is what lets you email
     just the office later.
   - **Invitation type** — Single or Family. This chooses which sentence appears under their
     name: "You are cordially invited" or "You and your family are cordially invited". It is
     wording only; **Seats** is still what limits the party size, and the form warns you if
     you pair Family with a single seat rather than quietly fixing one or the other.
3. **Add guest**. You land on their panel, where **Send via messaging** will send them their
   invitation straight away — see Part 3a.

There is no "invited to" list, because the event is the list you are standing on. To invite
the same person to a second ceremony, open that event and add them there — they become a
second, independent record with its own link. Their phone number appearing on both lists is
expected, not a duplicate.

You can also add, import and search across every list from Admin → **Guests**; choose an event
in the filter first, since a guest has to join one.

### 2b. Import a spreadsheet

1. Admin → **Guests**.
2. **Choose the event in the filter.** Every row joins that event's list, and the Import
   button stays greyed out until you have picked one — a guest has to belong to an event.
3. **Import CSV**, then upload the file. The panel that opens names the event it will write
   to, so you can see the destination before you choose the file.
4. The app shows the **first 10 rows** as it understands them, so you can catch a
   mismatched column before it becomes 400 wrong rows.
5. It then shows a **validation report** — which rows are fine, which are not, and why.
6. Import. The guest list behind the panel updates as soon as it finishes, so you can close
   it and see the new rows straight away.

Columns:

```
full_name, email, phone, whatsapp, side, group_tags, invitation_type, max_guests, locale, preferred_channel
```

- `full_name` is required. Every row needs **either** a usable phone **or** an email —
  a row with neither is rejected, because there would be no way to reach or identify them.
- `invitation_type` is `single` or `family`, and defaults to `single` when absent.
- `group_tags` is comma-separated.
- **An `events` column is ignored.** The destination is the event chosen in the filter on the
  Guests screen, so a spreadsheet cannot quietly put guests on a list you were not looking at.
  An old file with the column still in it imports fine.
- Common header spellings are matched automatically, so a sheet with `Name` and `Mobile`
  does not need editing by hand first.
- **Duplicates are merged, not doubled — within this event.** A row whose phone or email
  matches a guest already on *this* list updates them instead of creating a second row. The
  same phone on another event's list is a different person's record there and is left alone,
  so importing the same sheet into the Mehedi and the Walima gives you two full lists rather
  than one list and a pile of merges.

### 2c. Let them add themselves (printed QR)

Covered in [Part 4](#part-4--the-printed-qr-code-walk-ins).

---

## Part 3 — Send the invitation

There are three ways, and they are for different moments. **One guest** and **a batch you
pick** are both the guest list; **everyone at once** is the Messages screen. Start here if
you are inviting one person.

### 3a. Inviting one guest

1. Admin → **Guests**, find them, press **Send invitation** on their row.
2. Their panel opens with three separate sections:
   - **Send via copying the link** — their private link and a Copy button, for pasting into
     WhatsApp or a message yourself. Copying is recorded in the audit log — it is the one
     way a guest's link leaves the system without an email to show for it — but it sends
     nothing and does not mark them as invited.
   - **Send via messaging** — the whole email, already written, as editable text:
     a subject, **Dear \<their name\>**, the invitation sentence you set for this event
     (the single or the family one, whichever fits them) with their link under it, and
     **Regards \<your names\>**. Above it you see the picture card the guest will get,
     exactly as it will look to them.
   - **Record an RSVP** — for an answer that came by phone.
3. Change any of it. What you see is exactly what they get.
4. Press **Send via email**.

Some things it will not let you do, on purpose:

- **A guest who unsubscribed, whose address bounced, or who has no address** cannot be
  emailed. The button is disabled and says which of the three it is. There is no override —
  an unsubscribe is a promise, not a preference.
- **Late at night** (after 22:00, before 08:00 in Dhaka) it asks first, telling you the local
  time, and sends only if you say yes. Automatic reminders never do this; they just wait for
  morning.

You can send again as many times as you need — "they never got it" is the usual reason, and
the panel tells you when they were last emailed so you can see if one already went.

Every part of the message is yours to edit **except** the unsubscribe line, which is added
after you finish. It names this event, because unsubscribing stops emails about this event
and no other.

### 3b. Inviting a batch you picked

This is the week the cards go out: forty addresses just confirmed, and you want to see both
messages before they go.

1. Admin → **Guests**, and **choose an event** in the filter. The message text belongs to an
   event, so nothing here works until you have picked one.
2. Tick the guests you want. The checkbox in the header takes everyone on the page, and if
   your filters match more than one page you get a **Select all N matching** button. The
   count is shown next to the filters and on the button.
3. Press **Send invitations**. It stays greyed out until you have both an event and at least
   one guest, and tells you which one is missing.
4. The window shows **two messages side by side** — the one for guests invited alone, and the
   one for guests invited with their family. Each says how many people it will go to. If you
   selected nobody of one kind, that side is still shown, greyed, saying so.
5. Edit anything. Both are the real messages, not previews of something else.
6. Press **Send**. Single guests get the single message, family guests get the family one.

Two pieces of text stand in for something different per person:

- `{guest_name}` becomes their name.
- `{invitation_link}` becomes **their own** private link.

Leave `{invitation_link}` in the message — without it the guest has no way to answer, and the
send is refused rather than going out broken. If you mistype one (`{guset_name}`), it is
refused too, with the mistake named and the message it is in highlighted. Nothing is sent
until every message that has recipients is right, so a typo never reaches half the list.

Before you press Send it tells you how many of your selection **will not** be emailed and
why — unsubscribed, bounced, or no address. Those are never sent to, and there is no
override.

Afterwards the window stays open and counts up: recorded, sent, waiting, not sent. Anyone it
could not reach is listed by name with the reason. You can close it — the sending carries on
without you, and the Messages screen has the same record.

If your guests have different languages set, a **Language** switch appears above the two
messages so you can write each one. Everyone is sent the version in their own language.

Sending again is a new batch and really does send again. Pressing the button twice by
accident is not — the same batch cannot be recorded twice.

### 3c. Inviting everyone at once

1. Admin → **Messages**.
2. Choose:
   - **Event** — which one this email is about.
   - **Audience** — Everyone invited / Accepted only / Not yet responded.
   - **Template** — Invitation, Reminder 15/7/2 days, Thank you.
   - **Tag** (optional) — narrows it further, e.g. only `office`.
3. Press **Preview**. **You cannot send without previewing first.** This is the one screen
   that can email hundreds of people at once, so it is deliberately two steps.
4. The preview tells you three things:
   - **how many people this will reach**
   - **how many have no email address** and will therefore be skipped
   - **a real sample email**, with a real guest's name, date, venue and link filled in — not
     a mock-up
5. If the numbers look right, **Send**.

### What actually goes out

Each guest gets their **own** email with their **own** private link. Nobody can see anyone
else's address, and no two guests get the same link.

The templates fill themselves in from these:

`guest_name` · `event_title` · `event_date` · `event_time` · `venue` · `days_left` ·
`invite_url` · `cancel_url` · `map_url` · `couple_names`

If a template ever references something that does not exist, the preview shows the raw
`{{placeholder}}` rather than quietly producing an empty sentence — so you see the problem
before your guests do.

### In development, nothing is actually sent

Development runs with `DRY_RUN=true`. Emails are fully rendered and written to the log, and
the message log records them as sent, but **nothing leaves the machine.** This is how you
rehearse a send without emailing your relatives by accident. See what was "sent" with:

```bash
make logs S=worker
```

### Watching what happened

Below the send controls is the **message log**: every message, its status, the provider's
id for it, and the exact error if it failed. Failures can be retried in bulk from here.

---

## Part 4 — The printed QR code (walk-ins)

For guests you cannot email — the ones invited by phone, or by a card in someone's hand.

1. Admin → **QR**.
2. Each event shows its real QR code — the actual artwork, not a preview of it.
3. Download **SVG** for anything being printed (stays sharp at any size a printer is given)
   or **PNG** for anything that will not take vector artwork.
4. Print it on the card, or on a stand at the door.

The codes are generated at the highest error-correction level, so one survives a
fingerprint, a fold, or a badly lit hall.

**What the guest does:** scans it → lands on the public event page → sees the invitation →
taps Accept → ticks which events they are coming to → gives name, phone, optional email and
party size → done. The app creates their guest row and their invitations on the spot.

If their phone or email matches somebody already on the list, they are **merged into that
existing guest** rather than added twice.

**Individual guest QR codes** are not offered in the admin. A guest's panel is for reaching
that one person now — copy their link, or email it — and printing is a batch job that belongs
to this screen. The per-guest print sheet is not built yet.

**Protection on the public page.** Because anyone on the internet can reach it, this page is
the only one that creates records without an invitation, so it is protected two ways: a
Cloudflare check that the submission came from a real browser, and a limit of 5 submissions
per 10 minutes from one connection. Both are enforced by the server, not by the page.

---

## Part 5 — What the guest sees and does

### They open their link

The envelope opens, and they see the invitation with their name on it: **Dear Rahim
Uddin**. The moment they open it, you can see that they opened it.

### They accept

Tapping **Accept Invitation** reveals the form. Their name, phone and email are already
filled in from what you entered — they only correct what is wrong.

They choose how many are coming (capped at the seats you gave them), and can leave a
message for the couple and any dietary notes.

### They get a confirmation

A success screen with the date, venue, how many they confirmed, and **Add to Calendar** —
both a downloadable calendar file and a Google Calendar link. If they gave an email address,
a confirmation email is queued immediately.

### They decline

A short "we'll miss you" screen. Their answer is recorded, and they are excluded from the
reminders that follow.

### They change their mind

Both directions work.

- **Declined, now coming?** The page offers **Changed your mind?** and lets them accept.
- **Accepted, now cannot come?** Every invitation email carries a cancel link. It asks them
  to confirm, then frees their seats and takes them out of the headcount.

Cancelling is deliberately awkward to do by accident: it requires an actual confirmed
button press on the page. A link scanner or a WhatsApp preview bot following the link
**cannot** cancel anyone's RSVP.

### Their short code

Every invitation also has a short code like `RZNVM7`. When someone phones the couple
instead of clicking anything — which is most of the older relatives — that code identifies
their invitation over the phone. It is shown in the guest's detail panel in the admin.

### If they are late

Once the RSVP deadline passes, or once the event has started, the page says so and points
them at the host's phone number instead of taking an answer that is no longer useful.

---

## Part 6 — Reminders, which run themselves

You do not send reminders. The app does, on its own, at **15 days**, **7 days** and
**2 days** before each event.

1. Admin → **Reminders**.
2. Per event and per wave you can turn a wave on or off and set its send time.
3. The preview shows, for each wave, **the exact date it will go out**, **how many people
   it will reach**, and **how many have no email**.

The rules it follows:

- Times are Bangladesh time. 10:00 means 10:00 in Dhaka regardless of where the server is.
- **A wave whose moment has passed is never sent late.** A "2 days to go" email arriving the
  morning after is worse than no email, so it is skipped and recorded as skipped.
- Nothing goes out between **22:00 and 08:00**. Anything due in that window waits until 08:00.
- Who gets a reminder depends on the wave's audience — you can chase only the people who
  have not answered, and leave the ones who already accepted alone.
- Cancelled and declined guests are excluded automatically.
- If the machine is off for a day, the next run catches up by itself. Nobody gets a
  duplicate, because each guest can only ever have one job per wave.

---

## Part 7 — If the date changes

The single most dangerous edit in the app, and the one it handles most carefully.

By the time a date moves, hundreds of reminder emails are already queued and pointing at
the old date.

1. Admin → **Events** → change **Starts at**. A warning appears before you save.
2. **Save.**
3. The app then tells you exactly what it did:

   > Moved 3 reminder(s), dropped 0 now-past wave(s), created 0 new

   - **Moved** — queued reminders re-pointed at the new date.
   - **Dropped** — waves the new date puts in the past. Skipped, not sent late.
   - **Created** — waves that did not exist before the move.

4. If guests have already been told the old date, the screen offers a **"date changed"
   announcement** to everyone affected. It is offered, never sent automatically — telling
   people is your decision.

Reminders that have **already gone out** are left alone. They cannot be unsent, and
rewriting them would destroy the record that they happened.

---

## Part 8 — Watching the numbers and feeding the caterer

### Dashboard

Admin → **Overview**. One card per event: accepted / pending / declined, headcount,
response rate, and days remaining. Plus a combined row across all three. It refreshes
itself every 30 seconds.

### Finding people

Admin → **Guests**. Search by name, phone or email, and filter by event, status, side, tag
or how they were invited. Fifty per page.

Open any guest for their full history: every invitation, when they opened it, when they
answered, what they said, their message to the couple, and their short code.

### Answering for someone

When a relative phones instead of clicking, open them in the Guests list and use **Set
RSVP** on the right invitation. It is recorded as **you** having answered on their behalf,
not as though they did it themselves — so the record stays honest.

### Export

Admin → **Guests** → **Export CSV**. It exports **what you are currently filtering**, so
narrow the list first if you only want part of it.

There is also a **caterer preset** — name, party size, dietary notes, nothing else. It is
the list you hand to the kitchen without handing over everyone's phone number.

Every export is recorded in the audit log, including who did it.

---

## Reference

### Guest statuses

| Status | Meaning |
|---|---|
| `pending` | Invited, has not opened it yet |
| `opened` | Opened the link, has not answered |
| `accepted` | Coming, with a party size |
| `declined` | Not coming |
| `cancelled` | Had accepted, then cancelled |
| `expired` | Deadline passed with no answer |

### Import columns

```
full_name, email, phone, whatsapp, side, group_tags, invitation_type, max_guests, locale, preferred_channel
```

An `events` column is ignored — the destination is the event chosen in the Guests filter.

### Template placeholders

```
guest_name   event_title   event_date   event_time   venue
days_left    invite_url    cancel_url   map_url      couple_names
unsubscribe_url
```

Every email carries an unsubscribe link whether or not you place `unsubscribe_url` yourself,
and its wording names the event it covers — opting out of the Walima does not stop Mehedi
email, because those are two separate guest records.

### Everyday commands

```bash
make dev
```

```bash
make seed
```

```bash
make logs S=worker
```

```bash
make backup
```

---

## What is not built yet

Being straight about the gaps, so you do not go looking for a screen that does not exist:

- **No editor for the invitation's words, photo or music.** Cover photo, music track and
  the visual theme are stored per event but have no upload screen; today they are set
  directly in the database. Titles, address, map link, dress code, notes and RSVP deadline
  can be changed through the API but are not on the Events screen either — it exposes date,
  venue, capacity and published only.
- **One animation, not three.** The envelope reveal is shared by all three events. The
  planned Mehedi henna-path and Walima curtain themes are not built.
- **No language toggle on the invitation page.** Each guest's page uses the language stored
  on their record. They cannot switch it themselves yet.
- **No free-text broadcast to an audience you did not pick by hand.** A message can be
  hand-written for one guest (Part 3a) or for a batch you ticked (Part 3b); the
  everyone-at-once screen (Part 3c) still sends from the seeded templates, as does the
  date-change announcement.
- **No per-guest QR sheet.** Per-event QR artwork is on the QR screen; there is no printable
  sheet of one QR per guest.
- **No email provider configured.** Sending is built and the button works, but until a
  provider key is set every send is a rehearsal — see "In development, nothing is actually
  sent" above.
- **No template editor.** The five email templates are seeded in both languages and edited
  in the database.
- **Email only.** No WhatsApp, no SMS. A guest with no email address cannot be reached by
  the app at all — reach them yourself and record their answer with **Set RSVP**.

---

## Before the real wedding

1. Point the app at a real domain, and set up email authentication (SPF, DKIM, DMARC) on
   the sending domain. Since everything goes by email, invitations landing in spam is the
   single biggest risk to the whole thing.
2. Register the production address on the Google sign-in client.
3. Get a Cloudflare Turnstile key pair for the domain.
4. Turn **off** dry-run mode, and send every template to your own inbox in both languages
   first.
5. Do a full rehearsal with shifted dates so you watch a reminder wave actually fire.
6. Scan a printed QR code at its final printed size, indoors, under the light the venue
   actually has.
7. Send to about 20 friendly guests before sending to everyone.
