Skip to main content

Architecture

Sessiq is a multi-tenant booking platform built as a TypeScript monorepo. One API serves every tenant (a venue operator), and every tenant has one or more sites (physical venues). This page describes the moving parts and where their code lives.

Tech stack​

LayerTechnology
APINestJS 11 on the Fastify 5 adapter, TypeScript
Web appNext.js 15 (App Router), React 19, next-intl with en and fi
UITailwind CSS, Headless UI and the shared primitives in components/ui
DatabasePostgreSQL 15 with Prisma 5
CacheRedis (optional in development)
Background jobsA separate NestJS worker app (apps/worker)
PaymentsStripe and Paytrail, plus pay-on-arrival
EmailBrevo
TestsVitest (API unit), Jest (API integration/e2e, web unit), Playwright
Package managerpnpm 9 workspaces, Node 22

Repository layout​

PathWhat it is
apps/apiThe HTTP API. Feature modules live in src/modules/.
apps/webThe Next.js app: public storefront, customer portal and the admin console.
apps/workerBackground processes: hold expiry, outbox delivery, slot generation, reports.
apps/docsThis documentation site (Docusaurus).
packages/dbPrisma schema, custom SQL, seeds and the tenant-scoped database client.
packages/contractsShared types and Zod schemas used by the API, web and worker.
packages/scheduling-enginePure TypeScript availability and scheduling logic.
packages/booking-widgetThe embeddable booking flow used on customer websites.
packages/wordpress-pluginThe WordPress plugin that wraps the widget.
packages/astro-embedThe Astro integration that wraps the widget.
packages/observabilityLogging, metrics, tracing and error reporting shared by the API and worker.

Runtime components​

Browser (storefront, portal, admin)       Customer websites (widget)
│ │
▼ ▼
apps/web ─────────────────────────► apps/api ◄──── payment provider callbacks
│
┌─────────────┼──────────────┐
▼ ▼ ▼
PostgreSQL Redis message_outbox table
▲ │
│ ▼
apps/worker ─────────► email, webhooks, analytics, push
  • API (apps/api) handles all reads and writes. Public, customer and admin routes live under /api/v1; a few internal operations routes do not. The OpenAPI spec is committed as apps/api/openapi.json, see Contracts.
  • Web (apps/web) renders the storefront, the customer portal (my-bookings), and the admin console (/[locale]/admin). The admin also installs as a mobile web app with push notifications.
  • Worker (apps/worker) runs as separate processes:
    • the main worker expires unpaid holds and runs scheduled jobs
    • the outbox worker delivers queued messages
    • the slot worker generates bookable slots from schedule templates
    • the scheduled-report worker sends report emails

API modules​

apps/api/src/modules/ has 39 feature modules. Each is a NestJS module with its own controllers, services and DTOs.

AreaModules
Bookings and schedulingbookings, availability, scheduling, booking-groups, event-packages, courses, waitlist, participant-types, waivers, party-invitations
Catalog and adminadmin (products, sites, resources, capacity pools, schedules, slots, add-ons, team, tenant settings, email templates, automation rules), checkout-offers, images, branding
Moneypricing, promotions, gift-cards, campaigns, payments, tax, reconciliation, billing, usage
Customers and messagingcustomers, auth, messaging, notifications, push, reviews, invitations, onboarding
Platformauthz, api-keys, feature-flags, analytics, reporting, public-stats, events
Integrationsone outbound partner integration, disabled by default

Cross-cutting code (guards, tenant resolution, rate limiting, validation, metrics) lives in apps/api/src/common/.

Booking lifecycle​

A booking has two independent status fields: lifecycleStatus (hold, confirmed, arrived, completed, canceled, no_show, expired) and paymentStatus (pending, paid, part_paid, failed, refunded). See Status field conventions.

  1. The client reads availability (GET /api/v1/availability). Slots are pre-generated by the slot worker from each product's schedule templates.
  2. POST /api/v1/bookings/hold reserves capacity and prices the booking. The hold has an expiry.
  3. POST /api/v1/bookings/confirm confirms the booking. For online payment, the payment provider's callback marks it paid.
  4. If a hold is not paid in time, the worker expires it and frees the capacity.
  5. Side effects (confirmation email, webhooks, analytics events) are written to the outbox in the same transaction, then delivered by the outbox worker.

Multi-step operations with compensation (for example cancel-and-refund) follow the saga pattern. See Saga orchestration and Outbox pattern.

Multi-tenancy​

  • Tenant resolution. The API finds the tenant from the signed-in user's token, from the request host (tenant subdomain or custom domain), or from the x-tenant-id header. See Tenant resolution.
  • Tenant isolation. Tenant data is read and written inside withTenant(tenantId, tx => ...) from @booking/db/rls-context. It opens a transaction scoped to that tenant. PostgreSQL row-level security policies then limit every query to that tenant's rows, so isolation holds at both the application and the database layer. See Database.
  • Sites. Most catalog data (products, resources, schedules) belongs to a site. Staff access can be limited to specific sites.

Authorization​

Access control is permission-based and stored in the database:

  • Permissions are keys such as bookings:checkin or tenant:manage. The catalog is defined in packages/contracts/src/authz/permissions.ts.
  • Roles are bundles of permissions. Four system roles are shared by all tenants: owner, admin, member and viewer. Tenants can also create custom roles.
  • Role assignments (the role_assignment table) give a user a role at a scope: the whole tenant, or one site. Restricted high-privilege permissions can never be granted through a site-scoped assignment.
  • Controllers declare the permissions a route needs with @RequirePermissions(...). The guard resolves the user's effective permissions on each request (tenant-scoped permissions plus those assigned for the request's site), with a short Redis cache.

Roles and assignments are managed in Settings → Team in the admin, or via /api/v1/admin/rbac/roles and /api/v1/admin/rbac/assignments. See Authorization.

Deployment​

Production runs as Docker Compose services, built from Dockerfile.prod and defined in docker-compose.prod.yml:

ServiceRole
caddyReverse proxy and TLS
api, webThe API and the Next.js app
worker, slot-workerBackground processes
postgres, redisDatabase and cache
migrateOne-off schema sync, run during each deploy
prometheus, grafana, alertmanagerMetrics, dashboards and alerting

A merge to main runs the quality gates again, builds versioned images for the API, web and worker, syncs the database schema and restarts the services. See Quality gates.

Observability​

  • Structured JSON logs with a request ID on every API request.
  • Prometheus metrics from the API and the worker.
  • Error reporting via Sentry and optional OpenTelemetry tracing, both set up by packages/observability.