Skip to main content
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.ts
  • apps/api/src/lib/authorization.ts
  • apps/api/src/routes/team/invites-routes.ts
  • apps/api/src/routes/team/members-routes.ts
  • apps/api/src/routes/webhooks/handlers/membership.ts
  • apps/api/src/routes/webhooks/handlers/invitation.ts
  • packages/db/src/queries/reflections.ts
  • packages/db/src/queries/memberships.ts
  • supabase/migrations/20260304190000_clerk_free_plan_role_canonicalization.sql
  • supabase/migrations/20260304193000_drop_team_ownership_transfers.sql
  • docs/runbooks/clerk-org-team-cutover.md

Decision

Adopt a strict Clerk-native free-plan model:
  1. Tenancy model: 1 reflection = 1 Clerk organization.
  2. Canonical authority: Clerk is the only write authority for organization invites, membership changes, and role changes.
  3. Role model: Free-plan defaults only:
    • org:admin
    • org:member
  4. Local projection: Postgres reflection_memberships and team_invites remain projection/cache for query performance, RLS, and auditability; they are synced from Clerk events and Clerk-backed command handlers.
  5. Creator-private behavior: owner_private access is derived from reflections.creator_user_id == current_user_id, not from a Clerk custom role.
  6. No legacy token acceptance path: Invite acceptance is Clerk-native only; legacy token endpoints are removed.
  7. Ownership transfer feature: Removed from runtime/API surface under the free-plan model; creator authority is identity-derived.
  8. 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.
  9. 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.
  10. Readiness vs sync health: sync_failed is a warning-only metadata-sync state; it does not block Team access when reflection mapping and active-org alignment are healthy.
  11. 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.
Cons:
  • 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.
Cons:
  • Adds billing dependency now.
  • Not required to satisfy current collaboration requirements.

Alternative 3: Collapse creator-private semantics into admin role

Pros:
  • Simpler conceptual model.
Cons:
  • 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.
Costs:
  • Two-role model (admin/member) is less granular than custom-role systems.
  • App must keep creator identity checks to preserve owner_private behavior.
  • 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.ts
    • apps/api/src/routes/webhooks/handlers/invitation.ts
    • apps/api/src/routes/webhooks/handlers/organization.ts
  • Replay-safe org deletion cleanup derives durable deleted_org UI state from backend truth rather than transient frontend inference.
  • Creator identity is stored in reflections.creator_user_id and used for virtual owner semantics:
    • packages/db/src/queries/reflections.ts
    • packages/db/src/queries/memberships.ts
  • DB canonicalization and cleanup migrations:
    • 20260304190000_clerk_free_plan_role_canonicalization.sql
    • 20260304193000_drop_team_ownership_transfers.sql