Database client, ORM abstractions, and entity tables.
The database layer uses libsql with a type-safe table abstraction that handles column definitions, field transformers (encrypt/decrypt), and generic CRUD operations.
Entity Tables
- Events — event CRUD with cached encrypted slugs/names
- Attendees — hybrid RSA+AES encryption for PII
- Users — password hashing, admin levels, wrapped keys
- Sessions — token hashing with TTL caching
- Groups — event grouping with encrypted names
- Settings — system configuration (currency, email, payment keys)
- Holidays — date exclusions for daily events
- Activity Log — admin audit trail
- Processed Payments — idempotency tracking
- Login Attempts — rate limiting and lockout
Activate a user by wrapping the data key with their KEK
Record a query (no-op when logging is disabled)
Assign events to a group by updating their group_id.
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)
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
Wrapper for test mocking - delegates to attendeesApi at runtime
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
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 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
Enable query logging and clear previous entries
Shared encrypted name column for tables that store a display name.
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)
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
Shared columns for tables with encrypted slug + blind-index slug_index.
Initialize database tables
Build SQL placeholders for an IN clause, e.g. "?, ?, ?"
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
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.
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
Register a cache stat provider (called at module load time)
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.
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)
Set a user's password (for invite flow)
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)
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
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
Table definition with CRUD operations
Input for creating an attendee atomically
Result of atomic attendee creation
Valid email template formats
Valid email template types
Derive Input type from Row type and Schema
Result of session reservation attempt
Table schema definition Keys are DB column names (snake_case), values are column definitions
Input for updating an attendee
Activity log table definition message is encrypted - decrypted only for admin view
Stubbable API for testing atomic operations
Helper to create column definitions
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
Default bookable days (all days of the week)
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.
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
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
The latest database update identifier - update this when changing schema
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
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
Threshold for considering an unfinalized reservation abandoned (5 minutes)
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.
Usage
import * as mod from "docs/database.ts";