Organizations
What is this
An organization is FitKit’s tenant unit — a single studio, gym, coach business, or chain. Every domain row (sessions, bookings, members, leads, plans, locations, programs, workouts, payments) is tied back to exactly one organizations.id. The org row holds branding (logoUrl, name, slug, description), localization (timezone, currency), commercial state (platformTier), and a few cross-cutting policies (cancellationWindowHours, allowLateCancellation).
Organizations are also mirrored into Clerk as Clerk organizations (clerk_organization_id) — FitKit creates one in Clerk on POST /organizations so Clerk’s org-membership UI (org switcher, invitations) is wired up alongside FitKit’s memberships rows.
Who uses it
| Persona | Why |
|---|---|
| Owner | Creates the org during onboarding; edits branding, contact, timezone, cancellation policy, uploads logo. |
| Admin | Edits org settings (same as owner except destructive operations). |
| Coach | Read-only on most fields. |
| Member | Reads name, logo, timezone for display. |
| Platform | Tier transitions (platform_tier) driven by platform-billing module. |
Persona impact
- Owner-only:
completeOnboarding(only the owner can finalize the org setup flow). Suspending or cancelling the last owner membership is blocked. - Logo uploads are two-step: presigned R2 upload URL + confirm. Lite tier has a max upload size of 2 MB and limited MIME types (
image/jpeg,image/png,image/webp). 10-minute upload window. - Slug auto-generated on create from name + 6-char random suffix; unique. Currently no slug-edit endpoint.
High-level capabilities
- Create org —
POST /organizations. Also creates the matching Clerk organization (withcreatedBy=clerkIdso the caller is admin in Clerk too). Inserts an owner-rolemembershipsrow. - List my orgs —
GET /organizations— every org where the caller has anactivemembership. - Get / update — owner/admin only.
- Logo flow —
POST /:id/logo/upload-urlreturns a presigned R2 PUT URL + R2 key;POST /:id/logo/confirmvalidates that the object exists and persists the key toorganizations.logo_url. - Onboarding complete —
POST /:id/onboarding-complete(owner only). Optionally creates a class type, batch-invites members, and sends a templated welcome email. - Cancellation policy —
cancellationWindowHours+allowLateCancellationflags read by booking cancel flow. - Tier —
platformTierenum (lite|pro|elite) drives feature gates via@fitkit/shared.checkTierLimitand@RequiresFeaturedecorators. - Minisite — public marketing site for the org. Owned via
minisite_contenttable (FK fromorganizations). Seeapps/api/src/organizations/minisites.controller.ts.
Relationship to other features
- users-auth — org creation calls Clerk;
clerk_organization_idstored. - memberships — org → members many-to-many through this table.
- onboarding — the wizard ends with
POST /organizationsthenPOST /:id/onboarding-complete. - scheduling-bookings —
cancellationWindowHours+allowLateCancellationread by the booking cancel path.timezoneused to format session times. - locations — direct FK from
locations.organization_id. - leads-crm —
organization_leads.organization_iddirect FK. - Platform tiers / billing —
platformTiercontrolsmaxLocations,maxMembers, feature flags (lead_management, etc.). Seeapps/api/src/platform-tiers/.
Current status
Shipped. Areas with active churn:
- The org-create flow creates a default program implicitly elsewhere (TODO: verify which module —
OrganizationsService.createitself only creates the org + owner membership; the wizard’shandleFinishinonboarding-content.tsxonly POSTs/organizations, no program creation visible). TheonboardingCompleteendpoint readsorganizationsand tries to find an existing program when creating a class type — implying programs are created elsewhere, possibly via a default-seeding step or a separate API call from the web app. (TODO: verify.) - Logo upload is R2-only; legacy
logoUrlvalues that are full URLs (e.g. from Clerk imageUrl) are passed through. Relative keys starting withorgs/get presigned for read.
Known gaps
- No “transfer ownership” endpoint. Updating an owner’s role to non-owner is guarded against if they’re the last owner.
- No org soft-delete path exposed in the controller. The schema has
deleted_at, the service does not write it. - Slug is auto-generated; no edit. If a name collision happens after slugification, the 6-char suffix makes it unique but the URL changes if the org is recreated.
- Currency is a plain
varchar(3)— no validation against ISO 4217 codes at the API layer (TODO: verify whether Zod schema enforces this).