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):
code | target_app | allowed_activity_types | default_activity_type | sort_order |
|---|---|---|---|---|
SPORT | GYM_APP | [SLOT_BASED, SERVICE] | SLOT_BASED | 0 |
EVENTS | TICKETS_APP | [SLOT_BASED, SERVICE] | SLOT_BASED | 1 |
SERVICES | GYM_APP | [SLOT_BASED, SERVICE] | SERVICE | 2 |
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, defaultgen_random_uuid()). code(text, UNIQUE, NOT NULL) — current values areSPORT | EVENTS | SERVICES. TheDININGrow was removed by migration0048_clear_tattoo.sql— see ADR sphere-targetapp-enum-cleanup-drop-dining-add-services.CINEMAandSHOWSrows were consolidated into a singleEVENTSrow by migration0049_worried_rattler.sql— see ADR activity-model-simplification.name(jsonb, NOT NULL) — multi-language{ uk, en, ru, de, fr }.icon(nullable text).targetApp(enumsphere_target_app:GYM_APP,TICKETS_APP,SERVICES_APP).DINING_APPwas removed andSERVICES_APPadded by migration0048_clear_tattoo.sql— see ADR sphere-targetapp-enum-cleanup-drop-dining-add-services. TheSERVICES_APPvalue is present in the enum but is not yet assigned to any sphere row — theSERVICESsphere still targetsGYM_APP(re-mapping is a separate ticket).allowedActivityTypes(activity_type[], NOT NULL) — sphere-level allowlist ofactivity_typeenum values (SLOT_BASED | SERVICE). Post-869dpxbj6every sphere carries[SLOT_BASED, SERVICE]uniformly.defaultActivityType(activity_typeenum, NOT NULL).sortOrder(integer, NOT NULL, default0).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
codeis UNIQUE (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/activities.schema.ts).code,name,targetApp,allowedActivityTypes,defaultActivityType,sortOrder,createdAtNOT NULL (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/activities.schema.ts).defaultActivityTypeandallowedActivityTypes[]are constrained to theactivities.activity_typePG enum domain (SLOT_BASED | SERVICE). Any other value is rejected at DB level with Postgres error22P02(enforced by migration 0046 — see specs/sphere-column-type-safety.md;DININGremoved in migration 0048 — see specs/sphere-targetapp-enum-cleanup-drop-dining-add-services.md;SHOWandMOVIEremoved 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_idNOT 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.defaultActivityTypemust be a value present inallowedActivityTypes. 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
targetAppvalue. - 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
| Surface | Exposed | Notes |
|---|---|---|
| client | yes — /spheres (SphereClientDto, SphereCode, SphereTargetApp) | Swagger UI |
| business | yes — /spheres (SphereAdminDto, SphereCode, SphereTargetApp) | Swagger UI |
| super-admin | yes — /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.
allowedActivityTypesanddefaultActivityTypeare stored as theactivities.activity_typePG enum (converted fromtext/text[]via migration 0046;DININGremoved by migration 0048;SHOWandMOVIEcollapsed intoSLOT_BASEDby migration 0049). DB rejects any value outsideSLOT_BASED | SERVICEwith22P02.targetAppdrives which Flutter app the activity ends up in (GYM_APP/TICKETS_APP/SERVICES_APP).SERVICES_APPwas added in migration 0048 but is not yet bound to any sphere row — theSERVICESsphere still targetsGYM_APPper the locked vertical architecture; re-mapping is deferred.- Renaming a
codevalue (e.g.SPORT→FITNESS) 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
targetAppif 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.