Technical reference by package

Settings, Astro components and props, CSS theme variables, cron tasks, declared outbound hosts, and documented bounds under EmDash’s 10-call sandbox budget.

  • EmDash ≥ 1.2.0 < 2.0.0 (1.4.0 recommended)
  • eventual · attendance · eventual-capacity
  • eventual-tickets
  • @vermeulen.solutions/eventual
  • 0.18.0 (14 Nov 2026, MIT; EmDash Registry)

Eventual RSVP – Core

Events, saved venues and organisers in native EmDash content collections, recurring dates, feeds, and zero-JS calendar views.

Setup and EmDash requirements

Requires EmDash >=1.2.0 <2.0.0 (tested on 1.2.0 and 1.4.0; EmDash 1.4.0 is recommended because it fixes nested Block Kit forms and Cloudflare plugin mutations). Source development tooling requires Node 24+.

  1. Install Eventual RSVP – Core from the EmDash Plugin Registry (publisher vermeulen.solutions). An npm package for astro.config.mjs installs follows in a later release.
  2. In Event settings, connect your EmDash content collections for events, venues, and organizers (schema blueprints can be exported via scripts/export-native-schema.mjs).
  3. Keep Show old events saved inside the plugin (legacyReads) disabled unless migrating pre-0.14.0 plugin storage records. Native EmDash collections remain authoritative for titles, rich text, images, drafts, revisions, and translations.
  4. Note your installed plugin ID: config-managed sites usually use eventual, whereas Registry installs receive a deterministic r_… identifier shown in Admin → Plugins. Always pass the actual plugin ID to API URLs and pluginId helper options.

Public routes and Astro helpers

Route Method & Parameters Purpose
/_emdash/api/plugins/<pluginId>/publicEvents GET (from, through, locale, strict, category, publicUrls, eventId) Inclusive window up to 366 days (default UTC today + 180 days). Setting publicUrls=false skips canonical-URL lookups when the site constructs its own paths.
/_emdash/api/plugins/<pluginId>/calendar GET (locale, strict, category) Raw text/calendar iCalendar subscription (UTC today + 365 days), stable UIDs, monotonic SEQUENCE, and STATUS:CONFIRMED / TENTATIVE / CANCELLED.
/_emdash/api/plugins/<pluginId>/publicEventImage?eventId=... GET (Cache-Control: public, max-age=3600, must-revalidate) Streams the published featured image with MIME and size checks; returns 404 for drafts or disabled legacy events.

The Astro helpers in eventual/astro provide fetchPublicFeed, <EventList showCalendarLinks={true} /> (zero client JS), seven query-parameter layout patterns (?view=list|timeline|cards|schedule|dates|month|locations), buildCalendarLinks (Google Calendar, Outlook Web, Office 365, and direct Apple/desktop .ics downloads), and Schema.org eventToJsonLd + serializeJsonLd. They ship in Core’s npm package, which follows in a later release; until then a site reads the same data from the public publicEvents route.

  • strict=true omits events without the requested language translation.
  • strict=false (default) falls back to another language prefixed with [FR] / [EN].
  • In 0.15.0+ on EmDash >=1.4.0, responses carry opt-in cache dependency and expiry metadata (remaining lifetime capped at 15 seconds, never serving stale data).
  • In 0.16.0+, imageMedia exposes native image dimensions and direct media storage keys (/_emdash/api/media/file/<key>) while keeping imageUrl compatible.

Settings, permissions, and limits

Area Detail
Declared capabilities media:read, media:bytes:read, content:read, content:write, content:publish, content:revisions:read, schema:read, hooks.content-policy:register
Outbound HTTP hosts (allowedHosts) [] (zero outbound network calls)
MCP read tools listEvents, getEvent, listOccurrences, listVenues, listOrganizers, getSettings, updateSettings (always supply defaultTimezone when updating settings)
Large events and sandbox limits Shares EmDash’s default 10 bridge calls / 50 ms CPU limit per invocation. Occurrence/exception copies are bounded at 500 items; exports page 5–10 records per cursor (10,000 scan guard before failing explicitly rather than truncating silently); public bundle plugin.mjs sits at 130,690 bytes inside EmDash’s 128 KiB Registry limit.

