# 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 " 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