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

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

PersonaWhy
OwnerAdds/edits spaces; sets coordinates to enable GPS check-in; sets concurrency, capacity and order.
AdminSame as owner.
CoachReads; selects a space at session-create.
MemberReads 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.create checks the cap from tierForSpaces(platformTier, spacesEnabled). The tier limit and tier copy follow spaces: 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-checkin with method=gps) reads location.latitude and location.longitude. If either is null when a member tries GPS check-in, the booking flow throws BadRequestException('Session location coordinates are not configured').

High-level capabilities

  1. CRUD — create, list, get, update, deactivate (soft via is_active=false). The list is ordered by sortOrder, then name.
  2. Tier enforcement — maxSpaces checked at create time; exceeds raises 403 with upgrade copy.
  3. Geo metadata — latitude/longitude (doublePrecision) used by self-checkin Haversine distance.
  4. 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 the spaces flag is on for the org.
  5. Capacity ceiling — with the spaces flag on, a session’s capacity may not exceed the space capacity. With the flag off, location.capacity is informational, as before.
  6. Minisite integration — public minisites can render a map embed for the location (see i18n minisite.locations*).
  7. 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. With spaces on, 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_id FKs here. GPS check-in resolves session.location.latitude/longitude. With the spaces flag on, session create/edit and the bulk writers check space overlap and capacity (see behavior).
  • class-types — class_types.default_location_id FKs here (ON DELETE SET NULL). It is the class type’s space: its sessions inherit it, and changing it (with spaces on) moves the future sessions still in the old space after a preview.
  • organizations — locations.organization_id is the direct org boundary (unlike class_types, no parent indirection).
  • Platform tiers / billing — see apps/api/src/platform-tiers/ + @taikan/shared.checkTierLimit. maxSpaces limit varies per tier (multiple_spaces feature on Pro and Elite with spaces on; off, tierForSpaces presents 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 spaces flag off, no overlap or capacity check covers spaces.