Hyphen User Guides

Events Management

Complete guide to creating literary events, workshops, and webinars in the Admin Console, configuring the Events Listing and Event Detail pages, and handling Reader Portal RSVP/registration with capacity control.

Version 1.2|Updated 2026-06-23|Content Editors, Operations Staff, Site Administrators, Marketing Team

What This Guide Covers

Events Management lets you publish literary events — readings, workshops, panels, lectures, festivals, meetups, and webinars — and collect attendee registrations through the Reader Portal. This guide covers the full lifecycle: authoring an event in the Admin Console, how it reaches reader-facing pages, how RSVP/registration works (including capacity), and every field and option you can set.

Where events live (important): Events are a first-class entity in the Admin Console, stored in PostgreSQL via Prisma (the events and event_registrations tables). The Admin Console is the source of truth. Events are then synced one-way to Strapi so the Reader Portal can render them, and registrations are written back to Postgres. You author and manage events in the Admin Console, not directly in Strapi.

High-level process flow: author in Admin Console, sync to Strapi for rendering, capture registrations back into Postgres.

Key Concepts

ConceptWhat It Means
EventA Prisma record (events table) with title, type, dates, location, capacity, speakers, schedule, and registration settings.
Event RegistrationA Prisma record (event_registrations table) capturing one attendee's submitted form data, deduplicated by email per event.
Events Listing PageThe reader-facing /events URL showing upcoming and past events.
Event Detail PageThe reader-facing /event/[slug] URL showing one event in full.
Registration Page/event/[slug]/register — the standalone RSVP form.
Capacity (rsvpCapacity)Maximum confirmed registrations. Empty/null = unlimited.
Strapi SyncOne-way push of each event from Postgres to Strapi so the portal can read it.

Who Should Use This

RoleWhat You'll DoRequired Permission
Content EditorsCreate, edit, and publish eventsevents:read, events:create, events:update
Operations / Publishing StaffManage event listings, capacity, archive past eventsevents:update, events:delete
Marketing TeamSet up event landing pages, registration forms, promote eventsevents:create, events:update
Admin / Super AdminFull event management plus page template configurationAll events:*

Permissions are enforced in the Admin Console. The Events area lives under Content Entities → Events, and the UI hides Create/Edit/Delete controls when you lack the matching events:* permission.


