doc.ts
Chobble Tickets
Activate a user by wrapping the data key with their KEK
Activity log table definition message is encrypted - decrypted only for admin view
Add days to a YYYY-MM-DD date string
Queue a promise that must complete before the response is sent
Record a query (no-op when logging is disabled)
Admin role levels
Example availability response JSON
Example free booking response JSON
Example paid booking response JSON
Example booking request body
Example event matching the webhook example data
The example PublicEvent, produced by toPublicEvent
Example list response JSON
Example single-event response JSON
Append iframe=true query param to a URL when in iframe mode
Replace form field values with demo data when demo mode is active. Only replaces fields that are present and non-empty in the form. Mutates and returns the same URLSearchParams for chaining.
Assign events to a group by updating their group_id.
Attendee PII fields
Input for creating an attendee atomically
Stubbable API for testing atomic operations
Booking result — callers map this to their response format
Create a bounded LRU (Least Recently Used) cache. Evicts the least-recently-used entry when capacity is reached. Uses a doubly-linked list for O(1) LRU tracking so that get() does not mutate the key-value index.
Resource management pattern (like Haskell's bracket or try-with-resources). Ensures cleanup happens even if the operation throws.
Build embed snippets (script and iframe variants) for a ticket URL
Build a frame-ancestors CSP value from allowed embed hosts. Returns null if the list is empty (allow embedding from anywhere).
Build info lines from ticket data (non-PII event and booking details)
Build input key mapping from DB columns snake_case DB column → camelCase input key
Build intent metadata for a multi-event checkout. Common fields: multi flag, name, email, serialized items, optional phone/date.
Build a complete .pkpass file as a Uint8Array (ZIP archive)
Build intent metadata for a single-event checkout. Common fields: event_id, name, email, quantity, optional phone/address/date.
Build SVG ticket data from an email entry (non-PII only)
Build the data object exposed to Liquid templates
Generate SVG ticket attachments for all entries
Build a consolidated webhook payload from registration entries
Wrapper for test mocking - delegates to attendeesApi at runtime
Result of creating a checkout session.
Clear all crypto caches (encryption key, HMAC key, private key, hybrid decrypt LRU) Called on key rotation and during test setup/teardown
Clear login attempts for an IP (on successful login)
Clear the active payment provider (disables payments)
Clear setup complete cache (for testing)
Helper to create column definitions
Create an in-memory collection cache with TTL. Loads all items via fetchAll on first access or after invalidation/expiry, then serves from memory until the TTL expires or invalidate() is called. Accepts an optional clock function for testing.
Remove null and undefined values from array
Complete initial setup by storing all configuration Generates the encryption key hierarchy:
Compute slug index from slug for blind index lookup
Compute HMAC-SHA256 using Web Crypto API, returning raw ArrayBuffer
Compute slug index from slug for blind index lookup
Compute ticket token index using HMAC for blind lookups Similar to slug_index for events - allows lookup without decrypting
Setting keys for configuration
- APPLE_WALLET_PASS_TYPE_ID
- APPLE_WALLET_SIGNING_CERT
- APPLE_WALLET_SIGNING_KEY
- APPLE_WALLET_TEAM_ID
- APPLE_WALLET_WWDR_CERT
- BUSINESS_EMAIL
- CONTACT_PAGE_TEXT
- CURRENCY_CODE
- CUSTOM_DOMAIN
- CUSTOM_DOMAIN_LAST_VALIDATED
- EMAIL_API_KEY
- EMAIL_FROM_ADDRESS
- EMAIL_PROVIDER
- EMAIL_TPL_ADMIN_HTML
- EMAIL_TPL_ADMIN_SUBJECT
- EMAIL_TPL_ADMIN_TEXT
- EMAIL_TPL_CONFIRMATION_HTML
- EMAIL_TPL_CONFIRMATION_SUBJECT
- EMAIL_TPL_CONFIRMATION_TEXT
- EMBED_HOSTS
- HEADER_IMAGE_URL
- HOMEPAGE_TEXT
- PAYMENT_PROVIDER
- PHONE_PREFIX
- PUBLIC_KEY
- SETUP_COMPLETE
- SHOW_PUBLIC_API
- SHOW_PUBLIC_SITE
- SQUARE_ACCESS_TOKEN
- SQUARE_LOCATION_ID
- SQUARE_SANDBOX
- SQUARE_WEBHOOK_SIGNATURE_KEY
- STRIPE_SECRET_KEY
- STRIPE_WEBHOOK_ENDPOINT_ID
- STRIPE_WEBHOOK_SECRET
- TERMS_AND_CONDITIONS
- THEME
- TIMEZONE
- WEBSITE_TITLE
- WRAPPED_PRIVATE_KEY
Stubbable API for internal calls (testable via spyOn, like stripeApi/squareApi)
Constant-time string comparison to prevent timing attacks Always iterates over the longer string and XORs the lengths so that different-length inputs don't leak via an early return.
All valid contact field names (runtime array matching the ContactField union)
Individual contact field name
Required name+email with optional phone/address/special_instructions from ContactInfo
Attendee contact details — the core PII fields collected at registration
Wrapper for test mocking - delegates to attendeesApi at runtime
Result of atomic attendee creation
Create an invited user (no password yet, has invite code)
Create manifest.json mapping filenames to SHA-1 hashes
Create a request timer for measuring duration
Create seed events and attendees using efficient batch writes. Encrypts all data before inserting, matching production behavior. Assigns random ticket quantities (1-4) per attendee without overselling.
Create a new session with CSRF token, wrapped data key, and user ID Token is hashed before storage for security
Create a new user with encrypted fields
Create a withClient helper that runs an operation with a lazily-resolved client. Returns null if the client is not available or the operation fails.
Default message for invalid/expired CSRF form submissions
Day name lookup from Date.getUTCDay() index (Sunday=0)
Decrypt a string value encrypted with encrypt() Expects format: enc:1:$base64iv:$base64ciphertext
Decrypt a user's admin level
Decrypt a single raw attendee, handling null input. Used when attendee is fetched via batch query.
Decrypt attendee PII using the private key Used in admin views after obtaining private key from session
Decrypt a list of raw attendees (all fields). Used when attendees are fetched via batch query.
Decrypt attendees for table display, skipping contact fields not configured on the event and payment fields for free events. For a free event that only collects email, this skips up to 6 RSA decryptions per attendee (phone, address, special_instructions, payment_id, refunded, plus 1 symmetric for price_paid).
Decrypt binary data encrypted with encryptBytes().
Decrypt a user's username
Decrypt data with a symmetric key
Default bookable days (all days of the week)
Default timezone when none is configured
Helper for tables whose primary key column is id.
Define a table with CRUD operations
Delete all sessions (used when password is changed)
Delete all stale reservations (unfinalized and older than STALE_RESERVATION_MS). Called from admin event views to clean up abandoned checkouts.
Delete an attendee and its processed payments in a single database round-trip. Uses write batch to cascade: processed_payments → attendee. Reduces 2 sequential HTTP round-trips to 1.
Delete an event and all its attendees in a single database round-trip. Uses write batch to cascade: processed_payments → attendees → event. Reduces 3 sequential HTTP round-trips to 1.
Delete an image from Bunny storage.
Delete all sessions except the current one Token is hashed before database comparison
Delete a session by token Token is hashed before database lookup
Delete a stale reservation to allow retry
Delete a user and all their sessions
Demo addresses
Demo email addresses
Demo event descriptions
Demo event locations
Demo event names
Demo first names for seed data (combined with surnames for more variety)
Demo group names
Demo holiday names
Demo attendee names (full names for demo mode overrides)
Demo page text (homepage / contact)
Demo phone numbers (UK format)
Demo special instructions
Demo surnames for seed data (combined with first names for more variety)
Demo terms and conditions
Demo website titles
Maps form field names to arrays of possible demo values
Derive a Key Encryption Key (KEK) from password hash and DB_ENCRYPTION_KEY Uses PBKDF2 with the password hash as input and DB_ENCRYPTION_KEY as salt
Detect iframe mode from a request URL and store it for the current request
Detect the actual image type from magic bytes. Returns the MIME type if matched, null otherwise.
Matches a valid hostname like "example.com" or "sub.example.com"
Download and decrypt an image from Bunny storage. Uses the storage SDK directly (same as upload/delete) instead of a CDN pull zone URL, which requires a separate pull zone linked to the storage zone. Returns the decrypted image bytes, or null if the file does not exist.
Display labels for email providers — keys must match EmailProvider
Event data needed for email rendering (extends webhook event with display fields)
Union of all supported email provider keys, derived from the PROVIDERS map
Valid email template formats
Valid email template types
Enable query logging and clear previous entries
Encrypt a string value using AES-256-GCM via Web Crypto API Returns format: enc:1:$base64iv:$base64ciphertext Note: ciphertext includes auth tag appended (Web Crypto API does this automatically)
Encrypt attendee PII using the public key from settings This can be called without authentication (for public ticket forms)
Encrypt binary data with AES-256-GCM. Delegates to encrypt() via base64 encoding for maximum code reuse. Used for encrypting image files before CDN storage.
Shared encrypted name column for tables that store a display name.
Encrypt data with a symmetric key (for wrapping private key with DATA_KEY)
Create a failed result
Error code strings for use in logError calls
Human-readable labels for error codes (shown in admin activity log)
Extract a human-readable message from an unknown caught value
Event metadata fields
Convert a UTC ISO datetime to a YYYY-MM-DD calendar date in the given timezone. Returns null if the input is empty or invalid. Used by the calendar view to map standard event dates to calendar days.
Contact fields setting for an event (comma-separated ContactField names, or empty for name-only)
In-memory events cache. Loads all events with attendee counts in a single query and serves subsequent reads from memory until the TTL expires or a write invalidates the cache.
Event type: standard (one-time) or daily (date-based booking)
Execute multiple write statements in a single round-trip using Turso batch API. Statements are executed in order within a single transaction, making this ideal for cascading deletes and multi-step writes. Reduces N sequential HTTP round-trips to 1.
Execute delete by field
Normalize validated session metadata into the canonical SessionMetadata shape.
Extract the inner content of an SVG element (strip the outer <svg> wrapper)
Extract the viewBox from an SVG element to compute its coordinate space
Curried filter
Finalize a reserved session with the created attendee ID (second phase)
Curried flatMap
Await all queued work. Call before returning the response.
Format an amount in minor units (pence/cents) as a currency string. e.g. formatCurrency(1050) → "£10.50" (when currency is GBP)
Format a YYYY-MM-DD date for display. Returns "Monday 15 March 2026"
Format a UTC ISO datetime string for display in the given timezone. Returns e.g. "Monday 15 June 2026 at 14:00 BST"
Format an ISO datetime string for display in the given timezone. Returns e.g. "Monday 15 June 2026 at 14:00 BST"
Format an error context into a human-readable activity log message
Generate a random 256-bit symmetric key for data encryption
Generate a random filename with the correct extension
Generate an RSA key pair for asymmetric encryption Returns { publicKey, privateKey } as exportable JWK strings
Build the pass.json content from pass data and signing credentials
Generate an SVG string for a QR code encoding the given text. Returns a complete <svg> element suitable for inline embedding.
Generate a cryptographically secure random token Uses Web Crypto API getRandomValues
Generate a random slug with at least 2 digits and 2 letters. Uses Fisher-Yates shuffle on the fixed positions to avoid bias.
Generate a standalone SVG ticket with QR code and event/booking details. Returns a complete SVG document string.
Generate a 5-byte uppercase hex ticket token for public ticket URLs
Generate a unique slug by retrying until one is not taken.
Get active events in a group with attendee counts.
Get aggregated statistics for active events. Filters active events from the provided list, computes attendees (sum of quantities) from cached EventWithCount data, and queries ticket count (rows) and income (sum of decrypted price_paid).
Get active holidays (end_date >= today) for date computation (from cache). "today" is computed in the configured timezone.
Resolve the active payment provider based on admin settings. Lazy-loads the provider module to avoid importing unused SDKs. Returns null if no provider is configured.
Get all activity log entries (most recent first)
Collect stats from all registered caches
Get all daily events with attendee counts (from cache).
Get all events with attendee counts (from cache)
Get all groups, decrypted, ordered by id (from cache)
Get all holidays, decrypted, ordered by start_date (from cache)
Get allowed domain for security validation (runtime config via Bunny secrets) This is a required configuration that hardens origin validation
Get all sessions ordered by expiration (newest first)
Get all standard events with attendee counts (from cache). Used by the calendar view to include one-time events on their scheduled date.
Get all users (for admin user management page, from cache)
Get Apple Wallet config for pass generation. DB settings take priority, falls back to env vars.
Get Apple Wallet config from DB (decrypted). Returns null if incomplete.
Get an attendee by ID (decrypted) Requires private key for decryption - only available to authenticated sessions
Get an attendee by ID without decrypting PII Used for payment callbacks and webhooks where decryption is not needed Returns the attendee with encrypted fields (id, event_id, quantity are plaintext)
Get raw attendees for a set of event IDs. Used by the calendar to load attendees for standard events whose decrypted date matches the selected calendar date.
Get attendees by ticket tokens (plaintext tokens, looked up via HMAC index) Returns attendees in the same order as the input tokens.
Get attendees for an event without decrypting PII Used for tests and operations that don't need decrypted data
Compute available booking dates for a daily event. Filters by bookable days of the week and excludes holidays. Returns sorted array of YYYY-MM-DD strings.
Get the Bunny CDN API key from environment
Gets the business email from the database (uses settings cache). Returns decrypted email or empty string if not set.
Return the cached session if already resolved, or undefined if not yet resolved
Get the CDN hostname derived from ALLOWED_DOMAIN. Replaces ".bunny.run" with ".b-cdn.net" for the CNAME target.
Get currency code from database Defaults to GBP if not set
Get currency code from database
Get the most recently generated CSRF token (for synchronous JSX rendering)
Get the custom domain last validated timestamp. Returns null if never validated.
Get distinct attendee dates for daily events. Used for the calendar date picker (lightweight, no attendee data).
Get raw attendees for daily events on a specific date. Bounded query: only returns attendees matching the given date.
Get the total attendee quantity for a specific event + date
Get or create database client
Get the number of decimal places for a currency code
Read email config from DB settings. Falls back to business email for fromAddress. Returns null if not configured.
Get a custom email template (decrypted). Returns null if not customised (use default).
Get all 3 parts of a custom email template (subject, html, text). Nulls mean "use default".
Get allowed embed hosts from database (encrypted, parsed to array) Returns empty array if not configured (embedding allowed from anywhere)
Get allowed embed hosts from database (decrypted) Returns null if not configured (embedding allowed from anywhere)
Get an environment variable value Checks process.env first (Bunny Edge), falls back to Deno.env (local dev)
Get a single event by ID (from cache)
Get activity log entries for an event (most recent first)
Get all events in a group with attendee counts (including inactive).
Get multiple events by slugs (from cache). Returns events in the same order as the input slugs. Missing or inactive events are returned as null.
Get event and its activity log in a single database round-trip. Uses batch API to reduce latency for remote databases.
Get event and a single attendee in a single database round-trip. Used for attendee management pages where we need both the event context and the specific attendee data.
Get event and all attendees in a single database round-trip. Uses batch API to execute both queries together, reducing latency for remote databases like Turso from 2 RTTs to 1. Computes attendee_count from the attendees array.
Get event with attendee count (from cache)
Get event with attendee count by slug (from cache)
Get a single group by slug_index (from cache)
Read Apple Wallet config from environment variables. Returns null if not fully configured.
Read host-level email config from environment variables. Returns null if not fully configured.
Get the current request's iframe mode
Get the proxy URL path for serving a decrypted image. Images are encrypted on CDN, so they must be served through the proxy.
Get the MIME type for an image filename from its extension.
Get the newest attendees across all events without decrypting PII. Used for the admin dashboard to show recent registrations.
Get the next available booking date for a daily event. More efficient than getAvailableDates()[0] — stops at first match. Returns null if no bookable dates are available.
Get the configured payment provider type Returns null if no provider is configured
Get the configured payment provider type Returns null if no provider is configured
Get the configured phone prefix from database. Returns the country calling code, defaulting to "44" (UK).
Derive the private key from session credentials Used to decrypt attendee PII in admin views Results are cached per session token for 10 seconds
Get the attendee ID for an already-processed session Used to return success for idempotent webhook retries
Get the public key for encrypting attendee PII Always available (it's meant to be public)
Return a snapshot of all logged queries
Return the start time recorded by enableQueryLog()
Get a session by token (with 10s TTL cache) Token is hashed for database lookup
Get a setting value. Reads from the in-memory cache, loading all settings in one query on first access or after TTL expiry.
Get the "show public site" setting synchronously from cache. Returns false if the cache is not populated or the setting is not "true". Safe to call from synchronous template code after the settings cache is warmed.
Get Square access token from database (encrypted) Returns null if not configured
Get Square location ID from database Returns null if not configured
Get Square location ID from database Returns null if not configured
Get Square sandbox mode setting from database Returns true if sandbox mode is enabled
Get Square webhook signature key from database (encrypted) Returns null if not configured
Get Stripe publishable key from environment variable Returns null if not set
Get Stripe secret key from database (encrypted) Returns null if not configured (payments disabled)
Get Stripe webhook endpoint ID from database Returns null if not configured
Get Stripe webhook signing secret from database (encrypted) Automatically configured when Stripe secret key is saved
Get terms and conditions text from database (30m cached). Returns null if not configured.
Get the current theme ("light" or "dark")
Get the configured theme from database. Returns "light" or "dark", defaulting to "light".
Get the configured timezone synchronously. Reads from the permanent cache, falling back to the TTL settings cache, then to the default timezone. Safe to call from synchronous template code because the middleware populates the settings cache on every request.
Get the configured timezone from database. Returns the IANA timezone identifier, defaulting to Europe/London. Also populates the permanent timezone cache for sync access via getTimezoneCached().
Get the configured timezone synchronously from cache. Safe to call from synchronous code (templates, helpers) because the settings cache is populated by middleware on every request.
Get ungrouped events (group_id = 0) with attendee counts.
Get a user by ID (from cache)
Find a user by invite code hash Scans all users, decrypts invite_code_hash, and compares
Look up a user by username (using blind index, from cache)
Get the wrapped private key (needs DATA_KEY to decrypt)
Group name field
Group array items by a key function
In-memory groups cache. Loads all groups in a single query and serves subsequent reads from memory until the TTL expires or a write invalidates the cache.
Groups table with CRUD operations — writes auto-invalidate the cache
Check if Apple Wallet is configured (DB settings or env vars).
Check if Apple Wallet DB settings are fully configured (all 5 settings present).
Wrapper for test mocking - delegates to attendeesApi at runtime
Check if an email API key has been configured in the database
Hash an invite code using SHA-256
Hash a password using PBKDF2 Returns format: pbkdf2:iterations:$base64salt:$base64hash
Hash a session token using SHA-256 Used to store session lookups without exposing the actual token
Check if a user has set their password (password_hash is non-empty encrypted value)
Validate that session metadata contains required fields (name) and either event_id (single) or multi+items (multi). Returns false if validation fails.
Check if a Square access token has been configured in the database
Check if a Stripe key has been configured in the database
HMAC-SHA256 hash using DB_ENCRYPTION_KEY Used for blind indexes and hashing limited keyspace values Returns deterministic output for same input (unlike encrypt)
Convert ArrayBuffer to base64 string
Convert ArrayBuffer to hex string
Holiday name field
In-memory holidays cache. Loads all holidays in a single query and serves subsequent reads from memory until the TTL expires or a write invalidates the cache.
Holidays table with CRUD operations — writes auto-invalidate the cache
Decrypt data using hybrid encryption Expects format: hyb:1:$base64WrappedKey:$base64iv:$base64ciphertext Results are cached in a bounded LRU (ciphertext -> plaintext)
Encrypt data using hybrid encryption (RSA + AES)
Shared columns for tables with encrypted slug + blind-index slug_index.
Identity function
User-facing messages for image validation errors
Image validation error
Image validation result
Import a private key from JWK string
Import a public key from JWK string
Initialize database tables
Build SQL placeholders for an IN clause, e.g. "?, ?, ?"
Derive Input type from Row type and Schema
Invalidate the events cache (for testing or after writes).
Invalidate the groups cache (for testing or after writes).
Invalidate the holidays cache (for testing or after writes).
Clear the entire page content cache (for testing or after bulk changes).
Invalidate the settings cache (for testing or after writes). Also clears the permanent timezone cache since it derives from settings, and the page content cache since it derives from encrypted settings.
Invalidate the users cache (for testing or after writes).
Type guard: check if a string is a valid AdminLevel
Check if Bunny CDN pull zone management is enabled Requires BUNNY_API_KEY to be set
Type guard: check if an arbitrary string is a valid ContactField
Check if value is not null or undefined
Check if demo mode is enabled
Type guard: checks if a string is a valid EmailProvider
Type guard: check if an arbitrary string is a valid EventType
Check if a group slug is already in use. Checks both events and groups for cross-table uniqueness.
Check if a user's invite has expired. Callers should skip this for users who have already set a password.
Check if a user's invite is still valid (not expired, has invite code)
Check if IP is rate limited for login
Check whether a submitted form value is the mask sentinel (i.e. unchanged)
Whether an event can accept payments (has a price or allows pay-what-you-want)
Check if payments are enabled (any provider configured with valid keys)
Type guard: check if a string is a valid PaymentStatus
Whether query logging is currently active
Check if a reservation is stale (abandoned by a crashed process)
Check if a payment session has already been processed
Check whether a token uses the signed format
Check if a slug is already in use (optionally excluding a specific event ID) Uses slug_index for lookup (blind index)
Check if image storage is enabled (both env vars are set)
Check if a username is already taken
Validates a basic email format: something@something.something
Check if a naive datetime-local string is a parseable datetime. Does not interpret timezone — purely a format check.
Validate that a string is a parseable PEM certificate
Validate that a string is a parseable PEM private key
Validate that a string is a valid IANA timezone identifier.
The latest database update identifier - update this when changing schema
Resettable lazy reference - like once() but can be reset for testing. Returns [get, set] tuple where set(null) resets to uncomputed state.
Load every setting row into the in-memory cache with a single query.
Load currency code from settings into sync-accessible state. Called once per request in routes/index.ts before templates render. Settings are already cached so this is cheap on repeat calls.
Load theme from settings into sync-accessible state. Called once per request in routes/index.ts before templates render. Settings are already cached so this is cheap on repeat calls.
Convert a naive datetime-local value (YYYY-MM-DDTHH:MM) to a UTC ISO string, interpreting the value as local time in the given timezone.
Log an activity
Log and send consolidated webhook for multi-event registrations
Log attendee registration and send consolidated webhook Used for single-event registrations
Log categories for debug logging
Log a debug message with category prefix For detailed debugging during development
Log a classified error to console.error and persist to the activity log. Console output uses error codes and safe metadata (never PII). Activity log entry is encrypted and visible to admins on the log pages.
Log a classified error to console.error only (no ntfy, no activity log). Use this where calling logError would cause infinite recursion (e.g. ntfy.ts).
Log a completed request to console.debug Path is automatically redacted for privacy
Curried map
Map over a promise-returning function in parallel (Promise.all)
Map over a promise-returning function sequentially (one at a time)
Mark an attendee as refunded (set refunded to encrypted "true"). Keeps payment_id intact so payment details can still be viewed.
Sentinel value rendered in password fields for configured secrets. The actual secret is never sent to the browser — only this placeholder. On form submission, if the value equals the sentinel, the update is skipped.
Max length for email templates
Max length for page text content
Max length for terms and conditions text
Max length for website title
Determine which contact fields to collect for multiple events. Returns the union of all field settings, sorted by canonical CONTACT_FIELDS order.
Registration intent for multi-event checkout
Normalizes email: trim and lowercase
Normalize datetime-local "YYYY-MM-DDTHH:MM" to full UTC ISO string. The input is interpreted as local time in the given timezone and converted to UTC.
Strip non-numeric characters from a phone number, then prefix if it starts with 0
Normalize a user-provided slug: trim, lowercase, replace spaces with hyphens
Current time as a Date
Full ISO-8601 timestamp for created/logged_at fields
Epoch milliseconds for numeric comparisons
Create a successful result
Lazy evaluation - compute once on first call, cache forever.
Use instead of let x = null; const getX = () => x ??= compute();
Parse a comma-separated list of hosts into trimmed, lowercased entries. Filters out empty strings from trailing commas etc.
Parse a comma-separated fields string into individual ContactField names
Data needed to generate a pass — maps to existing ticket/event data
Supported payment provider identifiers
Stubbable API for internal calls (testable via spyOn, like stripeApi/squareApi)
Valid payment status values
Error subclass for user-facing payment validation errors (e.g. invalid phone number). These propagate through safeAsync so the message can be shown to the user.
Pick specific keys from an object
Compose functions left-to-right (pipe) Uses recursive conditional types for arbitrary-length type safety.
Async pipe - compose async functions left-to-right Each function receives the awaited result of the previous one. Uses recursive conditional types for arbitrary-length type safety.
Result type for price validation
Process a single-event booking.
Query all rows, returning a typed array
Execute a SQL query and map result rows through an async transformer.
Execute multiple read queries in a single round-trip using Turso batch API. Significantly reduces latency for remote databases.
Query single row, returning null if not found
Pick a random element from an array
Generate a random full name from first name + surname arrays
Record a failed login attempt Returns true if the account is now locked
Redact dynamic segments from paths for privacy-safe logging Replaces:
Curried reduce
Registration intent for a single event checkout
Render all 3 parts (subject, html, text) using custom templates with fallback to defaults
Render markdown to HTML (block-level: paragraphs, lists, etc.). Raw HTML is escaped.
Render markdown to inline HTML (no wrapping <p> tags). Raw HTML is escaped.
Render a single Liquid template string with the given data
Get a required environment variable, throwing if not set.
Use this instead of getEnv(key) as string when the variable must exist.
Reserve a payment session for processing (first phase of two-phase lock) Inserts with NULL attendee_id to claim the session. Returns { reserved: true } if we claimed it, or { reserved: false, existing } if already claimed.
Result of session reservation attempt
Reset the registry (for testing)
For testing: reset the currency code to default
Reset the database by dropping all tables
Reset cached demo mode value (for testing and cache invalidation)
For testing: reset the engine (so filters can be re-registered after currency changes)
Reset group assignment on all events in a group.
Clear session cache (exported for testing)
For testing: reset the theme to default
Result type for operations that can fail with a Response
Cast libsql ResultSet rows to a typed array (single centralized assertion)
Run a function within a pending-work scope
Run a function with a request-scoped random ID for log correlation
Run a function within a session-memoization scope
Safely execute async operation, returning null on error. Re-throws PaymentUserError so user-facing messages propagate.
Constant-time string comparison to prevent timing attacks
Max attendees per seeded event
Send a single email via the configured provider. Logs errors, never throws. Returns HTTP status or undefined on non-HTTP errors.
Send an error notification to the configured ntfy URL Returns a promise so callers can await delivery if needed. Delivery failures are logged locally (via logErrorLocal) but never throw.
Send registration confirmation + admin notification emails. Entries is an array because one registration can cover multiple events. Silently skips if email is not configured. Attaches one SVG ticket per entry to the confirmation email.
Send consolidated webhook to all unique webhook URLs for the given entries
Send a test email to the business email address. Returns HTTP status or undefined on non-HTTP errors.
Send a webhook payload to a URL Fires and forgets - errors are logged but don't block registration
Serialize multi-ticket items for metadata storage (compact JSON)
Store the resolved session in the current request scope
For testing: set the currency code directly
Set database client (for testing)
Explicitly set demo mode on or off (for testing). Bypasses Deno.env to avoid races between parallel test workers.
Set the active payment provider type
Set a setting value. Invalidates the cache so the next read will pick up the new value.
Store Stripe webhook configuration (secret encrypted, endpoint ID plaintext)
For testing: set the theme directly
In-memory settings cache. Loads all rows in a single query and serves subsequent reads from memory until the TTL expires or a write invalidates the cache.
Stubbable API for testing - allows mocking in ES modules Use spyOn(settingsApi, "method") instead of spyOn(settingsModule, "method")
- PAGE_CACHE_TTL_MS
- clearPaymentProvider
- clearSetupCompleteCache
- completeSetup
- getAppleWalletConfig
- getAppleWalletDbConfig
- getAppleWalletPassTypeIdFromDb
- getAppleWalletSigningCertFromDb
- getAppleWalletSigningKeyFromDb
- getAppleWalletTeamIdFromDb
- getAppleWalletWwdrCertFromDb
- getContactPageTextFromDb
- getCurrencyCodeFromDb
- getCustomDomainFromDb
- getCustomDomainLastValidatedFromDb
- getEmailApiKeyFromDb
- getEmailFromAddressFromDb
- getEmailProviderFromDb
- getEmailTemplate
- getEmailTemplateSet
- getEmbedHostsFromDb
- getHeaderImageUrlFromDb
- getHomepageTextFromDb
- getHostAppleWalletConfig
- getPaymentProviderFromDb
- getPhonePrefixFromDb
- getPublicKey
- getSetting
- getShowPublicApiFromDb
- getShowPublicSiteCached
- getShowPublicSiteFromDb
- getSquareAccessTokenFromDb
- getSquareLocationIdFromDb
- getSquareSandboxFromDb
- getSquareWebhookSignatureKeyFromDb
- getStripeSecretKeyFromDb
- getStripeWebhookEndpointId
- getStripeWebhookSecretFromDb
- getTermsAndConditionsFromDb
- getThemeFromDb
- getTimezoneFromDb
- getWebsiteTitleFromDb
- getWrappedPrivateKey
- hasAppleWalletConfig
- hasAppleWalletDbConfig
- hasEmailApiKey
- hasSquareToken
- hasStripeKey
- invalidatePageCache
- invalidateSettingsCache
- isSetupComplete
- loadAllSettings
- setPaymentProvider
- setSetting
- setStripeWebhookConfig
- updateAppleWalletPassTypeId
- updateAppleWalletSigningCert
- updateAppleWalletSigningKey
- updateAppleWalletTeamId
- updateAppleWalletWwdrCert
- updateContactPageText
- updateCustomDomain
- updateCustomDomainLastValidated
- updateEmailApiKey
- updateEmailFromAddress
- updateEmailProvider
- updateEmailTemplate
- updateEmbedHosts
- updateHeaderImageUrl
- updateHomepageText
- updatePhonePrefix
- updateShowPublicApi
- updateShowPublicSite
- updateSquareAccessToken
- updateSquareLocationId
- updateSquareSandbox
- updateSquareWebhookSignatureKey
- updateStripeKey
- updateTermsAndConditions
- updateTheme
- updateTimezone
- updateUserPassword
- updateWebsiteTitle
Set a user's password (for invite flow)
Compute SHA-1 hex digest of a Uint8Array
Create a signed CSRF token: s1.{timestamp}.{nonce}.{hmac}
Sign the manifest with PKCS#7 detached signature
Site contact page fields
Site homepage fields
Non-mutating sort with comparator
Sort by a key or getter function
Sort events in unified 3-tier order. Works with any Event subtype (Event, EventWithCount, etc.).
Threshold for considering an unfinalized reservation abandoned (5 minutes)
Non-PII ticket data for SVG rendering
Table definition with CRUD operations
Table schema definition Keys are DB column names (snake_case), values are column definitions
Terms and conditions field
Convert snake_case to camelCase
Convert a provider-specific checkout result to a CheckoutSessionResult. Returns null if session ID or URL is missing.
Get today's date as YYYY-MM-DD in the given timezone.
Convert minor units to major units string for form display. e.g. toMajorUnits(1050) → "10.50" (for GBP)
Convert major units (decimal) to minor units (integer). e.g. toMinorUnits(10.50) → 1050 (for GBP)
Convert camelCase to snake_case
Run an async DB operation and log it when tracking is active
Try to delete an image from CDN storage, logging errors on failure
Create a TTL (Time-To-Live) cache. Entries expire after ttlMs milliseconds. Accepts an optional clock function for testing.
Remove duplicate values (by reference/value equality)
Remove duplicates by a key function
Unwrap a symmetric key Expects format: wk:1:$base64iv:$base64wrapped
Unwrap a key using a session token
Update an attendee's information (encrypted fields) Caller must be authenticated admin (public key always exists after setup)
Input for updating an attendee
Updates the business email in the database and invalidates the settings cache. Pass empty string to clear the business email. Email is encrypted at rest.
Update an attendee's checked_in status (encrypted) Caller must be authenticated admin (public key always exists after setup)
Update the custom domain last validated timestamp to now (UTC ISO 8601).
Update a custom email template (encrypted at rest). Pass empty string to clear (revert to default).
Update allowed embed hosts (encrypted at rest) Pass empty string to clear the restriction
Update the configured phone prefix.
Store Square location ID (plaintext - not sensitive)
Update terms and conditions text Pass empty string to clear
Update the configured theme.
Update the configured timezone.
Update a user's password and re-wrap DATA_KEY with new KEK Requires the user's old password hash (decrypted) and their user row
Upload an image to Bunny storage. Encrypts the image bytes before uploading. Returns the filename (without path) on success.
In-memory users cache. Loads all rows in a single query and serves subsequent reads from memory until the TTL expires or a write invalidates the cache.
Convert a UTC ISO datetime string to a datetime-local input value (YYYY-MM-DDTHH:MM) in the given timezone. Used for pre-populating form inputs with timezone-adjusted values.
Valid provider names, derived from the PROVIDERS map
Validate a custom domain (delegates to bunnyCdnApi for testability).
A validated payment session returned after checkout completion
Validate a comma-separated list of host patterns. Returns null if all valid, or the first error message.
Validate encryption key is present and valid Call this on startup to fail fast if key is missing
Validate a single host pattern Returns null if valid, or an error message if invalid
Validate an image file: check MIME type, size, and magic bytes.
Validate and convert a raw price string to minor units. Returns ok with 0 if raw is empty and minPrice is 0 (pay-what-you-want with no input). Returns error if raw is empty and minPrice > 0, or if parsed value is out of range.
Validate a normalized slug. Returns error message or null.
Validate a Liquid template by parsing it (no rendering). Returns null if valid, or an error message string if invalid.
Verify a password against a hash Uses constant-time comparison to prevent timing attacks
Verify a signed CSRF token's signature and expiry
Verify a user's password (decrypt stored hash, then verify) Returns the decrypted password hash if valid (needed for KEK derivation)
Decoded icon files for inclusion in .pkpass bundles
Pretty-printed JSON for embedding in documentation
The example payload, matching what buildWebhookPayload would produce
Attendee data needed for webhook notifications
Consolidated payload sent to webhook endpoints
Event data needed for webhook notifications
Result of webhook endpoint setup
Single ticket in the webhook payload
Result of webhook signature verification
Wrap a symmetric key with another key using AES-GCM Returns format: wk:1:$base64iv:$base64wrapped
Wrap a key using a session token (derives a wrapping key from the token)
Wrap a named resource so create/update apply demo overrides to the form
Encrypt closes_at for DB storage (null/empty → encrypted empty)
Encrypt event date for DB storage
docs/config.ts
Configuration, environment, and session context.
Admin role levels
Stubbable API for internal calls (testable via spyOn, like stripeApi/squareApi)
All valid contact field names (runtime array matching the ContactField union)
Individual contact field name
Required name+email with optional phone/address/special_instructions from ContactInfo
Attendee contact details — the core PII fields collected at registration
Contact fields setting for an event (comma-separated ContactField names, or empty for name-only)
Event type: standard (one-time) or daily (date-based booking)
Get allowed domain for security validation (runtime config via Bunny secrets) This is a required configuration that hardens origin validation
Get the Bunny CDN API key from environment
Return the cached session if already resolved, or undefined if not yet resolved
Get the CDN hostname derived from ALLOWED_DOMAIN. Replaces ".bunny.run" with ".b-cdn.net" for the CNAME target.
Get currency code from database Defaults to GBP if not set
Get allowed embed hosts from database (encrypted, parsed to array) Returns empty array if not configured (embedding allowed from anywhere)
Get an environment variable value Checks process.env first (Bunny Edge), falls back to Deno.env (local dev)
Get the configured payment provider type Returns null if no provider is configured
Get Square access token from database (encrypted) Returns null if not configured
Get Square location ID from database Returns null if not configured
Get Square sandbox mode setting from database Returns true if sandbox mode is enabled
Get Square webhook signature key from database (encrypted) Returns null if not configured
Get Stripe publishable key from environment variable Returns null if not set
Get Stripe secret key from database (encrypted) Returns null if not configured (payments disabled)
Get Stripe webhook signing secret from database (encrypted) Automatically configured when Stripe secret key is saved
Get the configured timezone synchronously from cache. Safe to call from synchronous code (templates, helpers) because the settings cache is populated by middleware on every request.
Type guard: check if a string is a valid AdminLevel
Check if Bunny CDN pull zone management is enabled Requires BUNNY_API_KEY to be set
Type guard: check if an arbitrary string is a valid ContactField
Type guard: check if an arbitrary string is a valid EventType
Whether an event can accept payments (has a price or allows pay-what-you-want)
Check if payments are enabled (any provider configured with valid keys)
Get a required environment variable, throwing if not set.
Use this instead of getEnv(key) as string when the variable must exist.
Run a function within a session-memoization scope
Store the resolved session in the current request scope
docs/crypto.ts
Encryption, hashing, CSRF protection, and secure operations.
Clear all crypto caches (encryption key, HMAC key, private key, hybrid decrypt LRU) Called on key rotation and during test setup/teardown
Compute HMAC-SHA256 using Web Crypto API, returning raw ArrayBuffer
Compute ticket token index using HMAC for blind lookups Similar to slug_index for events - allows lookup without decrypting
Constant-time string comparison to prevent timing attacks Always iterates over the longer string and XORs the lengths so that different-length inputs don't leak via an early return.
Default message for invalid/expired CSRF form submissions
Decrypt a string value encrypted with encrypt() Expects format: enc:1:$base64iv:$base64ciphertext
Decrypt attendee PII using the private key Used in admin views after obtaining private key from session
Decrypt binary data encrypted with encryptBytes().
Decrypt data with a symmetric key
Derive a Key Encryption Key (KEK) from password hash and DB_ENCRYPTION_KEY Uses PBKDF2 with the password hash as input and DB_ENCRYPTION_KEY as salt
Encrypt a string value using AES-256-GCM via Web Crypto API Returns format: enc:1:$base64iv:$base64ciphertext Note: ciphertext includes auth tag appended (Web Crypto API does this automatically)
Encrypt attendee PII using the public key from settings This can be called without authentication (for public ticket forms)
Encrypt binary data with AES-256-GCM. Delegates to encrypt() via base64 encoding for maximum code reuse. Used for encrypting image files before CDN storage.
Encrypt data with a symmetric key (for wrapping private key with DATA_KEY)
Generate a random 256-bit symmetric key for data encryption
Generate an RSA key pair for asymmetric encryption Returns { publicKey, privateKey } as exportable JWK strings
Generate a cryptographically secure random token Uses Web Crypto API getRandomValues
Generate a 5-byte uppercase hex ticket token for public ticket URLs
Get the most recently generated CSRF token (for synchronous JSX rendering)
Derive the private key from session credentials Used to decrypt attendee PII in admin views Results are cached per session token for 10 seconds
Hash a password using PBKDF2 Returns format: pbkdf2:iterations:$base64salt:$base64hash
Hash a session token using SHA-256 Used to store session lookups without exposing the actual token
HMAC-SHA256 hash using DB_ENCRYPTION_KEY Used for blind indexes and hashing limited keyspace values Returns deterministic output for same input (unlike encrypt)
Convert ArrayBuffer to base64 string
Convert ArrayBuffer to hex string
Decrypt data using hybrid encryption Expects format: hyb:1:$base64WrappedKey:$base64iv:$base64ciphertext Results are cached in a bounded LRU (ciphertext -> plaintext)
Encrypt data using hybrid encryption (RSA + AES)
Import a private key from JWK string
Import a public key from JWK string
Check whether a token uses the signed format
Constant-time string comparison to prevent timing attacks
Create a signed CSRF token: s1.{timestamp}.{nonce}.{hmac}
Unwrap a symmetric key Expects format: wk:1:$base64iv:$base64wrapped
Unwrap a key using a session token
Validate encryption key is present and valid Call this on startup to fail fast if key is missing
Verify a password against a hash Uses constant-time comparison to prevent timing attacks
Verify a signed CSRF token's signature and expiry
Wrap a symmetric key with another key using AES-GCM Returns format: wk:1:$base64iv:$base64wrapped
Wrap a key using a session token (derives a wrapping key from the token)
docs/database.ts
Database client, ORM abstractions, and entity tables.
Activate a user by wrapping the data key with their KEK
Activity log table definition message is encrypted - decrypted only for admin view
Record a query (no-op when logging is disabled)
Assign events to a group by updating their group_id.
Input for creating an attendee atomically
Stubbable API for testing atomic operations
Build input key mapping from DB columns snake_case DB column → camelCase input key
Wrapper for test mocking - delegates to attendeesApi at runtime
Clear login attempts for an IP (on successful login)
Clear the active payment provider (disables payments)
Clear setup complete cache (for testing)
Helper to create column definitions
Complete initial setup by storing all configuration Generates the encryption key hierarchy:
Compute slug index from slug for blind index lookup
Compute slug index from slug for blind index lookup
Setting keys for configuration
- APPLE_WALLET_PASS_TYPE_ID
- APPLE_WALLET_SIGNING_CERT
- APPLE_WALLET_SIGNING_KEY
- APPLE_WALLET_TEAM_ID
- APPLE_WALLET_WWDR_CERT
- BUSINESS_EMAIL
- CONTACT_PAGE_TEXT
- CURRENCY_CODE
- CUSTOM_DOMAIN
- CUSTOM_DOMAIN_LAST_VALIDATED
- EMAIL_API_KEY
- EMAIL_FROM_ADDRESS
- EMAIL_PROVIDER
- EMAIL_TPL_ADMIN_HTML
- EMAIL_TPL_ADMIN_SUBJECT
- EMAIL_TPL_ADMIN_TEXT
- EMAIL_TPL_CONFIRMATION_HTML
- EMAIL_TPL_CONFIRMATION_SUBJECT
- EMAIL_TPL_CONFIRMATION_TEXT
- EMBED_HOSTS
- HEADER_IMAGE_URL
- HOMEPAGE_TEXT
- PAYMENT_PROVIDER
- PHONE_PREFIX
- PUBLIC_KEY
- SETUP_COMPLETE
- SHOW_PUBLIC_API
- SHOW_PUBLIC_SITE
- SQUARE_ACCESS_TOKEN
- SQUARE_LOCATION_ID
- SQUARE_SANDBOX
- SQUARE_WEBHOOK_SIGNATURE_KEY
- STRIPE_SECRET_KEY
- STRIPE_WEBHOOK_ENDPOINT_ID
- STRIPE_WEBHOOK_SECRET
- TERMS_AND_CONDITIONS
- THEME
- TIMEZONE
- WEBSITE_TITLE
- WRAPPED_PRIVATE_KEY
Wrapper for test mocking - delegates to attendeesApi at runtime
Result of atomic attendee creation
Create an invited user (no password yet, has invite code)
Create a new session with CSRF token, wrapped data key, and user ID Token is hashed before storage for security
Create a new user with encrypted fields
Decrypt a user's admin level
Decrypt a single raw attendee, handling null input. Used when attendee is fetched via batch query.
Decrypt a list of raw attendees (all fields). Used when attendees are fetched via batch query.
Decrypt attendees for table display, skipping contact fields not configured on the event and payment fields for free events. For a free event that only collects email, this skips up to 6 RSA decryptions per attendee (phone, address, special_instructions, payment_id, refunded, plus 1 symmetric for price_paid).
Decrypt a user's username
Default bookable days (all days of the week)
Define a table with CRUD operations
Delete all sessions (used when password is changed)
Delete all stale reservations (unfinalized and older than STALE_RESERVATION_MS). Called from admin event views to clean up abandoned checkouts.
Delete an attendee and its processed payments in a single database round-trip. Uses write batch to cascade: processed_payments → attendee. Reduces 2 sequential HTTP round-trips to 1.
Delete an event and all its attendees in a single database round-trip. Uses write batch to cascade: processed_payments → attendees → event. Reduces 3 sequential HTTP round-trips to 1.
Delete all sessions except the current one Token is hashed before database comparison
Delete a session by token Token is hashed before database lookup
Delete a stale reservation to allow retry
Delete a user and all their sessions
Valid email template formats
Valid email template types
Enable query logging and clear previous entries
Shared encrypted name column for tables that store a display name.
In-memory events cache. Loads all events with attendee counts in a single query and serves subsequent reads from memory until the TTL expires or a write invalidates the cache.
Execute multiple write statements in a single round-trip using Turso batch API. Statements are executed in order within a single transaction, making this ideal for cascading deletes and multi-step writes. Reduces N sequential HTTP round-trips to 1.
Execute delete by field
Finalize a reserved session with the created attendee ID (second phase)
Get active events in a group with attendee counts.
Get aggregated statistics for active events. Filters active events from the provided list, computes attendees (sum of quantities) from cached EventWithCount data, and queries ticket count (rows) and income (sum of decrypted price_paid).
Get active holidays (end_date >= today) for date computation (from cache). "today" is computed in the configured timezone.
Get all activity log entries (most recent first)
Get all daily events with attendee counts (from cache).
Get all events with attendee counts (from cache)
Get all groups, decrypted, ordered by id (from cache)
Get all holidays, decrypted, ordered by start_date (from cache)
Get all sessions ordered by expiration (newest first)
Get all standard events with attendee counts (from cache). Used by the calendar view to include one-time events on their scheduled date.
Get all users (for admin user management page, from cache)
Get Apple Wallet config for pass generation. DB settings take priority, falls back to env vars.
Get Apple Wallet config from DB (decrypted). Returns null if incomplete.
Get an attendee by ID (decrypted) Requires private key for decryption - only available to authenticated sessions
Get an attendee by ID without decrypting PII Used for payment callbacks and webhooks where decryption is not needed Returns the attendee with encrypted fields (id, event_id, quantity are plaintext)
Get raw attendees for a set of event IDs. Used by the calendar to load attendees for standard events whose decrypted date matches the selected calendar date.
Get attendees by ticket tokens (plaintext tokens, looked up via HMAC index) Returns attendees in the same order as the input tokens.
Get attendees for an event without decrypting PII Used for tests and operations that don't need decrypted data
Get currency code from database
Get the custom domain last validated timestamp. Returns null if never validated.
Get distinct attendee dates for daily events. Used for the calendar date picker (lightweight, no attendee data).
Get raw attendees for daily events on a specific date. Bounded query: only returns attendees matching the given date.
Get the total attendee quantity for a specific event + date
Get or create database client
Get a custom email template (decrypted). Returns null if not customised (use default).
Get all 3 parts of a custom email template (subject, html, text). Nulls mean "use default".
Get allowed embed hosts from database (decrypted) Returns null if not configured (embedding allowed from anywhere)
Get a single event by ID (from cache)
Get activity log entries for an event (most recent first)
Get all events in a group with attendee counts (including inactive).
Get multiple events by slugs (from cache). Returns events in the same order as the input slugs. Missing or inactive events are returned as null.
Get event and its activity log in a single database round-trip. Uses batch API to reduce latency for remote databases.
Get event and a single attendee in a single database round-trip. Used for attendee management pages where we need both the event context and the specific attendee data.
Get event and all attendees in a single database round-trip. Uses batch API to execute both queries together, reducing latency for remote databases like Turso from 2 RTTs to 1. Computes attendee_count from the attendees array.
Get event with attendee count (from cache)
Get event with attendee count by slug (from cache)
Get a single group by slug_index (from cache)
Read Apple Wallet config from environment variables. Returns null if not fully configured.
Get the newest attendees across all events without decrypting PII. Used for the admin dashboard to show recent registrations.
Get the configured payment provider type Returns null if no provider is configured
Get the configured phone prefix from database. Returns the country calling code, defaulting to "44" (UK).
Get the attendee ID for an already-processed session Used to return success for idempotent webhook retries
Get the public key for encrypting attendee PII Always available (it's meant to be public)
Return a snapshot of all logged queries
Return the start time recorded by enableQueryLog()
Get a session by token (with 10s TTL cache) Token is hashed for database lookup
Get a setting value. Reads from the in-memory cache, loading all settings in one query on first access or after TTL expiry.
Get the "show public site" setting synchronously from cache. Returns false if the cache is not populated or the setting is not "true". Safe to call from synchronous template code after the settings cache is warmed.
Get Square location ID from database Returns null if not configured
Get Stripe webhook endpoint ID from database Returns null if not configured
Get terms and conditions text from database (30m cached). Returns null if not configured.
Get the configured theme from database. Returns "light" or "dark", defaulting to "light".
Get the configured timezone synchronously. Reads from the permanent cache, falling back to the TTL settings cache, then to the default timezone. Safe to call from synchronous template code because the middleware populates the settings cache on every request.
Get the configured timezone from database. Returns the IANA timezone identifier, defaulting to Europe/London. Also populates the permanent timezone cache for sync access via getTimezoneCached().
Get ungrouped events (group_id = 0) with attendee counts.
Get a user by ID (from cache)
Find a user by invite code hash Scans all users, decrypts invite_code_hash, and compares
Look up a user by username (using blind index, from cache)
Get the wrapped private key (needs DATA_KEY to decrypt)
In-memory groups cache. Loads all groups in a single query and serves subsequent reads from memory until the TTL expires or a write invalidates the cache.
Groups table with CRUD operations — writes auto-invalidate the cache
Check if Apple Wallet is configured (DB settings or env vars).
Check if Apple Wallet DB settings are fully configured (all 5 settings present).
Wrapper for test mocking - delegates to attendeesApi at runtime
Check if an email API key has been configured in the database
Hash an invite code using SHA-256
Check if a user has set their password (password_hash is non-empty encrypted value)
Check if a Square access token has been configured in the database
Check if a Stripe key has been configured in the database
In-memory holidays cache. Loads all holidays in a single query and serves subsequent reads from memory until the TTL expires or a write invalidates the cache.
Holidays table with CRUD operations — writes auto-invalidate the cache
Shared columns for tables with encrypted slug + blind-index slug_index.
Initialize database tables
Build SQL placeholders for an IN clause, e.g. "?, ?, ?"
Derive Input type from Row type and Schema
Invalidate the events cache (for testing or after writes).
Invalidate the groups cache (for testing or after writes).
Invalidate the holidays cache (for testing or after writes).
Clear the entire page content cache (for testing or after bulk changes).
Invalidate the settings cache (for testing or after writes). Also clears the permanent timezone cache since it derives from settings, and the page content cache since it derives from encrypted settings.
Invalidate the users cache (for testing or after writes).
Check if a group slug is already in use. Checks both events and groups for cross-table uniqueness.
Check if a user's invite has expired. Callers should skip this for users who have already set a password.
Check if a user's invite is still valid (not expired, has invite code)
Check if IP is rate limited for login
Check whether a submitted form value is the mask sentinel (i.e. unchanged)
Whether query logging is currently active
Check if a reservation is stale (abandoned by a crashed process)
Check if a payment session has already been processed
Check if initial setup has been completed Result is cached in memory - once true, we never query again.
Check if a slug is already in use (optionally excluding a specific event ID) Uses slug_index for lookup (blind index)
Check if a username is already taken
The latest database update identifier - update this when changing schema
Load every setting row into the in-memory cache with a single query.
Log an activity
Mark an attendee as refunded (set refunded to encrypted "true"). Keeps payment_id intact so payment details can still be viewed.
Sentinel value rendered in password fields for configured secrets. The actual secret is never sent to the browser — only this placeholder. On form submission, if the value equals the sentinel, the update is skipped.
Max length for email templates
Max length for page text content
Max length for terms and conditions text
Max length for website title
Query all rows, returning a typed array
Execute a SQL query and map result rows through an async transformer.
Execute multiple read queries in a single round-trip using Turso batch API. Significantly reduces latency for remote databases.
Query single row, returning null if not found
Record a failed login attempt Returns true if the account is now locked
Reserve a payment session for processing (first phase of two-phase lock) Inserts with NULL attendee_id to claim the session. Returns { reserved: true } if we claimed it, or { reserved: false, existing } if already claimed.
Result of session reservation attempt
Reset the database by dropping all tables
Reset group assignment on all events in a group.
Clear session cache (exported for testing)
Cast libsql ResultSet rows to a typed array (single centralized assertion)
Set database client (for testing)
Set the active payment provider type
Set a setting value. Invalidates the cache so the next read will pick up the new value.
Store Stripe webhook configuration (secret encrypted, endpoint ID plaintext)
In-memory settings cache. Loads all rows in a single query and serves subsequent reads from memory until the TTL expires or a write invalidates the cache.
Stubbable API for testing - allows mocking in ES modules Use spyOn(settingsApi, "method") instead of spyOn(settingsModule, "method")
- PAGE_CACHE_TTL_MS
- clearPaymentProvider
- clearSetupCompleteCache
- completeSetup
- getAppleWalletConfig
- getAppleWalletDbConfig
- getAppleWalletPassTypeIdFromDb
- getAppleWalletSigningCertFromDb
- getAppleWalletSigningKeyFromDb
- getAppleWalletTeamIdFromDb
- getAppleWalletWwdrCertFromDb
- getContactPageTextFromDb
- getCurrencyCodeFromDb
- getCustomDomainFromDb
- getCustomDomainLastValidatedFromDb
- getEmailApiKeyFromDb
- getEmailFromAddressFromDb
- getEmailProviderFromDb
- getEmailTemplate
- getEmailTemplateSet
- getEmbedHostsFromDb
- getHeaderImageUrlFromDb
- getHomepageTextFromDb
- getHostAppleWalletConfig
- getPaymentProviderFromDb
- getPhonePrefixFromDb
- getPublicKey
- getSetting
- getShowPublicApiFromDb
- getShowPublicSiteCached
- getShowPublicSiteFromDb
- getSquareAccessTokenFromDb
- getSquareLocationIdFromDb
- getSquareSandboxFromDb
- getSquareWebhookSignatureKeyFromDb
- getStripeSecretKeyFromDb
- getStripeWebhookEndpointId
- getStripeWebhookSecretFromDb
- getTermsAndConditionsFromDb
- getThemeFromDb
- getTimezoneFromDb
- getWebsiteTitleFromDb
- getWrappedPrivateKey
- hasAppleWalletConfig
- hasAppleWalletDbConfig
- hasEmailApiKey
- hasSquareToken
- hasStripeKey
- invalidatePageCache
- invalidateSettingsCache
- isSetupComplete
- loadAllSettings
- setPaymentProvider
- setSetting
- setStripeWebhookConfig
- updateAppleWalletPassTypeId
- updateAppleWalletSigningCert
- updateAppleWalletSigningKey
- updateAppleWalletTeamId
- updateAppleWalletWwdrCert
- updateContactPageText
- updateCustomDomain
- updateCustomDomainLastValidated
- updateEmailApiKey
- updateEmailFromAddress
- updateEmailProvider
- updateEmailTemplate
- updateEmbedHosts
- updateHeaderImageUrl
- updateHomepageText
- updatePhonePrefix
- updateShowPublicApi
- updateShowPublicSite
- updateSquareAccessToken
- updateSquareLocationId
- updateSquareSandbox
- updateSquareWebhookSignatureKey
- updateStripeKey
- updateTermsAndConditions
- updateTheme
- updateTimezone
- updateUserPassword
- updateWebsiteTitle
Set a user's password (for invite flow)
Threshold for considering an unfinalized reservation abandoned (5 minutes)
Table definition with CRUD operations
Table schema definition Keys are DB column names (snake_case), values are column definitions
Convert snake_case to camelCase
Convert camelCase to snake_case
Run an async DB operation and log it when tracking is active
Update an attendee's information (encrypted fields) Caller must be authenticated admin (public key always exists after setup)
Input for updating an attendee
Update an attendee's checked_in status (encrypted) Caller must be authenticated admin (public key always exists after setup)
Update the custom domain last validated timestamp to now (UTC ISO 8601).
Update a custom email template (encrypted at rest). Pass empty string to clear (revert to default).
Update allowed embed hosts (encrypted at rest) Pass empty string to clear the restriction
Update the configured phone prefix.
Store Square location ID (plaintext - not sensitive)
Update terms and conditions text Pass empty string to clear
Update the configured theme.
Update the configured timezone.
Update a user's password and re-wrap DATA_KEY with new KEK Requires the user's old password hash (decrypted) and their user row
In-memory users cache. Loads all rows in a single query and serves subsequent reads from memory until the TTL expires or a write invalidates the cache.
Verify a user's password (decrypt stored hash, then verify) Returns the decrypted password hash if valid (needed for KEK derivation)
Encrypt closes_at for DB storage (null/empty → encrypted empty)
Encrypt event date for DB storage
docs/demo.ts
Demo mode and seed data generation.
Replace form field values with demo data when demo mode is active. Only replaces fields that are present and non-empty in the form. Mutates and returns the same URLSearchParams for chaining.
Attendee PII fields
Create seed events and attendees using efficient batch writes. Encrypts all data before inserting, matching production behavior. Assigns random ticket quantities (1-4) per attendee without overselling.
Demo addresses
Demo email addresses
Demo event descriptions
Demo event locations
Demo event names
Demo first names for seed data (combined with surnames for more variety)
Demo group names
Demo holiday names
Demo attendee names (full names for demo mode overrides)
Demo page text (homepage / contact)
Demo phone numbers (UK format)
Demo special instructions
Demo surnames for seed data (combined with first names for more variety)
Demo terms and conditions
Demo website titles
Maps form field names to arrays of possible demo values
Event metadata fields
Group name field
Holiday name field
Check if demo mode is enabled
Pick a random element from an array
Generate a random full name from first name + surname arrays
Reset cached demo mode value (for testing and cache invalidation)
Max attendees per seeded event
Explicitly set demo mode on or off (for testing). Bypasses Deno.env to avoid races between parallel test workers.
Site contact page fields
Site homepage fields
Terms and conditions field
Wrap a named resource so create/update apply demo overrides to the form
docs/email.ts
Email sending, templates, and notifications.
Build SVG ticket data from an email entry (non-PII only)
Build the data object exposed to Liquid templates
Generate SVG ticket attachments for all entries
Display labels for email providers — keys must match EmailProvider
Event data needed for email rendering (extends webhook event with display fields)
Union of all supported email provider keys, derived from the PROVIDERS map
Gets the business email from the database (uses settings cache). Returns decrypted email or empty string if not set.
Read email config from DB settings. Falls back to business email for fromAddress. Returns null if not configured.
Read host-level email config from environment variables. Returns null if not fully configured.
Type guard: checks if a string is a valid EmailProvider
Validates a basic email format: something@something.something
Normalizes email: trim and lowercase
Render all 3 parts (subject, html, text) using custom templates with fallback to defaults
Render a single Liquid template string with the given data
For testing: reset the engine (so filters can be re-registered after currency changes)
Send a single email via the configured provider. Logs errors, never throws. Returns HTTP status or undefined on non-HTTP errors.
Send an error notification to the configured ntfy URL Returns a promise so callers can await delivery if needed. Delivery failures are logged locally (via logErrorLocal) but never throw.
Send registration confirmation + admin notification emails. Entries is an array because one registration can cover multiple events. Silently skips if email is not configured. Attaches one SVG ticket per entry to the confirmation email.
Send a test email to the business email address. Returns HTTP status or undefined on non-HTTP errors.
Updates the business email in the database and invalidates the settings cache. Pass empty string to clear the business email. Email is encrypted at rest.
Valid provider names, derived from the PROVIDERS map
Validate a Liquid template by parsing it (no rendering). Returns null if valid, or an error message string if invalid.
docs/embed.ts
Embeddable widget: iframe integration and CDN storage.
Append iframe=true query param to a URL when in iframe mode
Build embed snippets (script and iframe variants) for a ticket URL
Build a frame-ancestors CSP value from allowed embed hosts. Returns null if the list is empty (allow embedding from anywhere).
Delete an image from Bunny storage.
Detect iframe mode from a request URL and store it for the current request
Detect the actual image type from magic bytes. Returns the MIME type if matched, null otherwise.
Matches a valid hostname like "example.com" or "sub.example.com"
Download and decrypt an image from Bunny storage. Uses the storage SDK directly (same as upload/delete) instead of a CDN pull zone URL, which requires a separate pull zone linked to the storage zone. Returns the decrypted image bytes, or null if the file does not exist.
Generate a random filename with the correct extension
Get the current request's iframe mode
Get the proxy URL path for serving a decrypted image. Images are encrypted on CDN, so they must be served through the proxy.
Get the MIME type for an image filename from its extension.
User-facing messages for image validation errors
Image validation error
Image validation result
Check if image storage is enabled (both env vars are set)
Parse a comma-separated list of hosts into trimmed, lowercased entries. Filters out empty strings from trailing commas etc.
Try to delete an image from CDN storage, logging errors on failure
Upload an image to Bunny storage. Encrypts the image bytes before uploading. Returns the filename (without path) on success.
Validate a custom domain (delegates to bunnyCdnApi for testability).
Validate a comma-separated list of host patterns. Returns null if all valid, or the first error message.
Validate a single host pattern Returns null if valid, or an error message if invalid
Validate an image file: check MIME type, size, and magic bytes.
docs/events.ts
Event management: fields, sorting, and availability.
Add days to a YYYY-MM-DD date string
Day name lookup from Date.getUTCDay() index (Sunday=0)
Convert a UTC ISO datetime to a YYYY-MM-DD calendar date in the given timezone. Returns null if the input is empty or invalid. Used by the calendar view to map standard event dates to calendar days.
Format a YYYY-MM-DD date for display. Returns "Monday 15 March 2026"
Format an ISO datetime string for display in the given timezone. Returns e.g. "Monday 15 June 2026 at 14:00 BST"
Compute available booking dates for a daily event. Filters by bookable days of the week and excludes holidays. Returns sorted array of YYYY-MM-DD strings.
Get the next available booking date for a daily event. More efficient than getAvailableDates()[0] — stops at first match. Returns null if no bookable dates are available.
Determine which contact fields to collect for multiple events. Returns the union of all field settings, sorted by canonical CONTACT_FIELDS order.
Normalize datetime-local "YYYY-MM-DDTHH:MM" to full UTC ISO string. The input is interpreted as local time in the given timezone and converted to UTC.
Parse a comma-separated fields string into individual ContactField names
Sort events in unified 3-tier order. Works with any Event subtype (Event, EventWithCount, etc.).
docs/payments.ts
Payment processing with Stripe and Square.
Booking result — callers map this to their response format
Build intent metadata for a multi-event checkout. Common fields: multi flag, name, email, serialized items, optional phone/date.
Build intent metadata for a single-event checkout. Common fields: event_id, name, email, quantity, optional phone/address/date.
Result of creating a checkout session.
Create a withClient helper that runs an operation with a lazily-resolved client. Returns null if the client is not available or the operation fails.
Extract a human-readable message from an unknown caught value
Normalize validated session metadata into the canonical SessionMetadata shape.
Resolve the active payment provider based on admin settings. Lazy-loads the provider module to avoid importing unused SDKs. Returns null if no provider is configured.
Validate that session metadata contains required fields (name) and either event_id (single) or multi+items (multi). Returns false if validation fails.
Type guard: check if a string is a valid PaymentStatus
Registration intent for multi-event checkout
Supported payment provider identifiers
Stubbable API for internal calls (testable via spyOn, like stripeApi/squareApi)
Valid payment status values
Error subclass for user-facing payment validation errors (e.g. invalid phone number). These propagate through safeAsync so the message can be shown to the user.
Process a single-event booking.
Registration intent for a single event checkout
Safely execute async operation, returning null on error. Re-throws PaymentUserError so user-facing messages propagate.
Serialize multi-ticket items for metadata storage (compact JSON)
Convert a provider-specific checkout result to a CheckoutSessionResult. Returns null if session ID or URL is missing.
A validated payment session returned after checkout completion
Result of webhook endpoint setup
Result of webhook signature verification
docs/tickets.ts
Ticket generation: QR codes, SVG tickets, and Apple Wallet passes.
Build info lines from ticket data (non-PII event and booking details)
Build a complete .pkpass file as a Uint8Array (ZIP archive)
Create manifest.json mapping filenames to SHA-1 hashes
Extract the inner content of an SVG element (strip the outer <svg> wrapper)
Extract the viewBox from an SVG element to compute its coordinate space
Build the pass.json content from pass data and signing credentials
Generate an SVG string for a QR code encoding the given text. Returns a complete <svg> element suitable for inline embedding.
Generate a standalone SVG ticket with QR code and event/booking details. Returns a complete SVG document string.
Validate that a string is a parseable PEM certificate
Validate that a string is a parseable PEM private key
Data needed to generate a pass — maps to existing ticket/event data
Compute SHA-1 hex digest of a Uint8Array
Sign the manifest with PKCS#7 detached signature
Non-PII ticket data for SVG rendering
Decoded icon files for inclusion in .pkpass bundles
docs/utilities.ts
Shared utilities: FP helpers, formatting, slugs, caching, and logging.
Queue a promise that must complete before the response is sent
Create a bounded LRU (Least Recently Used) cache. Evicts the least-recently-used entry when capacity is reached. Uses a doubly-linked list for O(1) LRU tracking so that get() does not mutate the key-value index.
Resource management pattern (like Haskell's bracket or try-with-resources). Ensures cleanup happens even if the operation throws.
Create an in-memory collection cache with TTL. Loads all items via fetchAll on first access or after invalidation/expiry, then serves from memory until the TTL expires or invalidate() is called. Accepts an optional clock function for testing.
Remove null and undefined values from array
Create a request timer for measuring duration
Default timezone when none is configured
Create a failed result
Error code strings for use in logError calls
Human-readable labels for error codes (shown in admin activity log)
Curried filter
Curried flatMap
Await all queued work. Call before returning the response.
Format an amount in minor units (pence/cents) as a currency string. e.g. formatCurrency(1050) → "£10.50" (when currency is GBP)
Format a UTC ISO datetime string for display in the given timezone. Returns e.g. "Monday 15 June 2026 at 14:00 BST"
Format an error context into a human-readable activity log message
Generate a random slug with at least 2 digits and 2 letters. Uses Fisher-Yates shuffle on the fixed positions to avoid bias.
Generate a unique slug by retrying until one is not taken.
Collect stats from all registered caches
Get the number of decimal places for a currency code
Get the current theme ("light" or "dark")
Group array items by a key function
Identity function
Check if value is not null or undefined
Check if a naive datetime-local string is a parseable datetime. Does not interpret timezone — purely a format check.
Validate that a string is a valid IANA timezone identifier.
Resettable lazy reference - like once() but can be reset for testing. Returns [get, set] tuple where set(null) resets to uncomputed state.
Load currency code from settings into sync-accessible state. Called once per request in routes/index.ts before templates render. Settings are already cached so this is cheap on repeat calls.
Load theme from settings into sync-accessible state. Called once per request in routes/index.ts before templates render. Settings are already cached so this is cheap on repeat calls.
Convert a naive datetime-local value (YYYY-MM-DDTHH:MM) to a UTC ISO string, interpreting the value as local time in the given timezone.
Log categories for debug logging
Log a debug message with category prefix For detailed debugging during development
Log a classified error to console.error and persist to the activity log. Console output uses error codes and safe metadata (never PII). Activity log entry is encrypted and visible to admins on the log pages.
Log a classified error to console.error only (no ntfy, no activity log). Use this where calling logError would cause infinite recursion (e.g. ntfy.ts).
Log a completed request to console.debug Path is automatically redacted for privacy
Curried map
Map over a promise-returning function in parallel (Promise.all)
Map over a promise-returning function sequentially (one at a time)
Strip non-numeric characters from a phone number, then prefix if it starts with 0
Normalize a user-provided slug: trim, lowercase, replace spaces with hyphens
Current time as a Date
Full ISO-8601 timestamp for created/logged_at fields
Epoch milliseconds for numeric comparisons
Create a successful result
Lazy evaluation - compute once on first call, cache forever.
Use instead of let x = null; const getX = () => x ??= compute();
Pick specific keys from an object
Compose functions left-to-right (pipe) Uses recursive conditional types for arbitrary-length type safety.
Async pipe - compose async functions left-to-right Each function receives the awaited result of the previous one. Uses recursive conditional types for arbitrary-length type safety.
Result type for price validation
Redact dynamic segments from paths for privacy-safe logging Replaces:
Curried reduce
Register a cache stat provider (called at module load time)
Render markdown to HTML (block-level: paragraphs, lists, etc.). Raw HTML is escaped.
Render markdown to inline HTML (no wrapping <p> tags). Raw HTML is escaped.
Reset the registry (for testing)
For testing: reset the currency code to default
For testing: reset the theme to default
Result type for operations that can fail with a Response
Run a function within a pending-work scope
Run a function with a request-scoped random ID for log correlation
For testing: set the currency code directly
For testing: set the theme directly
Non-mutating sort with comparator
Sort by a key or getter function
Get today's date as YYYY-MM-DD in the given timezone.
Convert minor units to major units string for form display. e.g. toMajorUnits(1050) → "10.50" (for GBP)
Convert major units (decimal) to minor units (integer). e.g. toMinorUnits(10.50) → 1050 (for GBP)
Create a TTL (Time-To-Live) cache. Entries expire after ttlMs milliseconds. Accepts an optional clock function for testing.
Remove duplicate values (by reference/value equality)
Remove duplicates by a key function
Convert a UTC ISO datetime string to a datetime-local input value (YYYY-MM-DDTHH:MM) in the given timezone. Used for pre-populating form inputs with timezone-adjusted values.
Validate and convert a raw price string to minor units. Returns ok with 0 if raw is empty and minPrice is 0 (pay-what-you-want with no input). Returns error if raw is empty and minPrice > 0, or if parsed value is out of range.
Validate a normalized slug. Returns error message or null.
docs/webhooks.ts
Webhook delivery and API examples.
Example availability response JSON
Example free booking response JSON
Example paid booking response JSON
Example booking request body
Example event matching the webhook example data
The example PublicEvent, produced by toPublicEvent
Example list response JSON
Example single-event response JSON
Pretty-printed JSON for embedding in documentation
The example payload, matching what buildWebhookPayload would produce