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 linkOn 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 onlyDisabling 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.