Organizations

Multi-tenant team system with role-based access control, invitations, and ownership transfer.

LaunchFst includes a full multi-tenant organization system. Users can create organizations, invite members, assign roles, and transfer ownership. All permissions are enforced through RBAC helpers in lib/rbac.ts.

Data Model

The organization system uses three Prisma models:

  • Organization — Has a unique slug, name, and optional logo.
  • OrganizationMember — Links a user to an organization with a role. Unique on [userId, organizationId].
  • OrgInvitation — Stores pending invitations with a unique token, expiry date, and status (PENDING, ACCEPTED, EXPIRED, REVOKED).
enum OrgRole {
  OWNER
  ADMIN
  MEMBER
}

Each user has an activeOrgId field stored in the JWT token, allowing organization-scoped data fetching without extra lookups.

Creating an Organization

Use the useCreateOrganization() hook from hooks/core/use-organizations.ts:

const createOrg = useCreateOrganization()

createOrg.mutate({ name: "Acme Inc", slug: "acme-inc" })

The API creates the organization and adds the creator as the OWNER. The slug must be unique and URL-safe.

Inviting Members

Only OWNER and ADMIN roles can invite new members:

const invite = useInviteMember(orgId)

invite.mutate({ email: "colleague@example.com", role: "MEMBER" })

This creates an OrgInvitation record with a unique token and sends an invitation email (if email is configured). The invitation expires after 7 days.

Accepting Invitations

The useAcceptInvitation() hook processes invitation tokens:

const accept = useAcceptInvitation()

accept.mutate(token) // token from the invitation link

On acceptance, the invitation status changes to ACCEPTED and the user becomes an OrganizationMember with the role specified in the invitation.

Changing Roles

Only the OWNER can change member roles:

const changeRole = useChangeRole(orgId)

changeRole.mutate({ memberId: "member-id", role: "ADMIN" })

Transferring Ownership

Only the current OWNER can transfer ownership to another member:

const transfer = useTransferOwnership(orgId)

transfer.mutate("new-owner-user-id")

This promotes the target member to OWNER and demotes the current owner to ADMIN.

RBAC Helpers

All permission checks live in lib/rbac.ts. Use these in API routes to enforce access control:

import { getUserOrgRole, requireOrgRole, canManageOrg } from "@/lib/rbac"

// Get the user's role in an org (returns null if not a member)
const role = await getUserOrgRole(userId, orgId)

// Throw if user doesn't have one of the allowed roles
const role = await requireOrgRole(userId, orgId, ["OWNER", "ADMIN"])

// Boolean checks
canManageOrg(role)         // OWNER or ADMIN
canInviteMembers(role)     // OWNER or ADMIN
canRemoveMembers(actor, target) // OWNER can remove anyone except OWNER; ADMIN can remove MEMBER only
canChangeRoles(role)       // OWNER only
canDeleteOrg(role)         // OWNER only
canTransferOwnership(role) // OWNER only
Always check permissions server-side in API routes using the RBAC helpers. Client-side checks are for UI convenience only and should never be the sole enforcement mechanism.

Disabling Organizations

If you do not need multi-tenancy, set the feature flag in your environment:

NEXT_PUBLIC_ENABLE_ORGANIZATIONS="false"

This hides the organization UI from the dashboard sidebar and navigation. The database tables remain but are unused.

Demo Mode — Explore freely. Some actions are restricted. demo@launchfst.dev / demo1234

Get LaunchFst →