Teams, invites and admin vs member for an app you built with a prompt
No builder ships teams for your end users. Give every customer their own workspace, let them invite colleagues by email, and make admin vs member a rule the server enforces, not a hidden button.
In short
To add teams to a vibe-coded app, give every customer a workspace of their own on first sign-in, let members invite colleagues by email with a role, and enforce that role where the data is read, on the server. BuildBase holds the workspaces, members and roles; your database keeps the data, scoped by workspace id.
Two people from the same company sign up for your app separately. Now you have two accounts that should have been one, two sets of data that should have been shared, and no way to merge them that does not involve you, a database, and a Saturday. This is the teams problem, and it arrives with the second customer rather than the hundredth.
No builder ships the answer for your end users. Lovable's changelog is full of workspaces, roles, invites and SCIM, and every entry is about Lovable's own workspaces for the people building the app. Its end-user sign-in docs describe no teams, roles or invitations; Base44's cover sign-in methods and no roles; Bolt, Replit and v0 document nothing here. All read on 27 October 2026. Whatever a prompt builds for teams, it builds from scratch, and what it builds is the subject of every audit of a vibe-coded app we have read.
So this guide does it the other way round. The workspaces, the members, the invitations and the roles live in BuildBase, where they already exist, and your database keeps your data with one extra column. When it works: a new customer lands in their own workspace the moment they sign in, invites a colleague who gets an email and arrives in the same workspace with the role they were given, sees a member list and a switcher, and cannot see, or change, a single row that belongs to another company, however the request is made.
What we ran on 27 October 2026: the workspace, invitation and role pieces below are the ones the published guides on workspaces in an afternoon, team invitations and roles per workspace are built on, and the data-isolation pattern is the one in per-org data isolation. The prompts are what we hand an agent. We have not run them inside a builder's chat today.
Before you start
An app with BuildBase sign-in working (the v0 guide or the Lovable guide gets you there), and a BuildBase organization in Platform mode, which is the default. A second email address you control, for the invite.
1. Decide what a workspace is, in your product's words
A workspace is the thing a customer pays for and shares with colleagues. Before any prompt, decide what your product calls it (team, company, studio, school, client) and whether a person can belong to more than one. That one decision shapes the switcher, the invitation email and the settings screen, and an agent will guess it wrong if you do not say.
Two settings in the console back the decision, under the organization's workspace settings:
- Auto-create first workspace. On by default: a new user lands in a workspace of their own on first sign-in, with no setup step. Leave it on unless every user is invited into an existing team and never starts one.
- Can users create more workspaces. On if a consultant can have one workspace per client; off if a person belongs to exactly one company.
What you should see: a decision written in one line ("a workspace is a school; teachers belong to one; admins can create more"), and the two switches set to match. Everything below uses your word.
2. The switcher and the first workspace
The SDK's useSaaSWorkspaces() returns every workspace the signed-in
person belongs to, the current one, and the calls to create and switch.
The switcher is one component you style; the data is already there. Prompt
for it:
Add a <team> switcher to the app header using useSaaSWorkspaces() from @buildbase/sdk/react: list the <teams> the user belongs to, mark the current one, switch on click, and show "New <team>" if the organization allows users to create more.
Call fetchWorkspaces() once after sign-in and select the first <team> if none is selected.
Do not create <team>, membership or role tables in my database; BuildBase holds them. Every page that shows my data reads currentWorkspace.id from useSaaSWorkspaces() and uses it in the query.What you should see: sign up with a fresh email and you are in a workspace already, named for you, with the switcher showing one entry. If creating more is allowed, create a second and switch; the pages re-query for the new id. If the switcher is empty, the override in step 1 is off or the organization is in Personal mode, and auto-created first workspace has the two-minute fix.
The last line of that prompt is the one that matters, and the next section is about why.
What an agent builds for teams when nobody tells it otherwise
Left to itself, an agent asked for "teams" writes a teams table, a
memberships table with a role column, and a page that lists members. It
is the textbook shape, and it is where every audit of a vibe-coded app we
have read finds the leak, so it is worth knowing before you prompt what
the generated version leaves out.
The invitation is usually a row inserted for an email address, with no email sent, no pending state and no expiry, so the invited person either never hears about it or arrives to find nothing. The role is checked on the page that lists members and nowhere else; the routes that change data trust whoever is signed in. The workspace id travels in the request body, because that is the easiest place for the agent to put it, which means any member can send another workspace's id. And the "current workspace" is whatever the browser last said it was.
None of that is carelessness. A developer who runs a competing platform said on Hacker News in August 2026 that the apps he sees from non-technical founders are "all apps with multi-tenancy - database backed
- payment integrations - the works", a dance-school management tool, a classifieds site with paid upgrades. The builders ask for teams because their customers are teams, and the agent delivers the textbook, which was written for a tutorial with one trusted user. The three findings from the February 2026 audit (self-promotion to admin, a paid flag anyone could set, a dev endpoint left live) are the textbook meeting its first stranger.
What this guide does differently is move the parts that need to be right (who is in which workspace, who may do what, how an invitation becomes a member) into a system that already enforces them, and leave the agent one job it can do well: filter your rows by an id the server already knows.
3. Your data, scoped by workspace: the part no SDK does for you
Your tables are yours, in Lovable Cloud, Supabase, Neon or wherever the builder put them, and every row that belongs to a customer carries their workspace id. BuildBase decides who is in which workspace; your queries decide which rows a workspace sees, and no SDK can write those queries for you. This is the step a prompt gets wrong most often, so it is the longest one.
The rule has three parts, and all three have to hold:
- Every customer row has a
workspace_idcolumn, set when the row is created, from the current workspace, on the server. - Every read and write filters by it, with the id taken from the session's workspace on the server, never from the request body.
- The database refuses what the query forgets. Row-level security in Supabase or Postgres, or a check in the data layer that no route can skip.
Why "never from the request body" gets its own clause: a developer who reviews AI-built code wrote on 25 October 2026 that the first thing he looks for is "server routes that take a user id from the request body instead of the session", alongside "tables without RLS, policies that say true". A workspace id in the request body is the same bug: any member can send another workspace's id and read its rows. The server knows which workspace the session is in; it never needs to be told.
Add a workspace_id column to every table that holds customer data (<tables>), filled on insert from the current workspace on the server. Filter every select, update and delete by workspace_id, taking the id from the server session's current workspace, never from the request body, query string or headers.
Add row-level security policies so a row is only readable and writable when its workspace_id matches the caller's current workspace. Keep the policies in a migration file I can read.If your sign-in is BuildBase and your data is in Supabase, there is one
honest wrinkle: Supabase policies keyed to auth.uid() do not know a
BuildBase session. Either read and write through server routes that check
the session and query with a server credential scoped by the workspace id,
or mint a Supabase-shaped token on your server for browser queries. Both
are laid out in the Lovable
guide, section 7,
and per-org data isolation shows
the server-route shape in Next.js.
What you should see, and this is the test that matters: sign in as a member of workspace A, open a page that lists rows, and in the browser's developer tools replay the request with workspace B's id in the body, the query string and a header. Every attempt returns A's rows or nothing. Then change the id in the page address, if the page carries one. Same result. If any of those returns B's data, the filter trusts the client, and nothing else in this guide helps until that is fixed.
Key takeaway
BuildBase decides who belongs to which workspace. Your queries decide which rows a workspace sees, with the id from the session, never the request. Both halves have to be right, and the audits say the second half is the one that is usually wrong.
4. Invite a colleague by email
Members invite teammates by email and give each one a role. An address with no account yet gets an email with a link; following the link is the proof, so the person signs up and lands in the workspace without a separate email-verification step. If the organization has switched invitations off, the landing page says so before anyone is sent anywhere.
The SDK's built-in members screen does all of this, which is why the prompt is mostly about not rebuilding it:
Add a /settings page that opens the SDK's built-in workspace, members and billing screens. On the members screen, admins can invite by email with a role, change a member's role, remove a member, and see and withdraw pending invitations; members see the list read-only.
Use the SDK's screens and hooks (useWorkspaceInvitations, inviteMember); do not build an invitation table, an invitation email or an accept page of my own.What you should see: invite your second email address as a member. The
email arrives from your sending domain with a link; open it in a private
window, sign up, and you land in the same workspace with the member role.
Back in the first window, the pending invitation has become a member. Then
invite the same address again: refused, with a named reason, because the
SDK's invitation errors carry a code and the server's fields (the seat
limit and the count when it is a seat limit, the retry delay when it is a
rate limit), and the screen says each in words.
Two things can refuse an invitation that look like bugs and are not. A
seat limit on the plan, which the members screen shows as "n of m seats"
through useSeatStatus, counting pending invitations the way the server
does. And an outsiders policy: an organization can block inviting people
from outside its own domains, or cap the role an outsider may hold, and
the error names that too. Why an invited member cannot see the
workspace walks the other
failure, where the invitation worked and the data did not follow.
5. Admin and member: enforce the role where the data is
A role is a rule about what a teammate may do, and it is only a rule if the server applies it. Owners and admins decide what each member can do; the API refuses what the role does not allow; the interface merely hides what would be refused. Generated apps get the third part and skip the second, which is the bug every audit finds.
The February 2026 audit of three apps posted to Reddit found "a live marketplace with real Stripe payments where any logged-in user could grant themselves admin and hijack payment routing with a single request". The admin button was hidden. The request behind it was not checked. One request.
Roles in BuildBase exist at two levels: organization roles for your own staff, and workspace roles for the customer's team, with defaults a workspace can adjust and permissions you add for your product ("export reports", "manage billing"). In the browser, the SDK ships the gates:
import { WhenWorkspaceRoles, WhenPermission } from '@buildbase/sdk/react';
<WhenWorkspaceRoles roles={['owner', 'admin']}>
<DeleteWorkspaceButton />
</WhenWorkspaceRoles>
<WhenPermission permission="reports.export">
<ExportButton />
</WhenPermission>And on the server, the same question asked of the session, not the request:
Every server route that changes <team> data or settings must check the caller's role or permission in the current workspace through the BuildBase server SDK, using the session, before doing the work, and return 403 with a short JSON error otherwise. Never read a role, a permission or an "isAdmin" value from the request body, query string or headers.
In the browser, wrap admin-only controls in <WhenWorkspaceRoles> or <WhenPermission> from @buildbase/sdk/react, as a convenience only; the server check is the rule.Two levels, and it is worth knowing which you are prompting for. An organization role is for your own staff: who at your company may see every workspace, change plans, read the audit log. A workspace role is for the customer's team: owner, admin, member, or the names your product uses, with the defaults a workspace admin may adjust for their own team. The permissions you add ("reports.export", "billing.manage") hang off workspace roles, so a customer's owner decides which of their colleagues may export, and your server asks that question of the workspace, not of a global table. If you have one role column today, this is why it is not enough: the same person can be an admin of one workspace and a member of another.
What you should see: as a member, copy the address of an admin-only page and open it: the controls are gone. Then, from the developer tools, replay an admin request (change a role, delete the workspace) with the member's session. 403, every time, whatever the body says. If it succeeds, the route checks nothing or checks the client, and that is the one-request bug.
One honest caveat about timing: a role change reaches a signed-in user within the hour, not instantly, because the SDK refreshes permissions on a schedule rather than on every request. Why permissions refresh hourly and not instantly is the trade, and a permission change not taking effect is what to do when you need it sooner. What is RBAC is the explainer if "role" and "permission" are blurring together.
6. Switching, and the data that did not switch
The last bug is the quiet one. A person switches workspace in the switcher, and a page still shows the old workspace's rows, because a hook cached them or a query ignored the change. It looks like a leak and it is usually a stale fetch.
Every data hook and loader in the app depends on currentWorkspace.id from useSaaSWorkspaces(): when it changes, refetch, and clear any cached rows from the previous <team> before the new rows arrive. Never keep rows from one <team> in memory while another is selected.What you should see: with two workspaces that have different data, switch back and forth. Each page shows only the selected workspace's rows, immediately, with no flash of the other's. Stale data after a workspace switch covers the cache details in Next.js.
The screens you do not have to build
The settings screens for all of this ship with the SDK: workspace, members
(invite, pending invitations, roles, remove), billing, plan, usage,
devices, notifications and the rest, thirteen in all, in eight languages
including right-to-left ones. The /settings prompt in step 4 opens them
inside your app's own layout. You can draw your own members screen against
the same hooks (useSaaSWorkspaces, useWorkspaceInvitations,
useSeatStatus), and the only reason to is a design system the built-in
screens do not match; the behaviour is the same either way, and the
refusals (a seat limit, an outsider policy, a duplicate invitation) come
back as the same named errors.
Where the built-in screens earn their keep is the part nobody budgets for: the pending-invitation list, the "withdraw" action, the role change that has to be refused when it would leave a workspace with no owner, the member removal that has to end that person's session on the server. A generated members page does the first thing on that list and none of the others.
Where the builder's built-in sign-in wins
If your app is for one person, a notebook, a personal tracker, a tool for a single operator, you do not need any of this, and the builder's built-in sign-in with its users list is the better pick: zero setup, no switcher, no roles to explain. Lovable's Users view, Replit's Users tab and Base44's auth cover that case well, and adding workspaces to an app that will never have two people in one account is weight for nothing.
The moment a second person from the same customer shows up, the maths flips. Merging two personal accounts into one team later is the migration every small SaaS dreads, and the audits say the generated version of teams is where the data leaks. What is multi-tenancy is the short explainer; the best multi-tenant backend is the comparison if you want to weigh the alternatives before committing.
What breaks, and how you can tell
A member sees another company's rows by changing an id. The filter takes the id from the request. Step 3, and nothing else matters until it is fixed.
The switcher is empty, or "New team" does nothing. Auto-create is off, the organization is in Personal mode, or "Can invite members" is disabled under advanced overrides. Step 1's two switches.
The invitation email never arrives. It is sent from your organization's sending domain, and that domain is not set up yet; email landing in spam is the guide. The invitation itself exists; check pending invitations on the members screen.
Invited, signed up, sees nothing. The person signed up with a different email than the one invited, or accepted in a browser that then switched to a personal workspace. Invited member cannot see the workspace walks it.
A member promoted themselves. The role-change route checks the client, or checks nothing. Step 5; replay the request to prove the fix.
The role changed but the member still sees the old buttons. Up to an hour, by design. Sign out and in to force it, or follow permission change not taking effect.
Install
Add the SDK to the app you have, or let the CLI write the sign-in files the switcher and members screen sit on:
npm i @buildbase/sdk
npx buildbase init --org-id <your 24-character org id>