Organizations
Organizations are the multi-tenant boundary in every starter. Users belong to organizations, invitations bring new
members in, and API routes scope work to the active organizationId on the request context.
The free auth starter has the same model underneath but hides it:
every account gets one organization named Personal workspace at creation, and there is no switcher, no members page,
and no way to send an invitation. That is what lets every feature pack, all of which scope data to the active
organization, install on it unchanged. Swap to a SaaS starter later and the personal workspace becomes the user's first
team.
Credential-free setup notes live on the starter pack pages. This page is the domain model and code paths.
What ships with a starter
| Concept | Role |
|---|---|
| Organization | Tenant boundary for data and settings |
| Membership + role | Ties a user to an org with a permission preset (admin, member, ...) |
| Invitation | Email invite into an organization |
| Permissions (CASL) | Server-side hasAccess checks mirrored in the UI |
| Onboarding | First-time flow to create or join an org before the private app |
Where the code lives
| Piece | Path pattern |
|---|---|
| Organization features | apps/backend/src/features/organizations/modules/ |
| Organization routes | apps/backend/src/routes/organizations/ |
| Invitation sockets | apps/backend/src/sockets/invitations/ |
| Permission presets / subjects | packages/enums (or the enums package your stack installs) |
hasAccess middleware | apps/backend/src/middleware/permissions/has-access.ts |
| Frontend org settings | apps/frontend/src/features/settings/modules/organization-tab/ |
| Onboarding routes | apps/frontend/src/routes/onboarding/ |
WorkOS vs Better Auth membership storage
| WorkOS starter | Better Auth starter | |
|---|---|---|
| Memberships | WorkOS organization memberships API | Local Prisma Member + Organization models |
| Invitations | Local OrganizationInvitation plus email | Same invitation feature modules; Better Auth's own Invitation model is unused by the app flow |
| Org settings / logos | Local Prisma + storage uploads | Same |
Both starters expose the same product UX: create org, invite members, manage roles, enforce hasAccess on routes.
Permission flow
From session to route gate
withAuthUser
Resolves the signed-in user and active organization membership.
resolvePermissionsForMembership
Loads the role permissions for that membership (WorkOS API or Prisma, depending on starter).
buildAbilityFromPermissions
Turns the permission list into a CASL ability on the Hono context.
hasAccess({ action, subject })
Route middleware denies the request when ability.can fails.
When you add a new domain (for example teams or projects), register subjects and grant them on the right roles. The teams pack notes that you may need to add CASL subjects manually after install.
Invitations
Organization invitation modules handle send, accept, and resend. Accepting an invite attaches the user to the org and continues into the private app. Email delivery needs Resend configured; see Mailing.
Feature flags
The two SaaS starters ship per-organization feature flags. Flags are defined in code, in
packages/enums/src/feature-flags/catalog.ts, so a new flag is a typed key reviewed in a pull request, not a row
someone typed into a dashboard:
export const FEATURE_FLAGS = [
{
key: "beta-reports",
name: "Reports",
description: "The new reporting screens",
kind: FeatureFlagKind.BETA,
defaultEnabled: false,
},
] as const satisfies readonly FeatureFlagDefinition[];The catalog ships empty. The database stores only per-organization overrides (OrganizationFeatureFlag), and resolution
is one pure function: an override wins, otherwise the default, and a key no longer in the catalog is ignored. Retiring a
flag is deleting its entry, with no migration.
| Piece | Where |
|---|---|
| Resolved flags API | GET /organizations/feature-flags |
| Backend gate | withFeature("beta-reports") middleware, or isFeatureEnabled inside a module |
| Frontend gate | useFeatureFlag("beta-reports") or <FeatureGate flag="beta-reports"> in features/feature-flags/, same on mobile and the extension |
| Admin | A Flags tab on every organization's detail page, with set and reset per flag |
There are no percentage rollouts and no per-user targeting. The unit is the organization, which is also the billing unit, so an entitlement flag and a plan line up. The free auth starter does not ship flags.
The admin org page is extensible the same way: feature packs add tabs next to Flags through the orgDetailsTabs
manifest field. The AI chat pack uses it for its per-organization usage tab.
Teams pack relationship
pack-teams adds teams inside an organization (Team, TeamMember, team
invitations). It does not replace org membership. Org role still gates who can manage teams; team membership is a second
layer for collaboration inside one tenant.
pack-projects is the same idea for projects and project members.
Practical guidance
- Scope queries by
organizationIdfrom context. Do not trust a client-supplied org id without membership checks. - Keep new permission subjects in the shared enums package so frontend and backend stay aligned.
- For admin-wide views across all orgs, use the admin app, not the customer frontend.
Related
Every purchase and sponsorship funds my 8+ years of work on open source given freely to the community. It keeps the lights on, funds new packs, and keeps the ecosystem alive. Even a small tier means a lot. Thank you!
