BuildBaseBuildBase

Invitations

Invite people who have no account yet, and let them accept from the email link.

An invitation reaches an email address whether or not it has an account. The person gets an email, follows the link into your app, signs in or registers, and accepts. Until then the invitation is pending, and a pending invitation holds a seat.

This is the same flow the BuildBase console uses for its own organization invitations, on your app's side: fourteen days of validity, a five-minute resend cooldown, and the pending list under the invite form.

The timings are the server's, not the SDK's. Every pending invitation carries resendAvailableAt, the instant the cooldown ends, so the Resend button counts down to the same moment the endpoint stops answering 429 even if we change the cooldown later. Nothing in your bundle has to know what five minutes means.

Note

Requires @buildbase/sdk 0.0.71 or later, and a BuildBase server from release 22 . On an older server the invite form answers 404.

The admin's side

The members screen does it all: invite by email with a role, see the pending invitations under the form, resend one, revoke one. Nothing to build.

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

function TeamButton() {
  const { openWorkspaceSettings } = useSaaSAuth();
  return (
    <button onClick={() => openWorkspaceSettings('users')}>Manage team</button>
  );
}

From code, the same actions live on useSaaSWorkspaces():

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

const { inviteMember, listInvitations, resendInvitation, revokeInvitation } =
  useSaaSWorkspaces();

// Invite someone who may not have an account yet
await inviteMember(workspaceId, '[email protected]', 'editor');

addUser is unchanged. It adds an existing account at once and answers 404 when the address has none, with a message saying to send an invitation instead; inviteMember is the way to reach a stranger.

The invitee's side

The email link lands on your app's auth redirect URL, the one the app returns to after login, with the invitation token in the URL. The SDK reads it, shows who invited them to what and as which role, and offers sign-in when they are signed out. Once signed in they accept or decline in place. You do not have to build any of this.

A brand-new invitee does not have to verify their email first. Following the link is the proof: the token was sent to that address and to nowhere else, and the SDK passes it back when it accepts, so the account is marked verified on the way in. The same standard a magic link sets. Accepting from the in-app list carries no token, so there a verified email is still required, and a stolen invitation id proves nothing by itself.

When public sign-up is off

An invitation works even when you have switched off Allow public sign-up under Authentication › Sign-up. That is what makes the setting invite-only rather than closed: the person the invitation was sent to can still create an account, and nobody else can.

The link is what lets them in. The hosted sign-up page shows its form, and the server creates the account, only for a visitor holding the token of a pending, unexpired invitation, and only for the address it was sent to. The same goes for a first sign-in with a social provider that starts from the link. A forwarded link does not open sign-up for anyone else, and your plan's end-user limit still applies. See Closing public sign-up.

Carrying the token to those pages needs SDK 0.0.75 or later. On an earlier version the invitation is parked but never sent on, so an invited person sees the invite-only notice rather than the form.

The hosted pages keep the token for the browser tab, because moving from login to register and the round trip through a social provider both drop the query string. So once a tab has opened an invitation link, the register form keeps showing there even on a URL without one: seeing the form is not the same as being allowed to register, and the server still refuses an address the invitation was not sent to.

For an invite-only app (workspace settings with autoCreateFirst: false) a new user has no workspace yet. The workspace switcher's empty state shows their pending invitations instead of "No workspaces available". To put them on your own screen:

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

function NoWorkspaceScreen() {
  return (
    <WhenPendingInvitations
      fallbackComponent={<p>Ask your team for an invitation.</p>}
    >
      <PendingInvitations onAccepted={(ws) => console.log('joined', ws.name)} />
    </WhenPendingInvitations>
  );
}

useMyInvitations() is the hook behind both, with accept and decline.

The server builds the link from the auth client the inviter's session signed in through: the first allowed redirect URL of that client, which is where the app lands after login anyway. To land somewhere else, pass landingUrl; it must be one of that client's allowed redirect URLs, or the server answers 400.

