For the complete documentation index, see llms.txt. Prefer markdown by appending .md to documentation URLs or sending Accept: 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

  1. Add a dedicated /dashboard/create-organization route (no product sidebar).
  2. Guard product routes: no memberships → create-organization; memberships → redirect /dashboard to an org slug.
  3. Hide the personal row in AccountSwitcher.
  4. Point leave / delete / invalid-slug redirects at another org or create-organization (never personal home).
  5. Keep product billing on organization.id. Optionally auto-create an org on first signup instead of prompting.

Default vs organizations-only

AreaDefault (hybrid)Organizations-only
Post-signup land/dashboard (personal)Create org, or first membership
Account switcherPersonal + organizationsOrganizations only
Product data / billinguser.id or organization.idPrefer organization.id
Leave / delete last orgBack to personal homeCreate org (or block leaving the last one)
Invalid org slugRedirect to personal homeFirst 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:

StrategyUXWhen to use
Prompt createAfter signup, full-page form: name → slug → /dashboard/{slug}You want the user to pick a company / workspace name
Auto-createSignup creates an org from the user name (or email local-part), then lands on that slugYou 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

apps/web/src/config/paths.ts
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.

apps/web/src/lib/auth/server.ts
export const listOrganizations = cache(async () => {
  try {
    return await auth.api.listOrganizations({
      headers: await getHeaders(),
    });
  } catch {
    return [];
  }
});
apps/web/src/app/[locale]/dashboard/(user)/(app)/layout.tsx
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:

apps/web/src/app/[locale]/dashboard/(user)/(app)/page.tsx
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 (getSlugauthClient.organization.createpathsConfig.dashboard.organization(slug).index). Extract the form into a shared component, then render it full-page:

apps/web/src/app/[locale]/dashboard/(user)/create-organization/page.tsx
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:

LocationDefault behaviorOrganizations-only
[organization]/layout.tsx when slug is missingRedirect to personal homelistOrganizations() → first slug, else create-organization
leave-organization.tsx onSuccessPersonal homeNext membership slug, else create-organization
delete-organization.tsx onSuccessPersonal homeSame as leave
Member remove / self-leave in the members tablePersonal homeSame as leave
Invitation error CTAsPersonal homecreate-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/billing if 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:

  1. Client after register success - call the same getSlug + organization.mutations.create path used by the modal, then router.replace to the slug.
  2. Server after first authenticated dashboard hit - if listOrganizations() is empty, call auth.api.createOrganization with 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

ScenarioExpected
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 membershipsRedirect to active or first org
Open /dashboard with zero membershipscreate-organization
Accept inviteOrg dashboard; skip create
Leave / delete when others remainNext org slug
Leave / delete last orgcreate-organization (or blocked)
Invalid org slugFirst membership or create-organization
Checkout / plan gatesUse organization referenceId

Checklist

  • pathsConfig.dashboard.user.createOrganization registered
  • Create page outside the product sidebar shell
  • (app) layout redirects when listOrganizations() is empty
  • /dashboard home 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.

How is this guide?

Last updated on

On this page

Ship your startup everywhere. In minutes.Try TurboStarter