For the complete documentation index, see llms.txt. Prefer markdown by appending.mdto documentation URLs or sendingAccept: text/markdown.
Disable personal accounts
Turn off personal workspaces and require users to create or join an organization before they can use the TurboStarter web dashboard.
By default, TurboStarter is hybrid: every user gets a personal workspace at /dashboard, and organizations are optional under /dashboard/[slug]. That matches products like GitHub (personal + teams).
Many B2B products need the opposite: no personal workspace. Every signed-in user must create an organization or accept an invitation before they can use the product. Billing, data, and the account switcher all stay organization-scoped.
Need the opposite (B2C, no teams)? See Disable organizations.
TL;DR
- Add a dedicated
/dashboard/create-organizationroute (no product sidebar). - Guard product routes: no memberships → create-organization; memberships → redirect
/dashboardto an org slug. - Hide the personal row in
AccountSwitcher. - Point leave / delete / invalid-slug redirects at another org or create-organization (never personal home).
- Keep product billing on
organization.id. Optionally auto-create an org on first signup instead of prompting.
Default vs organizations-only
| Area | Default (hybrid) | Organizations-only |
|---|---|---|
| Post-signup land | /dashboard (personal) | Create org, or first membership |
| Account switcher | Personal + organizations | Organizations only |
| Product data / billing | user.id or organization.id | Prefer organization.id |
| Leave / delete last org | Back to personal home | Create org (or block leaving the last one) |
| Invalid org slug | Redirect to personal home | First membership or create org |
Use this when your product is seat-based or always shared (project tools, team chat, B2B admin). Stay hybrid when freelancers should start solo and invite later.
Ship this before production data accumulates
Switching to organizations-only after users already created personal data means migrating or abandoning that data. Decide before launch. Pair with Onboarding if you also collect profile answers in the same first-run flow.
Prompt create vs auto-create
Two common patterns:
| Strategy | UX | When to use |
|---|---|---|
| Prompt create | After signup, full-page form: name → slug → /dashboard/{slug} | You want the user to pick a company / workspace name |
| Auto-create | Signup creates an org from the user name (or email local-part), then lands on that slug | You want zero friction; rename later in settings |
Invitees who already accepted a membership should skip create and go straight to that organization.
Register a create-organization path
dashboard: {
user: {
index: DASHBOARD_PREFIX,
createOrganization: `${DASHBOARD_PREFIX}/create-organization`,
// ...settings, ai
},
organization: (slug: string) => ({ /* unchanged */ }),
},Use this constant in layout redirects, auth redirectTo / callbackURL, and leave/delete success handlers.
Split the personal dashboard so create has no product shell
Today (user)/layout.tsx always mounts the sidebar and prefetches personal billing (referenceId: user.id). For organizations-only, keep create-organization (and account settings if you want) outside the product shell.
dashboard/(user)/
layout.tsx ← session required only
create-organization/page.tsx ← full-page create form
settings/... ← optional: account security without an org
(app)/
layout.tsx ← membership guard + sidebar
page.tsx ← redirect to an org (see next step)
ai/...Move the existing sidebar layout into (app)/layout.tsx. Parent (user)/layout.tsx only checks the session (same pattern as the onboarding recipe).
Guard access = require at least one membership
List the user's organizations with Better Auth, then redirect.
export const listOrganizations = cache(async () => {
try {
return await auth.api.listOrganizations({
headers: await getHeaders(),
});
} catch {
return [];
}
});import { redirect } from "next/navigation";
import { pathsConfig } from "~/config/paths";
import { getSession, listOrganizations } from "~/lib/auth/server";
export default async function AppShellLayout({
children,
}: {
children: React.ReactNode;
}) {
const { user } = await getSession();
if (!user) {
return redirect(pathsConfig.auth.login);
}
const organizations = await listOrganizations();
if (organizations.length === 0) {
return redirect(pathsConfig.dashboard.user.createOrganization);
}
return /* existing SidebarProvider + children */;
}On the personal home page, never render a personal product surface. Send people into an organization:
import { redirect } from "next/navigation";
import { pathsConfig } from "~/config/paths";
import { getSession, listOrganizations } from "~/lib/auth/server";
export default async function UserDashboardPage() {
const { session } = await getSession();
const organizations = await listOrganizations();
if (organizations.length === 0) {
return redirect(pathsConfig.dashboard.user.createOrganization);
}
const activeId = session?.activeOrganizationId;
const active =
organizations.find((organization) => organization.id === activeId) ??
organizations[0];
return redirect(pathsConfig.dashboard.organization(active.slug).index);
}Do the same for marketing CTAs and auth success URLs that currently point at pathsConfig.dashboard.user.index. After login, /dashboard still works: it immediately redirects to an org slug.
Build the create-organization page
Reuse the existing create flow in apps/web/src/modules/organization/create-organization.tsx (getSlug → authClient.organization.create → pathsConfig.dashboard.organization(slug).index). Extract the form into a shared component, then render it full-page:
import { CreateOrganizationForm } from "~/modules/organization/create-organization-form";
export default function CreateOrganizationPage() {
return (
<div className="mx-auto flex min-h-[80vh] max-w-lg flex-col justify-center gap-8 px-4">
<CreateOrganizationForm />
</div>
);
}Keep the modal for creating additional organizations from the switcher. The page is only for users with zero memberships (or for a first-run step inside onboarding).
If the user already has memberships and hits this URL, redirect them to their active (or first) organization so they cannot get stuck.
Hide the personal account in the switcher
In apps/web/src/modules/organization/account-switcher.tsx, remove the personal-account CommandItem and the separator above the organizations list. The trigger should always show the active organization (never t("account.personal")).
Also stop treating "no active organization" as a valid product state in that component: if activeOrganization is missing but organizations is non-empty, navigate to the first org slug (the URL remains the source of truth; see Active organization).
Fix redirects that still assume a personal home
Search the web app for pathsConfig.dashboard.user.index in organization flows and replace the fallbacks:
| Location | Default behavior | Organizations-only |
|---|---|---|
[organization]/layout.tsx when slug is missing | Redirect to personal home | listOrganizations() → first slug, else create-organization |
leave-organization.tsx onSuccess | Personal home | Next membership slug, else create-organization |
delete-organization.tsx onSuccess | Personal home | Same as leave |
| Member remove / self-leave in the members table | Personal home | Same as leave |
| Invitation error CTAs | Personal home | create-organization or login |
Example leave success handler:
onSuccess: async () => {
const { data } = await refetch();
const next = data?.[0];
toast.add({ title: t("leave.success"), type: "success" });
router.replace(
next
? pathsConfig.dashboard.organization(next.slug).index
: pathsConfig.dashboard.user.createOrganization,
);
},Optional product rule: block leaving or deleting the last organization (always keep one workspace). TurboStarter already has canLeave helpers for owner/seat cases; extend that check for "last membership".
Scope billing and product data to organizations
Personal billing in (user) layouts uses referenceId: user.id. In organizations-only mode:
- Prefer the org layout billing prefetch (
referenceId: activeOrganization.id) for plan gates and checkout. - Hide or remove
/dashboard/settings/billingif you no longer sell personal plans. - Gate features with the active organization's summary, same rules as Feature-based access.
New tenant-owned tables should store organizationId, not userId, as the ownership key. User id still belongs on membership and audit columns.
Handle invites and post-auth redirects
Invitation accept already creates a membership. After accept, send the user to pathsConfig.dashboard.organization(slug).index, not personal home.
For new self-serve signups (no invitationId), point redirectTo / callbackURL at create-organization (or onboarding that includes the create step). Returning users with memberships can keep /dashboard and rely on the home redirect.
// register / login success for users without an invitation
redirectTo={pathsConfig.dashboard.user.createOrganization}Join / invite query params already flow through apps/web/src/app/[locale]/auth/register/page.tsx and friends. Do not force create-organization when invitationId is present.
Optional: auto-create an organization on signup
If you want MakerKit-style organizations-only (no create screen), create the org as soon as the user exists, then set it active and redirect to its slug.
Practical places to do that:
- Client after register success - call the same
getSlug+organization.mutations.createpath used by the modal, thenrouter.replaceto the slug. - Server after first authenticated dashboard hit - if
listOrganizations()is empty, callauth.api.createOrganizationwith a derived name, then redirect.
Sketch for a server helper:
import { auth } from "@workspace/auth/server";
// reuse packages/api/src/modules/organization/queries/generate-slug.ts
// or call the same slug endpoint the create modal uses
export async function ensureDefaultOrganization(user: {
id: string;
name: string;
email: string;
}) {
const existing = await auth.api.listOrganizations({
headers: await getHeaders(),
});
if (existing.length > 0) {
return existing[0];
}
const name = user.name.trim() || user.email.split("@")[0] || "Workspace";
const { slug } = await generateSlug(name);
return auth.api.createOrganization({
body: { name, slug },
headers: await getHeaders(),
});
}Call it from the membership guard instead of redirecting to create-organization. Still offer rename in organization settings.
Better Auth options
The organization plugin also supports allowUserToCreateOrganization, organizationLimit, and related options in packages/auth/src/server.ts. Those gate who may create orgs; they do not replace the UI/route work above for disabling personal workspaces.
Verify the matrix
| Scenario | Expected |
|---|---|
| New signup (no invite) | Lands on create-organization (or auto-created org slug) |
| Completes create | /dashboard/{slug}; switcher has no personal row |
Open /dashboard with memberships | Redirect to active or first org |
Open /dashboard with zero memberships | create-organization |
| Accept invite | Org dashboard; skip create |
| Leave / delete when others remain | Next org slug |
| Leave / delete last org | create-organization (or blocked) |
| Invalid org slug | First membership or create-organization |
| Checkout / plan gates | Use organization referenceId |
Checklist
-
pathsConfig.dashboard.user.createOrganizationregistered - Create page outside the product sidebar shell
-
(app)layout redirects whenlistOrganizations()is empty -
/dashboardhome redirects to an org slug - Personal row removed from
AccountSwitcher - Leave, delete, and invalid-slug fallbacks updated
- Auth / marketing redirects no longer assume a personal product home
- Billing and tenant data keyed by organization
- Invitees skip create
Other platforms
Mobile and extension also expose personal vs organization switching. Apply the same rules there: hide personal workspace entry points, require a membership before product screens, and land leave/delete on another org or a create flow. Reuse @workspace/auth APIs; only the navigation shells differ.
Organizations overview
Multi-tenancy model and Better Auth plugin.
Active organization
URL slug vs session activeOrganizationId.
Onboarding flow
Combine create-org with a first-run wizard.
Disable organizations
Opposite mode: personal accounts only.
Feature-based access
Gate features with organization billing.
Better Auth organization plugin
better-auth.com
How is this guide?
Last updated on