chris d9f980b640 Tone down the Retro Arcade theme
- Swap Press Start 2P (a pixel-block font meant for short logo-sized
  text) for VT323, a retro terminal font that stays legible across
  full paragraphs of body copy
- Loosen the pixel-grid background from a dense 40px wall-to-wall
  neon grid to a sparser 120px tile with faint gridlines and a few
  low-opacity flecks, in line with the other themes' patterns
2026-07-12 12:20:59 -04:00
2026-07-12 12:20:59 -04:00

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 checkGET /api/health verifies DB connectivity; wired into both the Dockerfile HEALTHCHECK and the app service's compose healthcheck.
  • Testsnpm test runs 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:

  1. Create a Twilio 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:
    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.

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 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
Description
No description provided
Readme 755 KiB
Languages
TypeScript 98.5%
Dockerfile 0.5%
JavaScript 0.5%
CSS 0.5%