Locations (Spaces)
What is this
A location is a physical space within an organization: a room, a floor, an outdoor area. The product calls it a space; the table keeps the name locations (ADR-0024 D1). A branch at another address is its own org. Each row carries an address, city, country, latitude/longitude (used for GPS check-in), capacity, a concurrency setting (exclusive | shared), a sort_order, and an is_active flag. Class sessions optionally reference a space (class_sessions.location_id). A class type can name a default space (class_types.default_location_id) that pre-fills new sessions.
Who uses it
| Persona | Why |
|---|---|
| Owner | Adds/edits spaces; sets coordinates to enable GPS check-in; sets concurrency, capacity and order. |
| Admin | Same as owner. |
| Coach | Reads; selects a space at session-create. |
| Member | Reads space name + map (minisite has an embed); GPS check-in uses lat/lng but the member doesn’t see the coordinates. |
Persona impact
- Tier-gated:
LocationsService.createchecks the cap fromtierForSpaces(platformTier, spacesEnabled). The tier limit and tier copy followspaces: on, Lite 1 / Pro 2 / Elite unlimited; off = Pro 1 (PRE_SPACES_MAX_SPACES, today’s numbers and “Location” wording, “Multiple Locations” on Elite only). - GPS check-in (
POST /sessions/:id/self-checkinwithmethod=gps) readslocation.latitudeandlocation.longitude. If either is null when a member tries GPS check-in, the booking flow throwsBadRequestException('Session location coordinates are not configured').
High-level capabilities
- CRUD — create, list, get, update, deactivate (soft via
is_active=false). The list is ordered bysortOrder, thenname. - Tier enforcement —
maxSpaceschecked at create time; exceeds raises 403 with upgrade copy. - Geo metadata —
latitude/longitude(doublePrecision) used by self-checkin Haversine distance. - Concurrency —
shared(the default) allows overlap (for example open gym next to a class);exclusive, an opt-in, blocks a second overlapping session in the same hall or area. Enforced only when thespacesflag is on for the org. - Capacity ceiling — with the
spacesflag on, a session’s capacity may not exceed the space capacity. With the flag off,location.capacityis informational, as before. - Minisite integration — public minisites can render a map embed for the location (see i18n
minisite.locations*). - Default space — every business has exactly one (
is_default, partial unique index per org). A session that names no space lands in its class type’s space, else the default, so every session has one (ADR-0024 D4 amendment). A business that never set up a space has a hidden,shared“Main space” (auto_created); its first own space takes that row over. Withspaceson, Settings → Spaces marks the default and refuses to remove it before another is chosen; off, removing it hands over to the next space.
Relationship to other features
- scheduling-bookings —
class_sessions.location_idFKs here. GPS check-in resolvessession.location.latitude/longitude. With thespacesflag on, session create/edit and the bulk writers check space overlap and capacity (see behavior). - class-types —
class_types.default_location_idFKs here (ON DELETE SET NULL). It is the class type’s space: its sessions inherit it, and changing it (withspaceson) moves the future sessions still in the old space after a preview. - organizations —
locations.organization_idis the direct org boundary (unlike class_types, no parent indirection). - Platform tiers / billing — see
apps/api/src/platform-tiers/+@taikan/shared.checkTierLimit.maxSpaceslimit varies per tier (multiple_spacesfeature on Pro and Elite withspaceson; off,tierForSpacespresents main’s lists).
Analytics
With the spaces flag on, the API sends space_created / space_updated
(concurrency, hasCapacity, activeSpaceCount, tier), space_rule_blocked
(rule, source, count), space_rule_overridden (rule, role) and
space_limit_reached (tier, limit) and space_cascade_applied
(sessionsChanged). Names and details:
space-context README.
Current status
Shipped. Spaces (TKN-444) added concurrency, sort_order and the class-type default space (migration 0173_spaces); enforcement sits behind the per-org spaces flag, default off. Halls and areas (TKN-463, TKN-479, migrations 0174–0176) apply to every business: each gets one hidden default hall, its locations become that hall’s areas, and a session’s location_id is its hall with its old location in area_id (space-context README §8.5). class_sessions.location_id is NOT NULL (migration 0177). The schema column address is a free-text text field (not normalized). Coordinates are populated either manually by owners or via the AddressAutocomplete component during onboarding (Google Places, see apps/web/src/components/onboarding/address-autocomplete.tsx).
In the dashboard, settings call locations Spaces.
Known gaps
- No bulk-import for multi-space studios; each space added one-at-a-time.
- Deactivating a space with future sessions doesn’t yet offer to move them to the default first (space-context README §8.4).
- Space capacity is a ceiling per session, not a pool shared by sessions running at the same time (TKN-391, out of scope).
- With the
spacesflag off, no overlap or capacity check covers spaces.