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

Locations (Spaces) — Behavior

State

Single boolean is_active:

StateMeaning
is_active=trueListed in findAll, selectable at session-create. Counts against the maxSpaces tier limit.
is_active=falseHidden from findAll. Existing sessions referencing it remain valid.

DELETE /locations/:id performs update set is_active=false, never DELETE FROM.

Invariants

  • Org-direct ownership — locations.organization_id is the boundary. Every read/write filters on it (and(eq(locations.id, id), eq(locations.organizationId, orgId))).
  • No hard delete — deactivation only. Historical sessions retain a valid FK.
  • Capacity gates sessions only with the spaces flag on — then a session’s capacity may not exceed location.capacity (see Spaces flag). Flag off: location.capacity is informational. Either way, booking checks only the session’s capacity.
  • List order — findAll orders by sort_order ASC, name ASC.
  • Tier limit checked at create only — moving from a higher tier down does not auto-deactivate excess spaces. (TODO: verify behavior at tier-downgrade — there’s no explicit guard found.)
  • Coords are an all-or-nothing — service does not reject saving only latitude without longitude; both are nullable. GPS check-in is what enforces both must be present.

Spaces flag

Per-org flag spaces (FeatureFlags.SPACES), default off. Off, unset or unevaluable behaves exactly as before: no space checks, and override fields are ignored. The columns concurrency, sort_order and class_types.default_location_id, and the class-type default-space prefill, are not gated.

With the flag on, a session with a space is checked on single create (POST /sessions) and edit (PATCH /sessions/:id):

#RuleResponse
1Space is exclusive and another live session in the same space overlaps [startsAt, endsAt)409 { code: 'space_conflict', message, locationId, conflictingSessionIds }
2Session capacity > space capacity (both non-null)409 { code: 'space_capacity_exceeded', message, locationId, spaceCapacity, capacity }
  • Live means not cancelled/archived and not soft-deleted. Other sessions are ignored.
  • Adjacent is not overlap. A session ending at 08:00 and one starting at 08:00 can share an exclusive space.
  • shared spaces allow any overlap. Rule 2 still applies.
  • Order. Rule 1 runs before rule 2. A client can get the conflict 409, retry with the conflict override, then get the capacity 409 and retry with both overrides.
  • Override. Owner/admin may send overrideSpaceConflict: { reason } and/or overrideSpaceCapacity: { reason } (reason 1–500 chars, trimmed). The write proceeds and an audit row class_session.space_override (category operations) records it. A coach sending an override that is needed gets 403. An override that is not needed is ignored.
  • Edit checks rule 1 only when locationId, startsAt or endsAt is in the request, and rule 2 only when locationId or capacity is.
  • Bulk writers (template preview/apply, copy-week, Spotter create_many and replicate_day) report and skip exclusive-space clashes; see scheduling-bookings behavior.

Golden paths

Owner: add a space

  1. Dashboard settings → Spaces (/dashboard/settings/locations), or onboarding.
  2. Click add. Form: name, address (autocomplete fills city/country/lat/lng), capacity, concurrency (exclusive default, or shared).
  3. POST /organizations/:orgId/locations (optional concurrency, sortOrder ≥ 0). Tier check runs first.
  4. Row inserted; appears in the selector when creating sessions and in the class-type default space picker.

Owner: deactivate

  1. Click delete. DELETE /organizations/:orgId/locations/:id.
  2. is_active=false. Removed from selector.

Edge cases & error states

ScenarioBehavior
Create when tier limit reached403 ‘Space limit reached (N/M). Upgrade your plan to add more.‘
Create/update with concurrency not exclusive/shared, or sortOrder < 0400 (DTO validation)
Coach/member tries to create/update/delete403 ‘Only owners and admins can …’
Get/update/delete by id from another org404 ‘Location not found’ (WHERE includes organization_id=$orgId)
Member tries to GPS check-in to a session whose location has null coords400 ‘Session location coordinates are not configured’ (from ClassSessionsService.selfCheckin)
Member tries GPS check-in but the session has location_id=NULL400 ‘Session location coordinates are not configured’ (same path; session.location is null)
Update with only some fieldsConditional spreads — only !== undefined fields apply

Side effects

None outside the row mutation. No events, no notifications.

Permissions

Actionowneradmincoachmember
List✓✓✓✓
Get✓✓✓✓
Create✓✓✗✗
Update✓✓✗✗
Deactivate✓✓✗✗

Active org membership required for all (requireMembership).