Skip to main content

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=all to GET /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 detailed createMeta config 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_settings table) is a generic key-value escape hatch — new per-event flags get added here instead of a migration on the events table.

How it works​

Creating an event — two paths​

POST /v1/event/ branches on the quickCreate query param:

  • Quick create (?quickCreate=true) — only eventSlug is 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 (unless noDateSet is 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, optional settingValueMeta for 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:

TypeWhenEmailPush
seven_days7 days before❌✅
three_days3 days before✅✅
same_day8 AM on event day❌✅
event_startat event start❌✅
post_event2 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) and GET /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 the guests table (matched by contact phone numbers) covers the guest case, results are merged with planning/attending relationship 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/generate produces 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 for for-you/all categories, at least two collections always marked featured. GET /collections/events then resolves one collection's query into an actual event list.

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.

A separate ranking pipeline, not computed on read:

  1. 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).
  2. 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.
  3. 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 rsvpActionType and rsvpActionTypeId are accepted on create — don't assume only one is present when reading historical data.
  • eventTypeId isn't validated against the eventTypes table 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.

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.

  • src/modules/events/event.routes.ts, events.controller.ts, events.service.ts
  • src/shared/validations/event.validation.ts — validateEventCreation
  • src/modules/events/event.openapi.ts — generated-docs schema (doc-only, mirrors the validator above)
  • src/models/event-setting.model.ts — setSetting/getSetting/getBooleanSetting/getEventIdsBySettingValue
  • src/shared/services/event-reminder.service.ts — reminder scheduling/rescheduling; template in src/shared/templates/email/event-reminder.ts
  • src/shared/services/event-trending.service.ts, src/repositories/event-trending.repository.ts, src/modules/eventExplore/eventTrending.controller.ts