Events & Event Types
What it is
An event is the central object planners create and guests interact with — a birthday, wedding, party, etc. Beyond the core entity, this guide covers four supporting systems: event types, flexible event settings, automated reminders, and trending rankings.
Core concepts
- Event types (
isEventType: 1) — 19 total, covering things like Birthday, Wedding, Anniversary, Party, Baby Shower, Concert, Corporate. Four are marked default (Birthday, Wedding, Anniversary, Party) and are what's shown on initial event creation; the rest only appear when the caller passes?mode=alltoGET /v1/event/types/event. - Gift types (
isGiftType: 1) — a separate, smaller set (Secret Santa, My Birthday) for the gift-link product rather than full events. Gift types carry a detailedcreateMetaconfig controlling labels, which planner/guest UI cards are hidden, and defaults like capacity=50 / eventBudget=100000. - The full current ID/name table lives in the repo's root
CLAUDE.md— that's the fastest way to check "what's eventTypeId 9" without a DB query. - Event settings (
event_settingstable) is a generic key-value escape hatch — new per-event flags get added here instead of a migration on theeventstable.
How it works
Creating an event — two paths
POST /v1/event/ branches on the quickCreate query param:
- Quick create (
?quickCreate=true) — onlyeventSlugis required. Built for fast onboarding flows where the rest of the event gets filled in later. - Standard create — requires
name,themeId,capacity,eventBudget,eventTypeId, and (unlessnoDateSetis true)startDate/endDate. Optional booleans toggle features per-event:enableWishlist,enablePartyStore,enableBudgetFeature,enableVendorsFeature,acceptGuestContributions,eighteenPlusCheck.
noDateSet exists because some events (e.g. an ongoing gift link) genuinely have no date — when true, date fields are skipped entirely rather than defaulted.
rsvpActionType (also accepted as rsvpActionTypeId) controls the guest-facing call to action and must be one of: buy-ticket, attend, celebrate-with-us, celebrate-with-me, register.
Event settings (admin-managed feature flags per event)
A generic (eventId, settingKey) → settingValue store, admin-only:
POST /event/admin/:eventId/settings— set/update a setting (settingKey,settingValue, optionalsettingValueMetafor context like who set it and why).GET /event/admin/:eventId/settings— list all settings for an event.DELETE /event/admin/:eventId/settings/:settingKey— remove one.
The one setting actually in active use today is addToDigest, which overrides whether an event is included in the weekly digest email — by default, personal event types (birthday/engagement/wedding — event type IDs 5, 6, 7) are excluded from the digest unless explicitly opted in via this setting. Booleans are stored as the strings 'true'/'false', not 1/0 — a query building on this table should compare against the string, not cast to a number.
Event reminders (automated, multi-channel)
Reminders are scheduled automatically whenever a guest is added or self-RSVPs — there's no separate "enable reminders" step. Five reminder types, each independently configured for which channels fire:
| Type | When | Push | |
|---|---|---|---|
seven_days | 7 days before | ❌ | ✅ |
three_days | 3 days before | ✅ | ✅ |
same_day | 8 AM on event day | ❌ | ✅ |
event_start | at event start | ❌ | ✅ |
post_event | 2 hours after start | ✅ | ✅ |
Only two of the five send email — a deliberate choice to reduce email fatigue while still pushing on every occasion.
The Cloud Tasks 720-hour ceiling: Google Cloud Tasks can't schedule a task more than 30 days out. For an event further away than that, the reminder isn't lost — it's deferred, and a daily job (1:30 AM Africa/Lagos) re-scans for events that have newly come within the 30-day window and schedules their reminders at that point. If you're debugging "why didn't a reminder fire," check whether the event was more than 30 days out when the guest was added — the answer may be "it's correctly waiting for the daily job."
RSVP settings & privacy (a second, separate settings mechanism)
Don't confuse this with the event_settings key-value table above — PUT /event/:eventId/rsvp-settings is a dedicated multipart endpoint on the event itself, covering RSVP configuration, media, and privacy: confirmationRequired (RSVPs need host approval before confirming), disablePublicRSVP (invite-only), keepMemoriesPrivate, plus poster/media upload and removal. Both mechanisms are real and independent — event_settings is the generic admin-only escape hatch (currently just addToDigest), this is the planner-facing structured settings surface.
keepMemoriesPrivate is the flag that gates memory visibility in the Memories & Comments explore feed — false (default) means memories are public to anyone browsing; true restricts them to the creator, accepted co-planners, and guests who actually attended.
Event visibility & discovery (EventExplore)
PATCH /event/:eventId/visibility (owner-only) toggles an event between public and private. This isn't just a display flag — going public automatically indexes the event in Qdrant for semantic search and recommendations; going private removes it from the index and clears any promotion flags. Indexing failures don't block the visibility change itself (graceful degradation).
Discovery lives under /v1/explore and layers several distinct mechanisms:
- Traditional:
GET /categories,GET /category/:categorySlug,GET /promoted,GET /trending(the live-ish "currently trending" list — distinct from the hourly-snapshotted trending rankings above),GET /nearby(lat/lng + radius). - Search:
GET /search(keyword) andGET /search/semantic(natural-language vector search via OpenAI embeddings + Qdrant, e.g."romantic evening celebrations"matching weddings without the word "wedding" appearing anywhere) — semantic search automatically falls back to keyword search on failure. - Contact-aware filtering: both search endpoints accept
contactTypes(host/planner/guest) to scope results to events connected to the caller's synced phone contacts. Implementation is a hybrid: Qdrant vector search covers the host/planner case, a direct SQL query against thegueststable (matched by contact phone numbers) covers the guest case, results are merged withplanning/attendingrelationship flags attached. Both contact ID lists are Redis-cached per user (24h TTL), populated on contact sync. - Personalized home:
GET /sections(authenticated) assembles a home-page feed — featured, from-your-network, recommended, per-category — in one call. - Dynamic collections:
POST /collections/generateproduces titled, queryable groupings (e.g. "Trending Now," "Friends' Birthdays," "Friday Night Vibes," seasonal collections tuned for Nigeria's dry/rainy/harmattan seasons) using deterministic priority rules — trending always first, contact collections only forfor-you/allcategories, at least two collections always markedfeatured.GET /collections/eventsthen resolves one collection's query into an actual event list.
Timezone-aware invite links
GET /event/:eventId/invite-link accepts locale, tz (IANA identifier), and tz_offset_minutes to render the shareable invite text in the recipient's own timezone with an abbreviation (3:00 PM (WAT)), rather than a fixed server timezone. Cached per (eventId, locale, tz, offset) combination for an hour; refresh=true bypasses it.
Trending events (hourly ranking)
A separate ranking pipeline, not computed on read:
- An hourly Cloud Scheduler job (
:30 past every hour) recomputes a trending score for every active public event:trendingScore = rsvpCount24h × 10(RSVP volume in the trailing 24 hours; the formula is deliberately simple today and designed to have more factors — memory uploads, capacity utilization, growth rate — added later). - Rankings are snapshotted into
event_trending_rankings(both an overall rank and a per-category rank), with 7 days of history retained for trend-over-time queries. - Reads (
GET /explore/trending-ranked,.../trending-ranked/category/:category,.../trending-ranked/free,.../trending-ranked/paid,GET /explore/events/:eventId/trending-rank,.../trending-history) all serve the latest precomputed snapshot — trending rank is never calculated live on request, which is why it can lag reality by up to an hour.
Key business rules & edge cases
- 18+ gating (
eighteenPlusCheck) — when enabled on an event, guest date-of-birth is checked both at RSVP payment initiation and again at webhook confirmation time (see the "Key business rules" section in RSVP & Guests for why it's checked twice). - Both
rsvpActionTypeandrsvpActionTypeIdare accepted on create — don't assume only one is present when reading historical data. eventTypeIdisn't validated against theeventTypestable at the route layer today — an invalid ID won't be rejected until something downstream tries to look it up.- A "trending" badge reflects a snapshot up to an hour old — don't build UI that implies real-time accuracy (e.g. "#3 trending right now" is really "#3 as of the last hourly run").
- Reminder scheduling must be triggered from every guest-creation path (manual add, bulk add, self-RSVP with or without items) — a past bug class here was guests added through one path getting reminders while guests added through another silently didn't; if you add a new way to create a guest, confirm it also calls the reminder scheduler.
Related endpoints
Core: GET /v1/event/types/event, POST /v1/event/, GET /v1/event/{eventId}/user — in the generated OpenAPI spec.
Settings: POST/GET /v1/event/admin/{eventId}/settings, DELETE /v1/event/admin/{eventId}/settings/{settingKey}.
Trending: GET /v1/explore/trending-ranked, GET /v1/explore/trending-ranked/category/{category}, GET /v1/explore/events/{eventId}/trending-rank.
Discovery: GET /v1/explore/categories, GET /v1/explore/search, GET /v1/explore/search/semantic, GET /v1/explore/sections, POST /v1/explore/collections/generate, PATCH /v1/event/{eventId}/visibility, PUT /v1/event/{eventId}/rsvp-settings, GET /v1/event/{eventId}/invite-link.
Reminders are not directly called by clients — they're system-scheduled (internal endpoints POST /v1/internal/events/process-reminder, POST /v1/internal/events/schedule-pending-reminders).
None of the settings/reminders/trending/discovery endpoints are in the generated OpenAPI spec yet.
Related code
src/modules/events/event.routes.ts,events.controller.ts,events.service.tssrc/shared/validations/event.validation.ts—validateEventCreationsrc/modules/events/event.openapi.ts— generated-docs schema (doc-only, mirrors the validator above)src/models/event-setting.model.ts—setSetting/getSetting/getBooleanSetting/getEventIdsBySettingValuesrc/shared/services/event-reminder.service.ts— reminder scheduling/rescheduling; template insrc/shared/templates/email/event-reminder.tssrc/shared/services/event-trending.service.ts,src/repositories/event-trending.repository.ts,src/modules/eventExplore/eventTrending.controller.ts