A session minted from an API token has no auth client, so the server factory's invitations.create(workspaceId, email, role, landingUrl) needs landingUrl there.

Seats

A pending invitation is billed as a seat, from the moment the email is sent until it is accepted (the member keeps the seat), declined, revoked or expired. The members screen counts pending invitations in its seat block and says so. The seat limit is checked before an invitation is created, so an admin cannot queue twenty invitations against three seats and discover it one acceptance at a time. Revoking a pending invitation releases its seat at once.

Modes

Workspace modeWhat invitations do
personal (one workspace per user)Nothing: the preset sets canInviteMembers: false, so every invite surface hides and the server refuses with 403 WORKSPACE_INVITES_DISABLED. A B2C app pays nothing for this feature existing.
platformThe full flow. Every user gets a workspace on first load, invited or not, and can be invited into others.
platform with autoCreateFirst: falseThe only way in. A new user sees their pending invitations in the switcher, or in <PendingInvitations /> where you place it.

One setting decides this: canInviteMembers. false turns invitations off everywhere, and 'admin-only' leaves them on and lets the role check decide. Selecting personal mode sets it to false for you, which is why a B2C app gets nothing it has to switch off by hand.

The mode itself is never read, on the server or in the SDK. It is a preset that seeds the settings and can be changed underneath them afterwards, so reading both would mean two sources deciding one thing and disagreeing the moment either moves. Change canInviteMembers and every surface follows.

When invitations are off, only the invite surfaces go: the members screen's invite form and every Resend button, and the invitee's side, where useMyInvitations() makes no request and reports an empty list, so <PendingInvitations />, <WhenPendingInvitations> and the switcher section render nothing. Everything else on the members screen stays. The members already in a workspace are still listed, their roles can still be changed and they can still be removed, because switching invitations off stops people being added and nothing more. Rows sent before the switch went off stay listed with Revoke, and the invitee can still decline: such a row is billing a seat until somebody resolves it.

A waiting invitation does not change what a user's first load does. Whether they get a workspace of their own is autoCreateFirst, as it is for everyone else, so with it on an invited user has their own workspace and the invited one beside it once they accept.

Errors

Every invitation call throws a plain Error with status and code, plus the fields a screen needs: currentUserCount and limit on a seat limit (SEAT_LIMIT_REACHED), retryAfterSeconds on a resend cooldown (INVITATION_RATE_LIMITED), the scrambled invitedEmail on a mismatch (INVITATION_EMAIL_MISMATCH). EMAIL_NOT_VERIFIED, INVITATION_EXPIRED and INVITATION_ALREADY_HANDLED are the others you will meet. Check with isInvitationError(err). EMAIL_NOT_VERIFIED only comes back for an accept with no link token, which means accepting from the list.

Events and webhooks

Five system events fire on your webhooks and workflow triggers: workspace.member_invited, workspace.invitation_accepted, workspace.invitation_declined, workspace.invitation_revoked and workspace.invitation_expired. Accepting also fires workspace.member_added, because a member was added, exactly as a direct add does.

Self-hosted

Three things are worth knowing before you invite anyone from a self-hosted image.

Invitations need a configured organization email sender. There is no platform fallback: with no usable sender the create route refuses with 503 NO_SENDER_CONFIGURED before any row is written, so nobody is billed a seat for a mail that was never sent. Set one up under Emails first.

Nothing extra has to run. Pending invitations expire on a ten-minute sweep, and the self-hosted image runs its server in full mode, which carries the crons alongside the API. The split deployment with separate worker replicas is a managed-hosting shape; you do not need it for invitations to expire on time.

The per-workspace send limit cannot be turned off. Sixty invitations an hour per workspace is enforced in every image, and DISABLE_RATE_LIMIT is pinned off in a locked build, so it holds whatever the environment says. The daily ceiling of two hundred invitation emails per organization applies too, on its own budget so a large onboarding cannot starve your users' sign-in emails.

Next Steps