Skip to content

Sphere

Purpose

The top-level partition of the entire bookable catalogue. A sphere is a coarse-grained domain (SPORT, EVENTS, SERVICES) that determines (a) which Flutter app surfaces the activity to end customers, (b) which activity_type values are legal under it, (c) the default activity type when none is specified. Spheres are platform-level — they are not owned by any company; only super-admin can create, modify, or delete them. The product side often calls a sphere a category in conversation; the two terms refer to the same row (see Glossary).

Final sphere catalogue (post-869dpxbj6, 3 rows, sort_order 0/1/2):

codetarget_appallowed_activity_typesdefault_activity_typesort_order
SPORTGYM_APP[SLOT_BASED, SERVICE]SLOT_BASED0
EVENTSTICKETS_APP[SLOT_BASED, SERVICE]SLOT_BASED1
SERVICESGYM_APP[SLOT_BASED, SERVICE]SERVICE2

EVENTS was inserted by migration 0049_worried_rattler.sql; the prior CINEMA and SHOWS rows were merged into it and then DELETEd after activities + categories had their sphere_id backfilled. All three rows now carry the same allowed_activity_types array — the column is retained as cheap optionality for future restrictive spheres. See ADR activity-model-simplification.

Identity & key fields

  • Primary key: id (uuid, default gen_random_uuid()).
  • code (text, UNIQUE, NOT NULL) — current values are SPORT | EVENTS | SERVICES. The DINING row was removed by migration 0048_clear_tattoo.sql — see ADR sphere-targetapp-enum-cleanup-drop-dining-add-services. CINEMA and SHOWS rows were consolidated into a single EVENTS row by migration 0049_worried_rattler.sql — see ADR activity-model-simplification.
  • name (jsonb, NOT NULL) — multi-language { uk, en, ru, de, fr }.
  • icon (nullable text).
  • targetApp (enum sphere_target_app: GYM_APP, TICKETS_APP, SERVICES_APP). DINING_APP was removed and SERVICES_APP added by migration 0048_clear_tattoo.sql — see ADR sphere-targetapp-enum-cleanup-drop-dining-add-services. The SERVICES_APP value is present in the enum but is not yet assigned to any sphere row — the SERVICES sphere still targets GYM_APP (re-mapping is a separate ticket).
  • allowedActivityTypes (activity_type[], NOT NULL) — sphere-level allowlist of activity_type enum values (SLOT_BASED | SERVICE). Post-869dpxbj6 every sphere carries [SLOT_BASED, SERVICE] uniformly.
  • defaultActivityType (activity_type enum, NOT NULL).
  • sortOrder (integer, NOT NULL, default 0).
  • createdAt (timestamptz, NOT NULL).

targetApp is the routing key — it decides which Flutter app surfaces the activities of this sphere to end customers. code is the canonical business identifier referenced throughout the codebase (selectors, routes, filters) — must remain stable.

Invariants

  • code is UNIQUE (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/activities.schema.ts).
  • code, name, targetApp, allowedActivityTypes, defaultActivityType, sortOrder, createdAt NOT NULL (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/activities.schema.ts).
  • defaultActivityType and allowedActivityTypes[] are constrained to the activities.activity_type PG enum domain (SLOT_BASED | SERVICE). Any other value is rejected at DB level with Postgres error 22P02 (enforced by migration 0046 — see specs/sphere-column-type-safety.md; DINING removed in migration 0048 — see specs/sphere-targetapp-enum-cleanup-drop-dining-add-services.md; SHOW and MOVIE removed and enum collapsed by migration 0049 — see specs/activity-model-simplification.md).

Business invariants:

  • Every Activity and every Category points at exactly one Sphere (sphere_id NOT NULL FK).
  • targetApp (sphere → Flutter app) is one-to-one in practice — one sphere routes to one app — even though the schema does not enforce uniqueness.
  • defaultActivityType must be a value present in allowedActivityTypes. Application-side check only — no DB CHECK constraint today.
  • Adding a new sphere requires Flutter app support: there is no auto-routing for an unrecognised targetApp value.
  • Sphere mutation is super-admin only — neither client nor business surfaces can write.

Lifecycle

No state machine.

  • Create: super-admin only, via /api/superadmin/spheres. Triggers a row in Sphere audit log.
  • Update: super-admin only — rename, reorder, add/remove allowed activity types. Each mutation is audit-logged.
  • Delete: super-admin only — refused by Postgres if any Activity or Category still references the sphere. The API returns SphereDeleteConflictDto (HTTP 409) with the conflict details. No cascade is offered today; the super-admin must reassign or delete the dependents first.

Relationships

  • Activity (ENT-005) — referenced by activities.activities.sphere_id (NOT NULL FK). 1:N. Denormalized from primary category.
  • Category (ENT-009) — referenced by activities.categories.sphere_id (NOT NULL FK). 1:N.
  • Sphere audit log (ENT-038) — referenced by sphere_audit_log.sphere_id (on-delete set null). 1:N.

API surfaces

SurfaceExposedNotes
clientyes — /spheres (SphereClientDto, SphereCode, SphereTargetApp)Swagger UI
businessyes — /spheres (SphereAdminDto, SphereCode, SphereTargetApp)Swagger UI
super-adminyes — /spheres, /spheres/{id}, /spheres/{id}/audit (SphereSuperAdminDto, CreateSphereDto, UpdateSphereDto, SphereDeleteConflictDto, SphereTypeInUseConflictDto)Swagger UI

Spheres are platform-level taxonomy (no companyId). Only super-admins can mutate them; client and business surfaces read.

Known gotchas / open questions

  • This is one of the few entities exposed on all three surfaces with different DTOs per surface — the super-admin surface adds delete/type-in-use conflict response shapes.
  • allowedActivityTypes and defaultActivityType are stored as the activities.activity_type PG enum (converted from text / text[] via migration 0046; DINING removed by migration 0048; SHOW and MOVIE collapsed into SLOT_BASED by migration 0049). DB rejects any value outside SLOT_BASED | SERVICE with 22P02.
  • targetApp drives which Flutter app the activity ends up in (GYM_APP / TICKETS_APP / SERVICES_APP). SERVICES_APP was added in migration 0048 but is not yet bound to any sphere row — the SERVICES sphere still targets GYM_APP per the locked vertical architecture; re-mapping is deferred.
  • Renaming a code value (e.g. SPORTFITNESS) is destabilising — it is referenced across client/business/mobile code. Prefer adding new spheres over renaming.

Recommendations

Forward-looking improvements suggested while filling this doc — not currently in place.

  • Add a DB CHECK constraint that enforces defaultActivityType = ANY(allowedActivityTypes). Today the invariant lives only in the service layer; promoting it to the DB removes a class of inconsistency bugs at zero cost. Note: migration 0046 already closed the invalid enum value vector — this remaining recommendation is about the cross-column consistency rule only.
  • Add a partial UNIQUE constraint on targetApp if the one-sphere-per-app rule is meant to be hard. Today the schema allows multiple spheres targeting the same Flutter app — the rule is convention-only.
  • Define a retention/archival policy for sphere_audit_log. The table is append-only and grows indefinitely. Consider a per-year partitioning or a cold-storage export after N months.
  • Consider a “reassign and delete” workflow for sphere deletion. Currently the super-admin must manually reassign every activity and category before the DB allows the delete. A bulk re-link operation (sphere A → sphere B, then delete A) would be safer than the current “hand-curate everything first” path.
  • Document the sphere → Flutter app routing matrix in Spheres context — which sphere goes to which app, and what happens when a new sphere is added with no app to serve it.