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
| Layer | Technology |
|---|---|
| API | NestJS 11 on the Fastify 5 adapter, TypeScript |
| Web app | Next.js 15 (App Router), React 19, next-intl with en and fi |
| UI | Tailwind CSS, Headless UI and the shared primitives in components/ui |
| Database | PostgreSQL 15 with Prisma 5 |
| Cache | Redis (optional in development) |
| Background jobs | A separate NestJS worker app (apps/worker) |
| Payments | Stripe and Paytrail, plus pay-on-arrival |
| Brevo | |
| Tests | Vitest (API unit), Jest (API integration/e2e, web unit), Playwright |
| Package manager | pnpm 9 workspaces, Node 22 |
Repository layout
| Path | What it is |
|---|---|
apps/api | The HTTP API. Feature modules live in src/modules/. |
apps/web | The Next.js app: public storefront, customer portal and the admin console. |
apps/worker | Background processes: hold expiry, outbox delivery, slot generation, reports. |
apps/docs | This documentation site (Docusaurus). |
packages/db | Prisma schema, custom SQL, seeds and the tenant-scoped database client. |
packages/contracts | Shared types and Zod schemas used by the API, web and worker. |
packages/scheduling-engine | Pure TypeScript availability and scheduling logic. |
packages/booking-widget | The embeddable booking flow used on customer websites. |
packages/wordpress-plugin | The WordPress plugin that wraps the widget. |
packages/astro-embed | The Astro integration that wraps the widget. |
packages/observability | Logging, 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 asapps/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.
| Area | Modules |
|---|---|
| Bookings and scheduling | bookings, availability, scheduling, booking-groups, event-packages, courses, waitlist, participant-types, waivers, party-invitations |
| Catalog and admin | admin (products, sites, resources, capacity pools, schedules, slots, add-ons, team, tenant settings, email templates, automation rules), checkout-offers, images, branding |
| Money | pricing, promotions, gift-cards, campaigns, payments, tax, reconciliation, billing, usage |
| Customers and messaging | customers, auth, messaging, notifications, push, reviews, invitations, onboarding |
| Platform | authz, api-keys, feature-flags, analytics, reporting, public-stats, events |
| Integrations | one 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.
- The client reads availability (
GET /api/v1/availability). Slots are pre-generated by the slot worker from each product's schedule templates. POST /api/v1/bookings/holdreserves capacity and prices the booking. The hold has an expiry.POST /api/v1/bookings/confirmconfirms the booking. For online payment, the payment provider's callback marks it paid.- If a hold is not paid in time, the worker expires it and frees the capacity.
- 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-idheader. 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:checkinortenant:manage. The catalog is defined inpackages/contracts/src/authz/permissions.ts. - Roles are bundles of permissions. Four system roles are shared by all tenants:
owner,admin,memberandviewer. Tenants can also create custom roles. - Role assignments (the
role_assignmenttable) 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:
| Service | Role |
|---|---|
caddy | Reverse proxy and TLS |
api, web | The API and the Next.js app |
worker, slot-worker | Background processes |
postgres, redis | Database and cache |
migrate | One-off schema sync, run during each deploy |
prometheus, grafana, alertmanager | Metrics, 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.