Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesOrganizationsOrganizations

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

PersonaWhy
OwnerCreates the org during onboarding; edits branding, contact, timezone, cancellation policy, uploads logo.
AdminEdits org settings (same as owner except destructive operations).
CoachRead-only on most fields.
MemberReads name, logo, timezone for display.
PlatformTier 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

  1. Create orgPOST /organizations. Also creates the matching Clerk organization (with createdBy=clerkId so the caller is admin in Clerk too). Inserts an owner-role memberships row.
  2. List my orgsGET /organizations — every org where the caller has an active membership.
  3. Get / update — owner/admin only.
  4. Logo flowPOST /:id/logo/upload-url returns a presigned R2 PUT URL + R2 key; POST /:id/logo/confirm validates that the object exists and persists the key to organizations.logo_url.
  5. Onboarding completePOST /:id/onboarding-complete (owner only). Optionally creates a class type, batch-invites members, and sends a templated welcome email.
  6. Cancellation policycancellationWindowHours + allowLateCancellation flags read by booking cancel flow.
  7. TierplatformTier enum (lite|pro|elite) drives feature gates via @fitkit/shared.checkTierLimit and @RequiresFeature decorators.
  8. Minisite — public marketing site for the org. Owned via minisite_content table (FK from organizations). See apps/api/src/organizations/minisites.controller.ts.

Relationship to other features

  • users-auth — org creation calls Clerk; clerk_organization_id stored.
  • memberships — org → members many-to-many through this table.
  • onboarding — the wizard ends with POST /organizations then POST /:id/onboarding-complete.
  • scheduling-bookingscancellationWindowHours + allowLateCancellation read by the booking cancel path. timezone used to format session times.
  • locations — direct FK from locations.organization_id.
  • leads-crmorganization_leads.organization_id direct FK.
  • Platform tiers / billingplatformTier controls maxLocations, maxMembers, feature flags (lead_management, etc.). See apps/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.create itself only creates the org + owner membership; the wizard’s handleFinish in onboarding-content.tsx only POSTs /organizations, no program creation visible). The onboardingComplete endpoint reads organizations and 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 logoUrl values that are full URLs (e.g. from Clerk imageUrl) are passed through. Relative keys starting with orgs/ 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).