Troubleshooting (Core)

  • Events show [EN] or [FR] unexpectedly: Check that native translations are published and linked in EmDash, or pass strict=true to the public feed request.
  • Public feed errors after upgrading to 0.14.0+: Public reads intentionally fail rather than falling back to old plugin storage. Connect your native events content collection under Event settings, or temporarily enable legacyReads during migration.
  • Registry installs cannot find /_emdash/api/plugins/eventual/…: Replace eventual with the installed r_… plugin ID shown in EmDash Admin.
  • @vermeulen.solutions/eventual-attendance
  • 0.1.0 (14 Nov 2026, MIT; npm and EmDash Registry)

Eventual RSVP – Registrations

Setup, settings, Astro components, CSS theming variables, emails, scheduled tasks, trust contract, and limits for Eventual RSVP – Registrations.

Registrations setup and settings

Eventual RSVP – Registrations reads published events from EmDash content collections (no attendee data leaves the host database). By default, registration is closed on every event until the event is published and either inventory_mode is set to rsvp (or left absent) and Open for registration is turned on inside the event editor’s Registrations panel.

Global Registration settings page (/attendance/settings)

  • Content collections that hold events: Comma-separated list of collection slugs (default events, e.g. camps, school_series); the first acts as default.
  • Replies to emails go to (optional): Default address for Reply-To headers and admin notifications (New request: …), overridable per event.
  • Send a reminder this many hours before the event (0 = no reminder): Default 24.
  • Language of emails when the form doesn’t say: Fallback en or fr when a booking lacks a saved UI locale.
  • Path of the self-service page: Public URL of the required page (default /rsvp/manage).
  • Delete names, emails and answers this many days after the event (30–3650): retentionDays, default 365.
  • Site setup / Turnstile (folded panel): Optional Cloudflare Turnstile site key and secret key stored in plugin KV (no EMDASH_ENCRYPTION_KEY required), plus buttons to inspect and repair background schedules.

Per-event panel (event-attendance)

  • Capacity per occurrence (0 = unlimited) and waiting-list toggle (allow_waitlist) live directly in the event schema when eventual-capacity fields are installed, or in the panel otherwise.
  • Most places one person can book (maxQuantity), Approve each registration yourself (requireApproval, with 1–720 hours to decide, default 48), and Questions on the registration form as rows: question, kind of answer in words, choices only for a choice list, must be answered; save, remove and move up.

