Guides
Build guide

How to create a default workspace on signup in Next.js, but not for invited users

Create a default workspace on signup in Next.js without giving invited users an empty one of their own, and decide when to ask for a name instead.

Dharmendra Jagodana11 min read

In short

Auto-create the first workspace only for users nobody invited. BuildBase makes one the first time a user lists workspaces, which suits a solo app. In a team app, turn auto-create off, show pending invitations first, and ask everyone else for a name: the workspace owns the trial and the Stripe customer.

"Create a workspace when someone signs up" is a rule that cannot see invitations.

Your customer invites a colleague. The colleague follows the link, registers, and the rule fires. Now they have two workspaces: the team's, and an empty one with their own name on it. That second one holds a trial nobody has used, it counts toward the workspace ceiling on your plan, and it sits in their switcher until somebody deletes it.

Both behaviors ship in products you have probably used. Murf's help center says that on its Enterprise plan, an invitee who signs up through the link joins the inviter's workspace and also gets "a separate free workspace of your own". Screenly's docs say the opposite: "No new workspace is created for you and no trial starts." Neither is a bug. They are two answers to a question most "create a default workspace on signup" tutorials skip: who is the first workspace for?

Here is mine. Auto-create the first workspace only for the user nobody invited. In a solo product, create it silently and never mention it. In a team product, ask the first person for a name, and give the invitee nothing of their own.

What BuildBase does before you write any code

It creates one for you, lazily. Not at sign-up: the first time anything lists a user's workspaces and finds none, the server makes one on the spot and returns it in the same response.

That listing can come from anywhere. The WorkspaceSwitcher fetches the list when it mounts, so does fetchWorkspaces() from useSaaSWorkspaces, and so does bb().workspace.list() on the server. Whichever runs first creates the workspace. The rest of what happens is in the route that serves the list:

  • The name is the first two words of the user's name plus 's Workspace, or My Workspace when the account has no name. The user is its admin.
  • The caps still apply. A per-user workspace limit and your plan's ceiling both get a say, exactly as they do when a user creates one by hand. The one setting skipped is Can Create Workspace, because that one is about the user's own "New workspace" button, not about the platform giving them a first one.
  • A refusal is an empty list, not an error. The server logs it, and when it was your plan's org-wide ceiling your organization is told, but the browser only ever sees [].
  • Two tabs cannot make two. Two first loads at once both see "no workspaces", so with Redis running the route takes a 30-second claim: one request creates, the other re-reads what it made.
  • It fires workspace.created, the same event the explicit create route fires. We learned this one the hard way. Until we fixed it on 24 September it did not, so any workflow hung on that event never ran for an auto-created workspace. We found it setting up example orgs on a local stack wired like production, where a "Workspace Created" workflow grants 20 credits. It had zero runs, and the new user started at 0.

Both workspace modes ship with auto-create on. A new organization starts in Personal mode, one user and one workspace, where the switch cannot be turned off at all. Platform mode, for shared and invitable workspaces, also starts with it on. And our own docs say plainly what that means for an invitation: a waiting one "does not change what a user's first load does", so with auto-create on, an invited user ends up with their own workspace and the invited one beside it.

That is the default. Whether to keep it is the decision.

Who the first workspace is for

Three people sign up to a product, and they want three different things.

Who signs upWhat they should getWhy
Someone using a solo productOne workspace, made quietlyNobody else will ever see its name
The first person from a new teamOne workspace, named by themThe name goes out on invitations and onto the invoice
Someone a teammate invitedNothing of their ownTheir workspace already exists

The solo user should never see the word "workspace" during sign-up. In a product built for one person, the workspace is the account, and asking them to name it is a question with no audience. This is what Personal mode does, it is the default for a reason, and if it describes your product you can stop reading here. Personal's preset also switches invitations off, and the server refuses to switch them back on in that mode, so the invitee row of that table does not apply to you.

The invitee should get nothing. Their workspace exists; somebody else made it and invited them into it. An auto-created second one is not harmless clutter:

  • It is a trial. BuildBase stamps trialUsedAt on the workspace, not the user, and checkout reads that workspace when it decides whether a trial is still on offer. So every invitee's empty workspace is a fresh trial waiting to be claimed. The console's help text under Max Workspaces Per User reads "Prevents trial abuse", and a workspace each is exactly what that cap counts.
  • It counts. Your plan's workspace ceiling is an org-wide count, and an invitee's empty workspace is in it.
  • It is where they start. When the list loads and nothing was saved from a previous visit, the SDK selects the first workspace in it. For an invitee who has not accepted yet, that is their own empty one.

The first person from a team is where "silent or ask" is a real choice, and I would ask. The name is not private. The default invitation email's subject is {{inviterName}} invited you to {{workspaceName}}, so silent naming sends the next person "Demo User invited you to Demo User's Workspace". The same name is in the accept dialog they see, and, the first time the workspace goes to checkout, on the Stripe customer that BuildBase creates for it. Rename the workspace later and BuildBase renames that customer too. But Stripe stops updating customer_name on an invoice once the invoice is finalized, so every invoice already issued keeps saying "Demo User's Workspace".

Key takeaway

