Organizations
What is this
An organization is Taikan’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) — Taikan creates one in Clerk on POST /organizations so Clerk’s org-membership UI (org switcher, invitations) is wired up alongside Taikan’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. - Booking policy —
cancellationWindowHours+allowLateCancellationread by the booking cancel flow,registrationOpensBeforeMinutes+registrationClosesBeforeMinutes(nullable lead times) read by the booking flow to bound when members may register, and the billing settingsbillingGraceDays(days to pay before a plan in debt is cancelled for non-payment, 0–30) /planRequiredToBookread by the payments debt rules, the subscription lifecycle and the booking billing gate (see Billing grace setting). - Tier —
platformTierenum (lite|pro|elite) drives feature gates via@taikan/shared.checkTierLimitand@RequiresFeaturedecorators. - Minisite — public marketing site for the org. Owned via
minisite_contenttable (FK fromorganizations). Seeapps/api/src/organizations/minisites.controller.ts. - Branding workspace — flag-gated Brand / Website / Mobile app workspace. Shared identity and bounded color/background defaults live in the organization appearance authority; channel-specific content and releases remain independently reviewed and published.
Relationship to other features
- users-auth — org creation calls Clerk;
clerk_organization_idstored. - memberships — org → members many-to-many through this table.
- onboarding — the setup sheet’s submit calls
POST /organizations, thenPOST /legal/consents, thenPOST /:id/onboarding-complete. - scheduling-bookings —
cancellationWindowHours+allowLateCancellationread by the booking cancel path.timezoneused to format session times. - locations — direct FK from
locations.organization_id. Each row is a space (ADR-0024 D1). - leads-crm —
organization_leads.organization_iddirect FK. - Platform tiers / billing —
platformTiercontrolsmaxSpaces,maxMembers,maxCoachesand feature flags (lead_management, etc.). Seeapps/api/src/platform-tiers/. - minisites — inherits the published shared brand unless the website has an explicit logo/content override.
Multi-org membership & the active-org switcher
A Clerk user can hold memberships in any number of orgs (the memberships
unique constraint is (user_id, organization_id)). The API never resolves a
“current org” server-side — org identity is always an explicit :orgId path
param validated by requireMembership, so any org the caller belongs to is
reachable per-request. The active org is purely a client concept:
- Selection precedence (identical on web and mobile): the explicitly
selected org if it still matches an active membership, else the
highest-privilege active membership (
owner > admin > coach > member). - Web: selection persisted in the
taikan_active_orgcookie (apps/web/src/lib/active-org.ts), read by the protected layout /prefetchUserAndDataon the server and byActiveOrgProvider→useCurrentUseron the client. Switcher UI:OrgSwitcher(components/shared/org-switcher.tsx) wraps the org block in the dashboard sidebar and the member header; it renders only when the user has 2+ active memberships. Switching sets the cookie and hard-navigates to the target role’s home, which resets query cache, realtime, and analytics grouping. - Mobile: selection persisted in AsyncStorage
(
src/lib/settings-store.tsactiveOrgId), preloaded behind the splash and held inActiveOrgProvider;OrgSwitcher(src/components/fk/org-switcher.tsx) makes the member-header org block tappable. Switching invalidates the whole React Query cache (all org-scoped keys embed the orgId). Cleared on sign-out. - Second-org invitations:
GET /users/meconvertspending_invitationshells viaacceptPendingInvitationseven when the user already has an active membership elsewhere;createInvitationre-arms a cancelled/suspended membership row back topending_invitationon re-invite.
Current status
Shipped. Areas with active churn:
OrganizationsService.createonly creates the org + owner membership. Starter programs are seeded bycompleteOnboarding: one per entry in the request’sdeliveryModes(named fromprogramNames[mode]or a per-locale default, skipped when the org already has a program of that mode), plus a default class type for ascheduleprogram that has none. The onboarding setup sheet sends that request right afterPOST /organizations(see onboarding).- 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).