Components and props (@vermeulen.solutions/eventual-attendance/astro/*)

Component & Import Key Props Behaviour
RsvpForm.astro eventId, occurrenceStart, collection?, maxQuantity?, title?, allowWaitlist?, apiEndpoint? (default /_emdash/api/plugins/eventual-attendance/rsvp), formEndpoint? (default ${apiEndpoint}/form), nameLabel?, emailLabel?, locale? ('en' | 'fr') Dispatches bubbling eventual:availability with { full, seatsLeft, allowWaitlist }. Displays the "Manage or cancel this registration" link (#token=…) directly on screen when built cleanly, mentioning inbox email only when email is "sent" or "queued". Requires client JS (D14).
RsvpManage.astro apiBase? (default /_emdash/api/plugins/eventual-attendance), locale? ('en' | 'fr') Placed at /rsvp/manage. Reads signed HMAC token from the URL #fragment, calls POST manage/lookup, and performs state changes (cancel, waitlist/claim) only after explicit button press so email link scanners cannot alter registrations.

Languages

The Registrations, Door list and Registration settings pages follow EmDash’s admin language (English or French). The plugin’s sidebar labels and the event panel’s title come from the English manifest and stay English.

Theming with --eventual-* CSS variables

Component stylesheets sit inside @layer eventual. In Tailwind CSS 4 setups, list eventual before components in your layer declaration:

@layer theme, base, eventual, components, utilities;

Or theme reliably without layer ordering concerns by defining CSS variables on a wrapper around <RsvpForm> and <RsvpManage>:

Category Supported CSS Variables
Colours --eventual-primary, --eventual-primary-hover, --eventual-on-primary, --eventual-bg, --eventual-field-bg (inputs and <select>, default white), --eventual-border, --eventual-text, --eventual-text-muted, --eventual-danger, --eventual-success, --eventual-success-bg, --eventual-warning, --eventual-warning-bg, --eventual-error, --eventual-error-bg
Shapes & focus --eventual-radius, --eventual-control-radius, --eventual-button-radius, --eventual-focus, --eventual-focus-ring

When theming a dark surface, always set --eventual-text and --eventual-field-bg together so light input ink never sits on a white default input background.

Emails, time zones, and scheduled tasks

All visitor emails (booking confirmation, waiting list confirmation, 2-hour held place offer, approval request updates, and reminders) are formatted in the booking’s saved language (en or fr) and the event’s IANA time zone via formatInZone (falling back to UTC), displaying explicit time-zone labels and including a direct link to the .ics calendar route (zero email attachments are needed under EmDash constraints).

Cron Task Schedule Work per Run & Throughput
waitlist-maintenance * * * * * (every minute) Expires up to 2 lapsed 2-hour waiting-list offers and sends ~1 next-in-line offer round-robin per site (~1 offer/min).
attendance-reminders * * * * * (every minute) Sends up to 3 reminder emails per minute (~180/hour) within reminderHours, at most once per confirmed person.
attendance-mail * * * * * (every minute) Drains up to 3 queued confirmation emails per minute when inline sending could not fit the 10-call budget.
attendance-approvals */5 * * * * (every 5 minutes) Auto-declines up to 2 overdue approval requests past their deadline, releases places, and sends queued organiser notices.
ledger-reconcile */10 * * * * (every 10 minutes) Recomputes 1 quiet seat ledger against attendee records when inactive for the quiet period.
attendance-retention 17 * * * * (hourly) Clears personal names, email addresses, question answers, and IP hashes for past occurrences older than retentionDays.

Security, capabilities, and large-event limits

  • Capabilities: content:read, email:send, network:request (allowedHosts: ["challenges.cloudflare.com", "cdn.jsdelivr.net"]: Cloudflare only when Turnstile is enabled; jsDelivr for the hash-checked language packs).
  • Spam & rate defence: Honeypot + form timing checks are always active. When Turnstile is off and EmDash provides client IP, Registrations enforces 10 bookings per 10 minutes using keyed ipHash (32 hex chars of HMAC-SHA256 — never raw IP).
  • Large-event operational bounds (under EmDash default 10 host calls / 50 ms CPU):
    • Per-occurrence reconciliation scans up to ~500 attendee + waiting-list records; larger pools log TOO_LARGE and skip until split across occurrences/dates.
    • Printable/CSV door list exports cap at 700 rows with a top truncation notice; filter by event and date when exporting larger sessions.
    • No usage caps exist in code, no licence key is required, and no EMDASH_ENCRYPTION_KEY is needed.

Troubleshooting (Registrations)

  • No emails are sent: Confirm an EmDash email provider plugin is installed. Registrations still succeed cleanly (email: "none") and display the direct self-service cancel link on screen.
  • Reminders or waiting-list offers do not fire: Open Admin → Registrations once (or check Site setup) to re-register the 6 cron jobs, and ensure your Cloudflare/host cron trigger is active.
  • Route not found on a Registry install: Copy the exact r_… endpoint printed under Site setup and pass it to <RsvpForm apiEndpoint="…" /> and <RsvpManage apiBase="…" />.
  • TRY_AGAIN public error: Check EmDash host logs—usually caused by an unconfigured event collection slug.
  • @vermeulen.solutions/eventual-tickets
  • 0.1.0 (coming soon, commercial licence)

Eventual RSVP – Tickets

Commercial ticketing documentation: Stripe setup, ticket types, pay by invoice, buyer QR order portal, door check-in, --et-* CSS variables, offline licence check, and large-event limits.

Setup and encryption prerequisites

Eventual RSVP – Tickets sells places only on published events whose inventory_mode field (from eventual-capacity) is explicitly set to tickets. That prevents Registrations and Tickets from ever double-selling the same occurrence.

  1. Free tickets & pay by invoice work immediately: Signing keys (state:signing-key) are created in plugin KV automatically, requiring no EMDASH_ENCRYPTION_KEY.
  2. Card payments via Stripe Checkout: Configure EMDASH_ENCRYPTION_KEY in your runtime environment (npx emdash secrets generate or wrangler secret put EMDASH_ENCRYPTION_KEY) so your Stripe secret key (sk_test_…, sk_live_…, or scoped rk_…) and webhook signing secret (whsec_…) store as AES-GCM encrypted settings.
  3. On Admin → Tickets, verify the setup checklist, copy the shown Stripe webhook URL, subscribe to checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, checkout.session.expired, and charge.refunded, then paste your signing secret and evt1.… licence key (production domains only).

Ticket types, pricing, and checkout flow

In each ticketed event’s Tickets sidebar panel (event-tickets), create up to 20 ticket types as rows (name, priceMinor or Free, optional per-date capacity bucket inside the total event cap, maxPerOrder 1–50, salesStart / salesEnd window), plus question rows and Offer payment by invoice or bank transfer instructions (≤ 2000 plain-text chars). A rename keeps the type’s id and its sales.

  • 30–31 minute hold: Stripe Checkout Sessions cannot expire in less than 30 minutes (S-1). Tickets sets expires_at to +31 min, holds places in its own atomic compare-and-set ledger, and verifies amount_total and currency in the raw-byte HMAC-SHA256 webhook (stripe-signature).
  • Pay by invoice: Immediately reserves places (pending_invoice), emails bank transfer instructions with the order reference (ord_…) and order-page link, and issues the signed QR tickets once an organiser clicks Mark paid.
  • Cancel on Stripe / expired holds: <TicketCheckoutReturn /> immediately notifies checkout/release when cancelled (result=cancelled) or checks status (result=success); otherwise tickets-hold-sweep queries Stripe directly before releasing any overdue hold so a delayed webhook never frees paid places.

Astro components and props (@vermeulen.solutions/eventual-tickets/astro/*)

Component Key Props Placement & Purpose
TicketCheckout.astro eventId, occurrenceStart, apiBase? (default /_emdash/api/plugins/eventual-tickets), title?, locale? ('en' | 'fr'), class? Event page ticket selector, quantity steppers, card vs invoice option, custom questions, idempotent commandId.
TicketCheckoutReturn.astro apiBase?, backHref? (default '/'), locale?, class? Return route (/tickets/checkout by default): polls checkout/status on return or calls checkout/release on cancellation.
TicketOrder.astro apiBase?, checkinPath? (default '/tickets/check'), locale?, class? Self-service order portal (/tickets/order): verifies fragment HMAC token (#order=…&token=…), renders browser QR codes (qrcode-generator), shows hex fallback, and triggers self-cancellation/refund before the deadline.
TicketCheckin.astro apiBase?, eventId?, occurrenceStart?, locale?, class? Organiser door check-in route (/tickets/check): staff open while logged into EmDash (content:edit_any); supports camera scan (BarcodeDetector where available), QR link fragment, or typing the 64-hex ticket code.

Theming with --et-* CSS variables

All .eventual-tickets-widget, .et-*, .etr-*, .eto-*, and .etc-* styles read custom properties from the nearest wrapper:

  • Colours: --et-primary, --et-primary-hover, --et-on-primary, --et-bg, --et-field-bg, --et-border, --et-text, --et-muted, --et-success, --et-success-bg, --et-warning, --et-warning-bg, --et-error, --et-error-bg, --et-info-bg.
  • Shapes & focus: --et-radius, --et-control-radius, --et-button-radius, --et-focus, --et-focus-ring.
  • Set --et-text and --et-field-bg together on dark wrappers so light text never renders inside default white inputs.

Offline licence key, emails, cron tasks, and limits

  • Offline licence verification (D17): Uses Ed25519 via Web Crypto inside the Worker (zero outbound calls). Development and preview hosts (localhost, 127.0.0.1, [::1], *.localhost, *.workers.dev, *.pages.dev) require no key. Without a valid key on production domains, only new checkouts return LICENCE_REQUIRED / LICENCE_EXPIRED — existing orders, webhooks, buyer order pages, door check-in, CSV exports, and refunds continue uninterrupted.
  • Languages (en & fr) & time zones: Buyer emails and line items use formatInZone for the event’s local time zone. The event panel, Orders page and setup page follow EmDash’s admin language (English or French); check-in verdicts remain English, and the plugin sidebar labels come from the English manifest.
  • Declared capabilities & outbound hosts: content:read, email:send, network:request limited to ["api.stripe.com", "challenges.cloudflare.com", "cdn.jsdelivr.net", "licences.eventual.rsvp"] (language packs from jsDelivr; the licence service only when an administrator clicks Activate, Refresh or Deactivate).
Cron Task Schedule Work per Run & Large-Event Bound
tickets-hold-sweep * * * * * Checks up to 2 overdue pending Stripe sessions per tick against Stripe before releasing places.
tickets-auto-refund */5 * * * * Automatically refunds paid card orders flagged PAID_NO_SEATS or AMOUNT_MISMATCH (autoRefund default true).
tickets-reconcile */10 * * * * Reconciles 1 quiet occurrence ledger per tick up to ~700 orders (TOO_LARGE above that unless split by date).
tickets-retention 23 * * * * Erases buyer names, emails, question answers, and IP hashes past retentionDays (30–3650, default 365), preserving financial amounts, currency, tier lines, and Stripe IDs.
  • Orders CSV download cap: Bounded at ~700 orders per export with formula-injection protection (= + - @); filter by event and date when exporting larger seasons.

Troubleshooting (Tickets)

  • Card payments disabled in checkout: Verify EMDASH_ENCRYPTION_KEY, your Stripe secret key, and webhook secret on Admin → Tickets. Free tickets and pay-by-invoice continue working without Stripe keys.
  • Missing Stripe refund status in Orders: Wait for Stripe’s charge.refunded webhook delivery or ensure your webhook secret matches the endpoint in Admin → Tickets.
  • QR code camera button unavailable on laptop or iOS browser: Native BarcodeDetector varies by browser; staff can always scan directly with the phone camera app (which opens /tickets/check#code=…) or enter the 64-character ticket code manually.
  • @vermeulen.solutions/eventual-capacity
  • 0.1.0 (14 Nov 2026, MIT; on npm for the field-pack command, bundled into the plugins)

eventual-capacity (Shared library)

Shared zero-overselling ledger, 10-call RPC budget guard, capacity field pack exporter, time zone helpers, and test kit.

Architecture: shared code, zero shared state

@vermeulen.solutions/eventual-capacity is a pure library, not an EmDash plugin. It declares no manifest, no database tables, no routes, and no capabilities. Both Registrations and Tickets list it under devDependencies so the EmDash Plugin CLI inlines the code into each isolated bundle while keeping each plugin’s KV and storage completely separate (P3, P16).

Exported modules

Module What it provides
budget RpcBudget, BudgetSlice, logWithin: guarantees every public route and cron tick remains inside EmDash’s default 10 host subrequest ceiling (including ctx.log RPCs on Cloudflare).
ledger & holds Pooled revision-checked compare-and-set seat ledger (reserveSeats, holdSeatsForOffer, holdSeats, adjustLedger, freeSeats), per-tier buckets inside event limits, and strict FIFO position state.
inventory inventoryModeOf, isRsvpManaged, isTicketManaged, poolKeyOfEvent: enforces a single inventory owner per event so neither plugin can oversell the other.
field-pack CAPACITY_FIELDS, capacityFieldPack, mergeCapacityFields: adds inventory_mode (rsvp | tickets), capacity (integer), and allow_waitlist (boolean) safely into Core’s collection blueprint.
time formatInZone, zonedParts, validTimeZone, eventTiming: resolves one occurrence’s start/end from Core’s start/end/all_day/timezone (with start_date/end_date fallback) and formats dates in IANA time zones (UTC fallback).
tokens & secret Purpose-prefixed HMAC-SHA256 tokens, constant-time hash checks, #fragment links, and KV signing-secret auto-generation (state:signing-key).
questions, ics, turnstile, reconcile, schedules Custom question parser/validator, RFC 5545 single-event .ics generator, Turnstile siteverify, quiet-period CAS ledger reconciliation, and progressive KV schedule self-healing (ensureSchedulesOnce).
testing @vermeulen.solutions/eventual-capacity/testing: withBudget and expectWithinBudget for verifying exact host bridge call counts on createPluginRuntimeTestHost.

Field pack export command

Always apply schema fields additively rather than editing live databases blindly:

npx -p @vermeulen.solutions/eventual-capacity eventual-capacity-fields --merge eventual-schema.json --output eventual-schema-with-capacity.json

Or omit --merge to print only the three fields (inventory_mode, capacity, allow_waitlist) for review against an existing collection schema.

Trust boundaries & performance design

  • 10 bridge calls & 50 ms CPU ceiling (P1): Every public route, webhook, and cron run is designed to complete cleanly within EmDash’s strict sandbox budget without requiring operators to raise worker limits.
  • No external calls in the library itself: Turnstile verification runs only when the consuming plugin passes a configured Turnstile secret and token.
  • Honest test evidence: Browser usability specs, keyboard navigation, WCAG 2.2 AA axe-core sweeps, transactional email snapshots, and runtime-host concurrency tests all verify call counts and race-condition rollbacks directly against EmDash’s test host.