Ask for a name whenever a second person will ever see it. In a team product that is every workspace, and the field costs one click if you prefill it. In a solo product nobody sees it, so let it be created silently.

The honest cost is one more screen between sign-up and the product. I do not have a number for what that screen costs, and you will not either until you measure it in your own funnel. Prefilling takes the typing out of it, not the screen. So if most of your sign-ups are individuals trying the product alone, silent creation wins, and the question to settle it is always the same: will anyone but this user ever read the name?

Build it for a team app

Four steps, and the order matters: step two lists workspaces on the server, and with auto-create still on, that check creates the very thing it is checking for.

Before you start

A Next.js App Router app with BuildBase sign-in working and the server helper bb() from BuildBase on Next.js. An organization in Platform mode: new organizations start in Personal mode, and moving to Platform depends on your BuildBase plan. Invitations also need an email sender configured in the console, or sending one answers 503.

Step one: turn auto-create off in the console

In the console, open Workspaces, then Workspace Settings. With Platform selected as the mode, switch on Advanced Overrides, turn Auto-Create First Workspace off and save. That writes autoCreateFirst: false to your organization's workspace settings.

Leave Can Create Workspace on Everyone. The switch's own help text says "Off = users must be invited", and that is only true if you also turn creation off; the console labels that pair "Invite only". With creation left on, the screen you build next is how an uninvited user gets their first workspace.

What you should see: sign up as a brand-new user who has no invitation, and the workspace switcher shows an empty state instead of "Demo User's Workspace". Sign up as someone with a pending invitation and the empty state lists it.

Step two: send anyone without a workspace to onboarding

Do the routing on the server, where nothing renders until the answer is in. Every page in the app group lists the user's workspaces first:

// app/(app)/layout.tsx
import { redirect } from 'next/navigation';

import { bb } from '@/lib/buildbase';

export const dynamic = 'force-dynamic';

export default async function AppLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const workspaces = await bb()
    .workspace.list()
    .catch(() => null);
  if (!workspaces) redirect('/');
  if (workspaces.length === 0) redirect('/onboarding');

  return <>{children}</>;
}

And the onboarding page does the reverse, so a user who already has a workspace never lands on it:

// app/onboarding/page.tsx
import { redirect } from 'next/navigation';

import { bb } from '@/lib/buildbase';

import { FirstWorkspace } from './first-workspace';

export const dynamic = 'force-dynamic';

export default async function OnboardingPage() {
  const workspaces = await bb()
    .workspace.list()
    .catch(() => null);
  if (!workspaces) redirect('/');
  if (workspaces.length > 0) redirect('/dashboard');

  return <FirstWorkspace />;
}

What you should see: a brand-new user who opens /dashboard lands on /onboarding. A user with a workspace goes straight through, and one who types /onboarding by hand is sent back to /dashboard.

Step three: show invitations first

Invitations first; everyone else names it. The SDK already has both halves: WhenPendingInvitations renders its children only when the signed-in user has an invitation waiting, and its fallback when they have none.

// app/onboarding/first-workspace.tsx
'use client';

import { useRouter } from 'next/navigation';
import {
  PendingInvitations,
  WhenPendingInvitations,
} from '@buildbase/sdk/react';

import { NameYourWorkspace } from './name-your-workspace';

export function FirstWorkspace() {
  const router = useRouter();
  const done = () => router.replace('/dashboard');

  return (
    <WhenPendingInvitations
      loadingComponent={<p>One moment...</p>}
      fallbackComponent={<NameYourWorkspace onCreated={done} />}
    >
      <PendingInvitations onAccepted={done} />
    </WhenPendingInvitations>
  );
}

<PendingInvitations /> switches to the workspace on accept by default, so onAccepted only has to move them on. Declining drops the invitation from the list, and with nothing left waiting, the fallback takes over and asks them to name a workspace of their own. That is the right outcome for someone who said no.

What you should see: an invited user gets the invitation with Accept and Decline, accepts, and lands in the team's workspace with no other in their switcher. If they arrived through the emailed link, the SDK's own accept dialog opens as well; either route ends in the same workspace.

Step four: ask everyone else for a name

In practice "everyone else" is the first person from a new team. They get one field, prefilled so the common case is a single click:

// app/onboarding/name-your-workspace.tsx
'use client';

import { useState } from 'react';
import { useSaaSAuth, useSaaSWorkspaces } from '@buildbase/sdk/react';

// Domains that say nothing about the company. Extend for your audience.
const PERSONAL_DOMAINS = new Set([
  'gmail.com',
  'outlook.com',
  'hotmail.com',
  'yahoo.com',
  'icloud.com',
]);

function suggestName(user?: { name?: string; email?: string }) {
  const domain = user?.email?.split('@')[1]?.toLowerCase();
  if (domain && !PERSONAL_DOMAINS.has(domain)) {
    const label = domain.split('.')[0];
    return label.charAt(0).toUpperCase() + label.slice(1);
  }
  const first = user?.name?.trim().split(' ')[0];
  return first ? `${first}'s team` : '';
}