Before You Begin

  • You have Admin Console access with at least events:read (and events:create to add events).
  • PostgreSQL/Prisma is running (events are stored there).
  • Strapi is reachable and STRAPI_API_TOKEN is configured, so events sync for the Reader Portal to render. (Sync is non-fatal — an admin save never fails because of a Strapi outage, but the portal won't see the event until sync succeeds.)
  • You understand the basics of the Page Template System if you want to customize the event page layouts (optional — fallback layouts work out of the box).

How It Works (Behind the Scenes)

End-to-end, an event passes through three systems: the Admin Console (authoring + storage of truth), Strapi (read model for the portal), and the Reader Portal (rendering + registration capture).

Authoring writes to Postgres and syncs to Strapi; the Reader Portal reads from Strapi and proxies registrations back to the Admin Console's public endpoint, which enforces capacity and dedup.

The authoring path

  1. An editor creates or edits the event in Content Entities → Events in the Admin Console.
  2. On save, the event is written to the Postgres events table (with audit fields: created_by_id, created_at, updated_at).
  3. The Admin Console calls syncEventToStrapi to upsert the event into Strapi by slug, keeping Strapi's publication state in lockstep with the event status. This is best-effort: failures log a warning and do not block the save.

The reader path

  1. The Reader Portal renders /events (listing) and /event/[slug] (detail) by reading events from Strapi. Layout is driven by the page template system, with built-in fallback sections so pages work even with zero template configuration.
  2. A visitor opens /event/[slug]/register (or clicks the RSVP/Register button on the detail page).

The registration path

  1. The form posts to the Reader Portal route POST /api/events/[slug]/register, which is a thin server-side proxy (it avoids CORS) that forwards to the Admin Console's public endpoint POST /api/content-entities/events/public/[slug]/register.
  2. The Admin Console endpoint runs the registration rules (below), creates an event_registrations row, and atomically increments rsvpCount on the event.

Registration rules enforced by the backend

The public registration endpoint applies, in order:

  • Rate limiting — 5 attempts per 15 minutes per IP (HTTP 429 if exceeded).
  • Event must exist (404 otherwise).
  • Event must be published or featureddraft, cancelled, and archived reject with 400.
  • RSVP must be enabled (rsvpEnabled = true), else 400.
  • Event must not have ended — past events reject with 400 ("This event has already occurred"). An event that has started but not yet ended (Live) is not treated as past; the detail-page RSVP form shows a Live state rather than the registration form.
  • Capacity check — if rsvpCapacity is set and rsvpCount >= rsvpCapacity, the response is HTTP 409 with status: "full".
  • Email required — extracted from the submitted form data; missing email = 400.
  • Required-field validation — every field marked required in the event's registration form schema must be present.
  • Duplicate guard — one registration per email per event (DB unique constraint (event_id, email)); a repeat returns 409 ("already registered").
  • Atomic write — the registration row and the rsvpCount increment happen in a single transaction; new registrations are created with status = confirmed and source = reader_portal by default.

Confirmation email: The registration endpoint itself returns { success, status: "confirmed" } and does not send a confirmation email inline. Transactional/registration emails are governed centrally by the Notifications & Email System (event registry + templates). If you need attendees to receive a confirmation, configure it through the email system rather than expecting it from the registration call itself.


Concepts: Event Model, Registration & Capacity

Event lifecycle (status)

An event's status controls both reader visibility and whether it accepts registrations.

Event status lifecycle. Only published and featured events are reader-visible and accept registrations.
StatusReader VisibilityAccepts Registration
draftHiddenNo
publishedVisible on listing and detailYes (if RSVP enabled & upcoming)
featuredVisible + "Featured" highlightYes (if RSVP enabled & upcoming)
cancelledHidden from listing (direct URL may still resolve)No
archivedHidden from listingNo

Lifecycle (Live / Upcoming / Past) is computed automatically from the dates, independent of the editorial status above:

  • UpcomingstartDate is in the future.
  • Live — the event has started (startDate ≤ now) and has not yet ended (endDate ≥ now). Live requires an End Date; without one, an event flips straight from upcoming to past at its start time and is never bucketed as live.
  • Past — the event has ended (endDate in the past, or startDate in the past when no end date is set).

Past events never accept new registrations regardless of editorial status. A Live event is treated as still-running, not past: its detail page shows a Live badge, and a live online event with an Online URL surfaces a Join now call-to-action (see Registration / RSVP flow below).

Registration / RSVP flow

Registration decision flow. The detail-page RSVP form first checks Live and Past states, then capacity, duplicate, and form rules enforced by the public endpoint.

The detail-page RSVP form resolves to one of several views: loading, not-open-yet / opens-soon (registration-open date in the future), external (a Registration URL is set), full, past, live (happening now — online events show a Join now link), form, and success. A live online event surfaces a Join link even when RSVP is disabled, as long as an Online URL is present.

Capacity and waitlist

  • Capacity is the rsvpCapacity integer. Leave it empty/null for unlimited registrations.
  • rsvpCount is the live confirmed count, auto-incremented on each successful registration.
  • When rsvpCount reaches rsvpCapacity, new registrations are rejected with status: "full".
  • Waitlist: the data model supports a waitlisted state — EventRegistrationStatus is an enum of confirmed | cancelled | waitlisted, so registration records can be marked waitlisted. However, the public reader-facing endpoint currently creates registrations as confirmed and rejects once full rather than auto-waitlisting. Treat waitlisted as an administratively settable status (manual waitlist management) rather than an automatic overflow queue.

Registration form schema

Each event has a registrationFields JSON array describing the form readers fill in. Each field is a FormField (built with the Admin Console's Form Field Builder) with id, type, label, placeholder, required, and order. If you enable RSVP without customizing the form, the default fields are:

Field IDTypeLabelRequiredOrder
nametextFull NameYes1
emailemailEmail AddressYes2
phonephonePhone NumberNo3

The name, email, and phone values are denormalized onto the registration row (for indexed queries and dedup); all submitted values are also stored verbatim in formData.


Configuration Reference

All event fields are configured in the Admin Console → Content Entities → Events (Create New Event, or open an existing event). The portal reads the synced copy from Strapi. The table below is the authoritative field list (backed by the Prisma Event model).

Event content & classification

Field / OptionWhat it doesTypeMandatory?DefaultWhere configuredRole
TitleEvent name shown everywhereStringYesAdmin → Events → Newevents:create
SlugURL identifier (/event/[slug]), uniqueString (UID)YesAuto from titleAdmin → Eventsevents:create
TypeClassification badgeEnum: workshop, panel, reading, lecture, festival, meetup, webinar, otherYesotherAdmin → Eventsevents:create
StatusLifecycle / visibilityEnum: draft, published, featured, cancelled, archivedNodraftAdmin → Eventsevents:update
DescriptionShort summary on cardsTextNoAdmin → Eventsevents:create
ContentRich-text/HTML body on detail pageText (HTML)NoAdmin → Eventsevents:create
Is FeaturedHighlights the eventBooleanNofalseAdmin → Eventsevents:update
PrioritySort weightIntegerNo0Admin → Eventsevents:update
Series NameGroups recurring eventsStringNoAdmin → Eventsevents:update
Tag IDsTopical tags (JSON array of tag IDs)JSONNoAdmin → Eventsevents:update

Date / time

Field / OptionWhat it doesTypeMandatory?DefaultWhere configuredRole
Start DateEvent start; drives upcoming/pastDateTimeYesAdmin → Eventsevents:create
End DateEvent endDateTimeNoAdmin → Eventsevents:create
TimezoneDisplay timezoneStringNoAsia/KolkataAdmin → Eventsevents:create

Location (offline / online)

Field / OptionWhat it doesTypeMandatory?DefaultWhere configuredRole
Is OnlineMarks event as virtualBooleanNofalseAdmin → Eventsevents:create
Online URLVirtual meeting link (also powers the Join now CTA for live online events)StringNoAdmin → Eventsevents:create
Online DetailsHuman-readable access note shown as the venue line on the detail page, e.g. Online (Zoom) | Free, with subscriber priorityString (max 255)NoAdmin → Eventsevents:create
Venue NamePhysical venueStringNoAdmin → Eventsevents:create
AddressStreet addressTextNoAdmin → Eventsevents:create
CityCityStringNoAdmin → Eventsevents:create
Map URLGoogle Maps link (offline events)StringNoAdmin → Eventsevents:create

Media, speakers & schedule

Field / OptionWhat it doesTypeMandatory?DefaultWhere configuredRole
Cover ImageCard + hero image, chosen via the Media Library picker (parity with the Campaign Hero); synced to Strapi as coverImageUrl. On the shared event-detail template, the per-event cover takes precedence over any section background image.String (URL)NoAdmin → Eventsevents:create
GalleryMultiple image URLsJSON arrayNoAdmin → Eventsevents:update
Speakers[{ name, role, bio, photo }]JSON arrayNoAdmin → Eventsevents:update
ScheduleAgenda rows for the detail page. Each item is { time, title, description, speakerId } — a single free-text time per row (event agenda rows do not carry a separate end time; that is a Marketing campaign-schedule feature, not events).JSON arrayNoAdmin → Eventsevents:update

RSVP / registration

Field / OptionWhat it doesTypeMandatory?DefaultWhere configuredRole
RSVP EnabledShows the Register button + formBooleanNofalseAdmin → Eventsevents:update
RSVP CapacityMax confirmed registrations (null = unlimited)IntegerNo— (unlimited)Admin → Eventsevents:update
RSVP CountLive confirmed count (system-maintained)IntegerNo (read-only)0System
Registration Open DateWhen registration becomes availableDateTimeNoAdmin → Eventsevents:update
Registration URLExternal link (e.g. Eventbrite); button links here if setStringNoAdmin → Eventsevents:update
Registration FieldsCustom form schema (Form Field Builder)JSON (FormField[])Noname/email/phone defaultsAdmin → Events (when RSVP enabled)events:update

Post-event

Field / OptionWhat it doesTypeMandatory?DefaultWhere configuredRole
Recording URLRecording playback for past eventsStringNoAdmin → Eventsevents:update
Recap URL"Read the recap" link on past-visits listingStringNoAdmin → Eventsevents:update

Creating an Event (Admin Console)

  1. Go to Content Entities → Events in the Admin Console.
  2. Click + New Event (visible only with events:create).
  3. Fill in the mandatory fields: Title, Slug (auto-generated), Type, Start Date.
  4. Add description, rich-text content, location (toggle Is Online for virtual events and add the Online URL plus an optional Online Details access note, or fill venue/address/city/map for in-person). Set an End Date if the event spans hours — it is what enables the Live state.
  5. Pick a Cover Image from the Media Library and optionally add a gallery.
  6. Add Speakers and Schedule as needed.
  7. To collect attendees, enable RSVP Enabled, set RSVP Capacity (leave blank for unlimited), and customize the Registration Form Fields (or accept the name/email/phone defaults).
  8. Set Status to published (or featured to highlight it).
  9. Save. The event is written to Postgres and synced to Strapi; it appears on the Reader Portal once sync completes.
Reader Portal home, the context in which event links and cards appear to visitors
Reader Portal context. Events surface on /events and /event/[slug]; published/featured events become reader-visible after syncing to Strapi.

Reader-Facing Pages & URLs

PageURLWhat it shows
Events Listing/eventsUpcoming and past events (cards / calendar / list variants)
Event Detail/event/[slug]Full event: header, description, schedule, speakers, RSVP
Registration/event/[slug]/registerStandalone registration form

Route note: The reader detail/registration routes are singular — /event/[slug] and /event/[slug]/register. The listing is plural — /events. Do not link to /event-detail.

The public Events listing page on the Reader Portal
The reader-facing Events listing at /events, rendered from events synced to Strapi.

Template-driven rendering

Event pages are template-driven, following the Hyphen design system. The page layout is composed of section variants from the Page Template System; each section variant is a self-contained, restyled component (shared event-card UI: a 96×120 calendar date badge and a 120×36 RSVP/Details button, reused across the grid and Related Events). The variants are resolved and their data is fetched server-side in the reader portal's section resolver, so what controls a given event page is its page template — not hard-coded markup.

  • Which template controls a page? The event page type drives the listing (/events) and the event-detail page type drives /event/[slug]. If no template is configured for those page types, built-in fallback sections render a complete layout automatically (header, hero, listing cards / detail header, description, speakers, RSVP form, related events, footer). Template configuration is optional and only needed to reorder or swap section variants.
  • Event listing controls. The listing section supports Manual (hand-picked) selection — render exactly the events you choose, in order (matched by Strapi id or the underlying admin event id) — and a dynamic Lifecycle multi-select (Live / Upcoming / Past). Lifecycle bucketing is computed from startDate/endDate (legacy single timeFilter of upcoming/past is still honoured). The listing also renders a View More link.
  • Related Events appears on event detail pages (campaign-related-events variant) — see Related Events below.
  • Campaigns stay out of Events. Marketing campaign events are not written into the reader event collection, so they do not appear in the reader /events grids or event search. Reader event listings show genuine Events only.

See the Page Templates guide for the Canvas Editor workflow; the event section variants (event-hero, event-listing-cards, event-calendar-view, events-list, event-detail-header, event-detail-description, event-speakers-grid, event-rsvp-form, event-past-recordings, event-series-header, campaign-related-events) are available there.

Event detail pages can show a Related Events section (campaign-related-events variant) so readers can discover sibling events. It lists upcoming, published events that share the opened event's Event Type, excluding the opened event itself. The set is resolved entirely server-side: the resolver reads the current event slug from the page path, looks up its type, and fetches up to the configured number of upcoming events of the same type (4 by default), honouring any status filter and explicit exclusions. The section self-hides when there are no related events (or when rendered off an event page, or when the opened event has no Event Type). Selection is fully automatic — there is no manual picker for this section.


SEO and Structured Data

Event pages automatically include meta/Open Graph/Twitter tags, breadcrumb JSON-LD (Home > Events > [Event Title]), and Schema.org Event JSON-LD on detail pages (name, description, dates, attendance mode online/offline, location, organizer). See the SEO/AEO practices in the analytics area for measurement.


Dependencies & Impact

Events touch several other modules. Changes here can ripple outward:

  • Editorial Workflow — Events are authored alongside articles in the Admin Console under Content Entities; the same publish/status discipline (draft → published → featured/archived) applies.
  • Notifications & Email System — Registration confirmation and reminder emails are owned by the email system (event registry + templates), not the registration endpoint. Configure attendee emails there.
  • Marketing & Outreach — The registration form uses the shared Form Field Builder (same primitive as marketing forms). Events can be promoted through marketing campaigns and conversion surfaces.
  • Analytics & Reporting — Registration counts (rsvpCount), capacity utilization, and conversion surfaces feed analytics for measuring event performance.

Operational dependencies: PostgreSQL/Prisma (storage of truth), Strapi + STRAPI_API_TOKEN (read model for the portal — events won't appear on the portal until sync succeeds), and IP-based rate limiting on the registration endpoint.


Common Mistakes and How to Fix Them

ProblemCauseFix
Event not on /eventsStatus is draft/cancelled/archived, or Strapi sync failedSet status to published/featured; confirm Strapi is up and STRAPI_API_TOKEN is set
404 on event pageWrong routeUse /event/[slug] (singular) for detail, /events (plural) for listing
Register button missingRSVP not enabled, or event has endedEnable RSVP Enabled; verify the event has not ended (set an End Date if it spans hours)
Event won't show as "Live"No End Date set, so it can't be in the started-but-not-ended windowAdd an End Date; Live is computed as startDate ≤ now ≤ endDate
"Join now" missing on a live online eventNot marked online, or no Online URLSet Is Online and an Online URL; Join now needs both
A marketing campaign appears in /eventsLegacy data from a prior dual-writeCampaign events are excluded from reader event grids/search; re-sync if you still see legacy rows
"Registration capacity reached" (409 full)rsvpCount >= rsvpCapacityIncrease RSVP Capacity or clear it for unlimited
"Already registered" (409)Email already used for this eventOne registration per email per event by design
"Too many attempts" (429)More than 5 tries in 15 min from one IPWait and retry; this is anti-abuse rate limiting
Required field rejectedA required registration field was emptyFill all required fields, or adjust the form schema in Admin
No confirmation emailRegistration endpoint does not send email inlineConfigure registration emails via the Email System
Cover image not on portalSync to Strapi didn't run/failedRe-save the event; check Strapi connectivity (coverImagecoverImageUrl)

FAQ