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.
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/healthverifies DB connectivity; wired into both the DockerfileHEALTHCHECKand theappservice's compose healthcheck. - Tests —
npm testruns the Vitest suite (validation schemas, rate limiter, email/phone parsing, SMS reply parsing, Twilio signature verification, HTML-escaping helpers).
Quick start (Docker)
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:
- Create a Twilio account and buy a phone number capable of sending/ receiving SMS.
- Set these in
.env(or theappservice environment indocker-compose.yml):
Both values are on the Twilio Console dashboard; the phone number is under Phone Numbers → Manage → Active Numbers.TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your-auth-token TWILIO_PHONE_NUMBER=+15551234567 - 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 beAPP_URLfrom your.envplus/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). docker compose up -d --force-recreate appto pick up the new env vars (no rebuild needed — these aren'tNEXT_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:
- VAPID keys set in
.env(NEXT_PUBLIC_VAPID_PUBLIC_KEY+VAPID_PRIVATE_KEY) -.env.exampleships with a working default pair; generate your own with:node -e "console.log(JSON.stringify(require('web-push').generateVAPIDKeys(),null,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 viahttp://localhost:3100directly, 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.
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:
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 parsingsrc/app/actions/— Server Actions for auth, events, RSVP, comments, invitessrc/app/(auth)/— signup/login/forgot-password/reset-password pagessrc/app/dashboard/— host dashboard, event editor, guest/RSVP/comment managementsrc/app/invite/[slug]/— the public, themed invite page guests land onsrc/app/api/health/— health check endpoint used by Docker/composesrc/app/api/sms/webhook/— inbound Twilio webhook for reply-to-RSVP-by-textsrc/app/error.tsx,src/app/not-found.tsx— custom error/404 pages