Status: Accepted Date: 2026-03-04 Deciders: Reflections Maintainers
Context
Reflection collaboration needs first-class team sharing with reliable invitation lifecycle, role management, and identity resolution. The prior model mixed Clerk-authenticated users with a mostly custom invite/membership stack, increasing operational and security surface area. At the same time, production is on Clerk free plan, where custom org roles/permissions are not available for production instances. The architecture must therefore avoid paid-only role features while keeping access control strict. Evidence in code/config:apps/api/src/lib/clerk-organizations.tsapps/api/src/lib/authorization.tsapps/api/src/routes/team/invites-routes.tsapps/api/src/routes/team/members-routes.tsapps/api/src/routes/webhooks/handlers/membership.tsapps/api/src/routes/webhooks/handlers/invitation.tspackages/db/src/queries/reflections.tspackages/db/src/queries/memberships.tssupabase/migrations/20260304190000_clerk_free_plan_role_canonicalization.sqlsupabase/migrations/20260304193000_drop_team_ownership_transfers.sqldocs/runbooks/clerk-org-team-cutover.md
Decision
Adopt a strict Clerk-native free-plan model:- Tenancy model:
1 reflection = 1 Clerk organization. - Canonical authority: Clerk is the only write authority for organization invites, membership changes, and role changes.
- Role model: Free-plan defaults only:
org:adminorg:member
- Local projection: Postgres
reflection_membershipsandteam_invitesremain projection/cache for query performance, RLS, and auditability; they are synced from Clerk events and Clerk-backed command handlers. - Creator-private behavior:
owner_privateaccess is derived fromreflections.creator_user_id == current_user_id, not from a Clerk custom role. - No legacy token acceptance path: Invite acceptance is Clerk-native only; legacy token endpoints are removed.
- Ownership transfer feature: Removed from runtime/API surface under the free-plan model; creator authority is identity-derived.
- Dashboard runtime: Team management runs in Clerk-backed dashboard surfaces only; readiness is derived from the live selected reflection plus the active Clerk org after hydration.
- Shipped team permissions: Owners and admins can manage members and invites in the shipped Team workspace; destructive organization actions remain disabled or hidden until reflection/org lifecycle symmetry is explicitly implemented.
- Readiness vs sync health:
sync_failedis a warning-only metadata-sync state; it does not block Team access when reflection mapping and active-org alignment are healthy. - Deterministic handoff: Team links and native handoffs carry reflection identity (and canonical Clerk org identity when available) so the Team route can switch or hydrate the intended workspace deterministically.
Alternatives Considered
Alternative 1: Keep custom owner/admin/operator/viewer stack as canonical
Pros:
- Maximum in-app role flexibility.
- Re-implements invite lifecycle and membership authority outside Clerk.
- Larger authz and sync surface; higher drift risk.
- Lower leverage of Clerk’s native org identity/invite model.
Alternative 2: Upgrade plan and use custom Clerk roles immediately
Pros:- Rich permission model in Clerk.
- Adds billing dependency now.
- Not required to satisfy current collaboration requirements.
Alternative 3: Collapse creator-private semantics into admin role
Pros:- Simpler conceptual model.
- Breaks product requirement that creator-private content remains creator-only.
Consequences
Benefits:- Fully Clerk-native authority for invites/memberships/roles.
- Free-plan-compatible production posture with no paid-role dependency.
- Clear separation between identity authority (Clerk) and data scoping authority (DB + creator identity + RLS).
- Reduced dead-path complexity by removing ownership-transfer runtime.
- Two-role model (
admin/member) is less granular than custom-role systems. - App must keep creator identity checks to preserve
owner_privatebehavior. - Legacy token acceptance internals are removed entirely; in-app team operations rely on Clerk UI/components.
Implementation Notes
- Clerk role mapping is normalized in
apps/api/src/lib/clerk-organizations.ts. - Authorization remains server-side and server-derived in
apps/api/src/lib/authorization.ts. - Team membership/invite projection is webhook-driven from Clerk events.
- Team UX uses Clerk-hosted organization management components.
- Dashboard Team UX uses a product-owned shell around Clerk-backed team management, with owner/admin management capability, deterministic invalid-mapping recovery, and explicit non-destructive policy copy.
- Webhook handlers keep projection eventually consistent and replay-safe:
apps/api/src/routes/webhooks/handlers/membership.tsapps/api/src/routes/webhooks/handlers/invitation.tsapps/api/src/routes/webhooks/handlers/organization.ts
- Replay-safe org deletion cleanup derives durable
deleted_orgUI state from backend truth rather than transient frontend inference. - Creator identity is stored in
reflections.creator_user_idand used for virtual owner semantics:packages/db/src/queries/reflections.tspackages/db/src/queries/memberships.ts
- DB canonicalization and cleanup migrations:
20260304190000_clerk_free_plan_role_canonicalization.sql20260304193000_drop_team_ownership_transfers.sql

