Locations (Spaces) — Data Model
Table: locations in libs/db/src/lib/schema/scheduling.ts. One row is one space (ADR-0024 D1); the table keeps the name locations.
Columns
| Column | Type | Notes |
|---|---|---|
id | uuid PK | defaultRandom() |
organization_id | uuid NOT NULL | FK → organizations.id. Direct org boundary. |
name | varchar(255) NOT NULL | Display name. |
address | text | Free text; not normalized. |
city | varchar(255) | |
country | varchar(100) | |
latitude | double precision | Decimal degrees. Nullable. |
longitude | double precision | Decimal degrees. Nullable. |
capacity | int | Ceiling for a session’s capacity in this space when the spaces flag is on (409 space_capacity_exceeded unless owner/admin override). NULL = no ceiling. Flag off: informational only. Never read by booking. |
concurrency | space_concurrency enum NOT NULL default 'exclusive' | exclusive blocks a second overlapping live session in the space (flag on); shared allows overlap. |
sort_order | int NOT NULL default 0 | List order: sort_order ASC, name ASC. |
is_active | bool NOT NULL default true | Soft-deactivate flag. |
created_at / updated_at | timestamptz | |
deleted_at | timestamptz | Column exists; not written by the service — soft-delete is via is_active=false. |
Indexes / constraints
Only the PK. No UNIQUE; multiple spaces can share a name within an org.
Related index on the child table: class_sessions_location_starts_at_idx on class_sessions(location_id, starts_at), used by the space overlap check.
Relations
organization— one mandatory.classSessions— one-to-many (nullable on the session side).- Class types point here through
class_types.default_location_id(nullable,ON DELETE SET NULL; relationclassTypes.defaultLocation, no inverse onlocations).
Migration
libs/db/drizzle/0167_spaces.sql (TKN-444): creates enum space_concurrency, adds concurrency, sort_order, class_types.default_location_id and class_sessions_location_starts_at_idx. Existing rows become exclusive, order 0. Space overlap and capacity checks stay off until the spaces flag is turned on for an org.
Lifecycle
- Create —
INSERTwithis_active=true,concurrencydefaultexclusive,sort_orderdefault 0. Tier check runs againstcount(active locations in org)andmaxSpaces. Address fields and coords are optional. - Update — partial PATCH; all fields conditional on
!== undefined. Cannot changeorganization_id. - Deactivate —
UPDATE … SET is_active=false. Historical sessions keep their FK.
Multi-org isolation pattern
locations has a direct organization_id FK. Every read/write filters on eq(locations.organizationId, orgId). No FK indirection like class_types.
Soft-delete vs hard-delete
Soft only, via is_active. The deleted_at column is unused but present for schema parity.
GPS check-in usage
ClassSessionsService.selfCheckin reads:
session.location?.latitude
session.location?.longitudeBoth must be non-null. If either is missing or the session has no location, selfCheckin with method='gps' returns 400 'Session location coordinates are not configured'. The distance check is Haversine (haversineMeters) compared against GPS_MAX_METERS (see class-sessions/utils/checkin.utils.ts).