export function NameYourWorkspace({ onCreated }: { onCreated: () => void }) {
  const { user } = useSaaSAuth();
  const { createWorkspace } = useSaaSWorkspaces();
  const [name, setName] = useState(() => suggestName(user));
  const [pending, setPending] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setPending(true);
    setError(null);
    try {
      await createWorkspace(name.trim());
      onCreated();
    } catch (err) {
      setError(
        err instanceof Error ? err.message : 'Could not create the workspace.'
      );
      setPending(false);
    }
  }

  return (
    <form onSubmit={onSubmit}>
      <label htmlFor="workspace-name">What should we call your team?</label>
      <input
        id="workspace-name"
        value={name}
        onChange={(e) => setName(e.target.value)}
        minLength={2}
        required
      />
      <button type="submit" disabled={pending || name.trim().length < 2}>
        {pending ? 'Creating...' : 'Create workspace'}
      </button>
      {error && <p role="alert">{error}</p>}
      <p>Expecting an invitation? Sign in with the address it was sent to.</p>
    </form>
  );
}

The two-character minimum is the server's, not taste: it refuses a shorter name. The line at the bottom is for the address mismatch in the failure modes below.

What you should see: the new workspace appears in the switcher, and about 300ms after createWorkspace(name) resolves, the SDK opens workspace settings on the subscription screen. That is the SDK, not this code. And it is the right next screen. The trial belongs to the workspace, so it should start on the one that was just named, picked by the person who named it rather than by whoever happens to sign up next.

The failure modes

The check creates the workspace. You built steps two to four and skipped step one. bb().workspace.list() is itself the auto-create trigger, so the layout's check mints "Demo User's Workspace" before it can redirect anyone, and /onboarding never appears. If no new user ever sees your name screen, look at the console switch first.

Personal mode refuses the switch. Turning auto-create off there answers "Cannot disable auto-create in Personal mode. Users need a workspace on signup." That is correct. Personal mode also refuses to let users create workspaces themselves, so auto-create is the only way anyone gets a workspace of their own there. Switch to Platform first.

Invite-only by accident. With Can Create Workspace set to Disabled and auto-create off, createWorkspace answers 403 with "Workspace creation is disabled for this organization." Your name screen becomes a dead end for everyone who was not invited. If that is the product you want, replace the fallback with "Ask your team for an invitation" instead of a form.

The invitee signed up with a different address. Their invitation was sent to demo@example.com and they registered as demo+work@example.com. To this system those are two people, so no invitation is pending, step three falls through to the form, and they create a workspace of their own. That is why the form carries the "Sign in with the address it was sent to" line. The longer version of this hunt is in an invited member who cannot see the workspace.

A ceiling turns a new user away, quietly or loudly. The automatic path answers a refused create with an empty list, which at the browser is indistinguishable from a bug. Your form gets the real answer: 402 when it is the plan's ceiling, 403 when it is your own setting, each with a message you can show. That is a small reason to prefer this design even where silent creation would do.

Welcome work hung on sign-up. If a workflow grants starting credits or sends the onboarding email, hang it on workspace.created, not on the user's sign-up. An invitee should not get starter credits for a workspace they never made, and the 20-credit bug above is what it looks like when the event and the work drift apart.

A double click. The server refuses a second workspace with the same name from the same user ("Workspace with name ... already exists"), but that is a read before a write, and two requests in the same instant can both pass it. The disabled button is what actually stops the double click. Keep it.

Everything here assumes the workspace, not the user, owns the trial and the bill. If that is not settled yet in your app, settle it first: multi-tenant workspaces in an afternoon makes that case, and a free trial without a card covers what happens once the trial this guide hands to the right workspace starts.

I have not run these snippets end to end as one app. The server calls are the bb() helper from the Next.js setup guide, the components and hooks are the SDK's own exports as its type definitions declare them, and every server behavior described above is read from the route that serves the workspace list.

Install

npm i @buildbase/sdk
workspaces
onboarding
invitations
nextjs

Frequently Asked Questions

Should an invited user get a workspace of their own?

Not in a team product. The workspace they came for already exists, and a second one is empty, carries an unspent trial and counts toward your workspace ceiling. BuildBase gives them one anyway while Auto-Create First Workspace is on, so turn it off and create the first workspace yourself for users with no pending invitation.

When does BuildBase create the first workspace?

On the first request that lists the user's workspaces and finds none, not at the moment of sign-up. It is named after the first two words of their name, or My Workspace when there is no name, the user is its admin, and it fires workspace.created like any other create. If a workspace cap or plan ceiling refuses it, the list comes back empty instead.

Should I ask for a workspace name during onboarding or create it silently?

Ask whenever a second person will see the name, which in a team product is always: it goes into every invitation email and onto the Stripe customer, and Stripe stops updating the name on an invoice once it is finalized. In a solo product nobody else sees it, so create it silently.

Can I turn off auto-create in Personal mode?

No. The server refuses the change with "Cannot disable auto-create in Personal mode. Users need a workspace on signup." The Personal preset also switches invitations off, and the server refuses to switch them back on in Personal mode, so the invited-user case does not come up there.

Ship it

Create a project and run this guide against your own workspace.

7-day free trialNo credit card requiredCancel anytime