invite/README.md
chris 2c78112a4f Initial commit: E-Invite, a self-hosted Evite-lite
Docker-ready Next.js + Prisma/Postgres e-invitation app: themed invite
pages, RSVP, comments, share links, email + SMS invites with
reply-to-RSVP-by-text, co-hosts, photo dropbox, and browser push
notifications.
2026-07-11 10:11:12 -04:00

188 lines
10 KiB
Markdown

# E-Invite
A stripped-down, self-hosted alternative to Evite: create themed online invitations, share a
single link with guests, collect RSVPs, and let guests leave comments — all Docker-ready.
## Features
- **Host accounts** — email + password signup/login, forgot/reset password via email.
- **Themed invites** — pick from several built-in themes (colors + fonts) per event.
- **Share links** — every event gets a unique, unguessable public URL. No guest accounts needed.
- **RSVP** — guests respond yes/no/maybe with a headcount and optional note; host and guest both
get a confirmation email.
- **Comments / well-wishes** — guests can leave a public message on the invite; hosts can hide or
delete any comment.
- **Email invites** — hosts can paste in a list of guest emails and send the invite link to all of
them at once.
- **Co-hosts** — invite another person by email to help manage an event (edit details, view RSVPs,
moderate comments); they accept via emailed link and sign up/log in. Only the original host can
delete the event or manage who else has access.
- **End times, food/drinks** — optional event end time (shown as a date range) and simple
food/drinks-available badges on the invite.
- **Photo dropbox** — guests can upload photos once the event starts; hosts can set an auto-close
time or close uploads manually at any time, and can hide/delete individual photos.
- **Email verification** — new accounts get a verification email; a dismissable-once-verified
banner nudges unverified hosts, but doesn't block using the app.
- **Notification preferences + browser push** — hosts can toggle which events (RSVP/comment/co-host
accepted) email them, and separately opt in to browser push notifications per-device from
Settings. See "Browser push notifications" below for the HTTPS caveat.
- **Text (SMS) invites + reply-to-RSVP** — the guest list accepts phone numbers alongside emails;
phone-based guests get a text with the invite link and can also just reply YES/NO/MAYBE (with an
optional headcount, e.g. "YES 3") directly to RSVP, no link click needed. Requires a Twilio
account — see "Text invites via Twilio" below. Fully optional; email-only hosts see no change.
### Robustness
- **RSVP dedup** — resubmitting an RSVP with the same email updates the existing response instead
of creating a duplicate row, so headcounts stay accurate.
- **Rate limiting** — in-memory limits on RSVP/comment submission, login, signup, forgot-password
and invite-sending guard against spam and brute-force/credential-stuffing attempts. Login/RSVP
limits combine IP (when a reverse proxy sets `X-Forwarded-For`) with a more specific key (email,
event) so a shared/unknown IP can't lock out every visitor.
- **Honeypot** — the public RSVP and comment forms include a hidden field real users never fill;
submissions that fill it are silently dropped as bots.
- **Resilient email** — RSVP/comment/event actions never fail because SMTP is down or
misconfigured; the guest-facing action still succeeds (data is already saved) and the failure is
logged server-side. The one exception is the host-facing "send invites" action, which reports
actual delivery failures back to the host.
- **Health check** — `GET /api/health` verifies DB connectivity; wired into both the Dockerfile
`HEALTHCHECK` and the `app` service's compose healthcheck.
- **Tests** — `npm test` runs the Vitest suite (validation schemas, rate limiter, email/phone
parsing, SMS reply parsing, Twilio signature verification, HTML-escaping helpers).
## Quick start (Docker)
```bash
cp .env.example .env # edit SESSION_SECRET, SMTP_* etc. for production use
docker compose up --build
```
This starts three containers:
| Service | Purpose | URL |
|------------|--------------------------------------------|---------------------------|
| `app` | The Next.js application | http://localhost:3100 |
| `postgres` | Database (migrations run automatically) | internal only |
| `mailpit` | Dev SMTP catcher — view all sent emails here | http://localhost:8025 |
On startup, the app container automatically runs `prisma migrate deploy` before starting the
server, so a fresh `docker compose up` is all that's needed. Uploaded photos are stored on disk
under `public/uploads`, backed by the `uploads-data` named volume so they survive rebuilds.
## Using a real SMTP provider
By default, `SMTP_HOST`/`SMTP_PORT` point at the bundled Mailpit container so email works out of
the box in development. To send real email in production, edit `.env` (or the `app` service
environment in `docker-compose.yml`) with your provider's SMTP credentials — no code changes
required:
```
SMTP_HOST=smtp.yourprovider.com
SMTP_PORT=587
SMTP_USER=your-smtp-username
SMTP_PASS=your-smtp-password
SMTP_FROM="Your Name <invites@yourdomain.com>"
SMTP_SECURE=false
```
Also update `APP_URL` to your real public URL so links in emails point to the right place, and set
`SESSION_SECRET` to a long random string.
## Text invites via Twilio
Adding a phone number to an event's guest list (alongside or instead of emails) sends a text
invite and lets that guest RSVP by replying YES, NO, or MAYBE — optionally with a headcount, e.g.
"YES 3" — directly to the text, no link click needed. This is entirely optional: leave the
`TWILIO_*` vars blank and the guest list just works with emails only, same as before.
To enable it:
1. Create a [Twilio](https://www.twilio.com) account and buy a phone number capable of sending/
receiving SMS.
2. Set these in `.env` (or the `app` service environment in `docker-compose.yml`):
```
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your-auth-token
TWILIO_PHONE_NUMBER=+15551234567
```
Both values are on the Twilio Console dashboard; the phone number is under
**Phone Numbers → Manage → Active Numbers**.
3. In the Twilio Console, open that phone number's configuration and set **"A message comes in"**
to a webhook: `POST https://your-real-domain.com/api/sms/webhook` (must be `APP_URL` from your
`.env` plus `/api/sms/webhook` — the webhook verifies Twilio's signature against that exact URL,
so a mismatch here causes every inbound reply to be silently rejected as unverified).
4. `docker compose up -d --force-recreate app` to pick up the new env vars (no rebuild needed —
these aren't `NEXT_PUBLIC_*`, so they're read at runtime, not baked into the build).
**Caveats worth knowing:**
- There's one shared Twilio number for the whole app instance (not one per event), so an inbound
reply is matched to whichever event most recently texted that phone number. If you invite the
same guest phone number to two events in quick succession before they reply to the first, their
reply could land on the wrong event. For most single-host, single-event use this never comes up;
if you're running many concurrent events through one number, prefer the email flow for guests
invited to more than one active event at a time.
- SMS costs money per message once you're past Twilio's trial credits — this app doesn't do any
cost tracking or budgeting, that's on the Twilio side.
- A misconfigured or suspended Twilio account behaves like SMTP being down: the guest still gets
added to the guest list, the host just sees "text(s) failed" in the send-invites result instead
of the whole action erroring out.
## Deploying behind HTTPS
Session cookies are **not** marked `Secure` by default, because the bundled docker-compose stack
serves plain HTTP and a `Secure` cookie would silently never be sent back by the browser, breaking
every login. Once you have a reverse proxy terminating TLS in front of the app, set
`COOKIE_SECURE=true` so session cookies get the `Secure` flag.
## Browser push notifications
Hosts can optionally enable browser push notifications per-device from Settings, as a companion
to (not a replacement for) email notifications. This requires:
1. VAPID keys set in `.env` (`NEXT_PUBLIC_VAPID_PUBLIC_KEY` + `VAPID_PRIVATE_KEY`) - `.env.example`
ships with a working default pair; generate your own with:
```bash
node -e "console.log(JSON.stringify(require('web-push').generateVAPIDKeys(),null,2))"
```
2. **HTTPS.** The browser Push API refuses to work over plain HTTP on anything other than
`localhost` - this is a browser platform restriction, not something this app can work around.
If you're accessing the app via `http://localhost:3100` directly, push works out of the box in
development. Once deployed behind a real domain, you need a reverse proxy terminating TLS in
front of the app (see "Deploying behind HTTPS" above) before the "Enable push notifications"
button will do anything.
The public key is inlined into the client JS bundle at **build** time (not read at container
startup), so changing it requires `docker compose up --build`, not just a restart. Leaving both
keys blank disables the feature entirely - the Settings page just says push isn't configured, and
email notifications are unaffected either way.
## Local development (without Docker)
Requires Node 20+ and a running Postgres instance.
```bash
npm install
cp .env.example .env # point DATABASE_URL at your local Postgres, SMTP_HOST at a local catcher
npx prisma migrate dev
npm run dev
```
Run the test suite with:
```bash
npm test
```
## Project structure
- `prisma/schema.prisma` — data model (User, Session, Event, Rsvp, GuestInvite, Comment)
- `src/lib/` — session/auth, mail (Nodemailer templates), sms (Twilio send/verify), theme presets,
validation (zod), rate limiting, email/phone list parsing, SMS reply parsing
- `src/app/actions/` — Server Actions for auth, events, RSVP, comments, invites
- `src/app/(auth)/` — signup/login/forgot-password/reset-password pages
- `src/app/dashboard/` — host dashboard, event editor, guest/RSVP/comment management
- `src/app/invite/[slug]/` — the public, themed invite page guests land on
- `src/app/api/health/` — health check endpoint used by Docker/compose
- `src/app/api/sms/webhook/` — inbound Twilio webhook for reply-to-RSVP-by-text
- `src/app/error.tsx`, `src/app/not-found.tsx` — custom error/404 pages