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.
188 lines
10 KiB
Markdown
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
|