Event invitation and RSVP management system built for self-hosting.
- Create events with details (date, location, description, images)
- Manage guest lists
- Send email invitations with unique RSVP links
- Track responses (yes/no/pending)
- Optional push notifications (via ntfy.sh)
- Token-based RSVPs — No guest logins, just click the unique link
- Party size tracking — Guests specify party size
- Email handling — Primary + CC addresses per attendee
- Batch imports — Parse from email headers, upload CSV/TSV, copy from other events
- Admin dashboard — Create/modify events and attendees
- ICS calendar downloads — One-click "Add to calendar" for guests
- Timezone support — Event times display correctly for all recipients
- Mild customization — Banner images, location links, rich descriptions
- Mobile-friendly and desktop-friendly UI — Responsive design built with PicoCSS
- Zero-config database — SQLite with automatic runtime migrations
Odette is designed to be run on your own infrastructure. Admin routes (/admin/*) have no built-in authentication — they're meant to be protected by an authenticating reverse proxy (nginx, Caddy, oauth2-proxy, etc.).
Multi-tenancy / event admin logins are not currently implemented (see Roadmap).
npm install
npm test
npm run dev
# Server runs at http://localhost:3000
# Auto-reloads on changesAdmin dashboard: http://localhost:3000/admin
npm ci
npm test
npm run build
npm startUsing pre-built image:
docker run -p 3000:3000 \
-v /path/to/data:/data \
-e SMTP_USER=your-email@gmail.com \
-e SMTP_PASS=your-app-password \
-e SESSION_SECRET=random-secret-here \
ghcr.io/cdanis/odette:latestBuilding locally:
docker build -t odette .
docker run -p 3000:3000 \
-v /path/to/data:/data \
-e SMTP_USER=your-email@gmail.com \
-e SMTP_PASS=your-app-password \
-e SESSION_SECRET=random-secret-here \
odetteDatabase and uploaded files persist in the /data volume.
Docker images are automatically built and published to GitHub Container Registry on each release.
Environment variables:
SMTP_USER— Gmail address for sending invitesSMTP_PASS— Gmail app password (create one)
SESSION_SECRET— Strong random string (default is insecure)
PORT(default:3000)APP_BASE_URL(default:http://localhost:3000) — Used in RSVP linksDB_PATH(default:./rsvp.sqliteexcept in Docker where it's/data/rsvp.sqlite)EVENT_BANNER_STORAGE_PATH(default:./data/uploads/event-bannersexcept in Docker where it's/data/uploads/event-banners)
NTFY_TOPIC— Topic name for push notificationsNTFY_BASE_URL(default:https://ntfy.sh)NTFY_USER,NTFY_PASS— Optional auth for private topics
Example nginx config with basic auth:
location /admin {
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://localhost:3000;
}
location / {
proxy_pass http://localhost:3000;
}Or use SSO solutions like Authelia, oauth2-proxy, etc.
- TypeScript + Express
- SQLite (better-sqlite3)
- EJS templates
- Nodemailer (Gmail SMTP)
- Pico CSS
npm testTests use in-memory SQLite.
- No email queueing or rate limiting
- No multi-tenancy (single admin of all events per instance)
- No social login or SSO; admin auth is via reverse proxy only
See TODO.md for more.
AGPL-3.0 — See LICENSE.txt
This is primarily a personal project, but bug reports and PRs are welcome.
This project likely wouldn't exist without AI assistance. Lots of code and documentation were generated or improved using AI tools.