Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesLocationsLocations (Spaces) — Data Model

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

ColumnTypeNotes
iduuid PKdefaultRandom()
organization_iduuid NOT NULLFK → organizations.id. Direct org boundary.
namevarchar(255) NOT NULLDisplay name.
addresstextFree text; not normalized.
cityvarchar(255)
countryvarchar(100)
latitudedouble precisionDecimal degrees. Nullable.
longitudedouble precisionDecimal degrees. Nullable.
capacityintCeiling 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.
concurrencyspace_concurrency enum NOT NULL default 'exclusive'exclusive blocks a second overlapping live session in the space (flag on); shared allows overlap.
sort_orderint NOT NULL default 0List order: sort_order ASC, name ASC.
is_activebool NOT NULL default trueSoft-deactivate flag.
created_at / updated_attimestamptz
deleted_attimestamptzColumn 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; relation classTypes.defaultLocation, no inverse on locations).

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

  1. Create — INSERT with is_active=true, concurrency default exclusive, sort_order default 0. Tier check runs against count(active locations in org) and maxSpaces. Address fields and coords are optional.
  2. Update — partial PATCH; all fields conditional on !== undefined. Cannot change organization_id.
  3. 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?.longitude

Both 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).