# Guest users

Let people try your app with one click as a guest, then keep everything they made by adding an email to the same account.

A sign-up form is a wall between a curious visitor and the thing they came to try. A guest method takes the wall down: **Continue as guest** on your sign-in page makes a real account with no address, signs the person in, and lets them use your app straight away. When they decide to stay, they add an email, and that same account, with the same user ID and everything hanging off it, becomes an ordinary one.

Step by step, with screenshots: [Set up guest users](/sign-in-setup/guests).

```tsx
import { GuestUpgrade, useSaaSAuth } from '@buildbase/sdk/react';

function SaveYourWork() {
  const { user } = useSaaSAuth();
  if (!user?.isAnonymous) return null;

  return ;
}
```

> **Note:**
  Guest users need a plan that includes them. Without one, the console shows the
  method locked and adding it is refused with a `402`.


## Setting it up

1. Open **User Management → Access → Authentication → Methods**, choose **Add auth method**, then **Guest**. The title is your own name for the method, shown in the console; the button always reads "Continue as guest", in the language the page is showing.
2. Tick it on your client in the **Settings** tab.

The button sits on the sign-in screen, below every other way in. It shows only while public sign-up is open and [Who can sign up](/authentication/signup-access) is set to Anyone: a guest has no address, so no allowlist or waitlist could name one.

## What a guest is

- **A real account.** Named `Guest`, with a placeholder address of the form `<userId>@anonymous.invalid` that is never shown. In the SDK, `user.isAnonymous` is `true` and `user.email` is empty.
- **An end user.** Guests count toward your plan's end-user limit. When the limit is reached, Continue as guest is refused. The idle cleanup below is what keeps the count honest.
- **Never mailed.** No welcome, no verification, no notification: BuildBase sends nothing to an `.invalid` address, whatever asks for it. Guests are not added to your audience, and making one is not a sign-up, so `user.registered` waits for the upgrade.
- **Marked in the console.** The users list shows them with a Guest badge, and filters to them.
- **No second step.** There is no factor to ask for and no address to set one up against. The second step starts once they upgrade. See [Two-step sign-in](/authentication/two-step).

A guest cannot change their address, connect a wallet, add a passkey or set up two-step sign-in (each answers `409 guest_account`): they create an account first.

**Bot protection.** Whenever [bot protection](/authentication/bot-protection) is on, the guest button is checked like a sign-up, since one click makes an account. One network address can make at most 10 guests an hour, then gets `429 rate_limited`.

## Upgrading in place

`` is the ready-made form, titled "Create your account": an address, an optional name and password, then a 6-digit code mailed to the address, with Resend counting down. The SDK's Profile screen shows it in place of the email for a guest, and the Security screen shows it in place of the password, phone, two-step and passkey sections.

For a form of your own, `useGuestUpgrade()` holds the same steps:

```tsx
import { useState } from 'react';
import { useGuestUpgrade } from '@buildbase/sdk/react';

function CreateAccount() {
  const upgrade = useGuestUpgrade();
  const [value, setValue] = useState('');
  if (!upgrade.isGuest) return null;

  const codeStep = upgrade.step === 'code';
  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        if (codeStep) upgrade.confirm(value);
        else upgrade.start({ email: value });
        setValue('');
      }}
    >
      <input
        value={value}
        onChange={(event) => setValue(event.target.value)}
        placeholder={codeStep ? '6-digit code' : 'you@example.com'}
      />
      <button disabled={upgrade.pending !== null}>
        {codeStep ? 'Create my account' : 'Email me a code'}
      </button>
      {upgrade.error && <p role="alert">{upgrade.error.message}</p>}
    </form>
  );
}
```

`step` moves from `details` to `code` to `done`. `pending`, `error` (with the server's sentence) and `waitSeconds` drive the buttons; `resend()` sends the code again and `reset()` goes back to the details. `<GuestUpgrade onUpgraded={...} />` tells you when it is done. Once the code is accepted, `useSaaSAuth().user` is the account at once, with `isAnonymous` false and the address verified. The session stays signed in.

The upgrade is held to everything a sign-up is: [who can sign up](/authentication/signup-access), closed sign-up, disposable addresses if you refuse them, and breached passwords. The address has to be free here. Without a password the account gets one nobody knows, as after a social sign-up, and **Forgot password?** sets a real one.

**A social account upgrades a guest too.** Connecting Google or another provider from the Security screen turns the guest into an account with the provider's address, when that address is verified, free and allowed to sign up. Otherwise nothing is linked, so a guest never ends up with a way in but no account to keep.

Either way, `user.registered` fires at the upgrade, with `fromGuest: true`, so a workflow on sign-ups sees the person once, when they gave you an address.

## Idle guests

A guest nobody has used for 30 days is deleted, through the same scrub as [a deleted account](/authentication/account-deletion), with `user.deleted` and `by: 'guest_expired'`. Change the 30 under **Security**, in the **Account deletion** card, with **Remove idle guests after (days)**: from 1 to 365. It is `guests.idleDays` on the auth config. A guest who upgrades is no longer removed for being idle.

"Used" means the session was used: the last-seen time moves at most once an hour while it is. A guest who created a workspace that other people joined is held, as a deletion would be.

A guest can delete their own account at any time without re-confirming, since there is no way for them to sign in again.

## The API

| Route                                       | Body                          | Answers                                           |
| ------------------------------------------- | ----------------------------- | ------------------------------------------------- |
| `POST /api/v1/auth/guest`                   | `{ turnstileToken? }`         | A sign-in, as `/login` answers                    |
| `POST /api/v1/public/guest/upgrade`         | `{ email, name?, password? }` | `{ retryAfter }`, the seconds before another code |
| `POST /api/v1/public/guest/upgrade/confirm` | `{ code }`                    | The profile, now an account                       |

The first is the sign-in page's, with its state header. The other two use the guest's session, as `WorkspaceApi.requestGuestUpgrade()` and `confirmGuestUpgrade()` send them. The code lasts 10 minutes and allows 5 tries.

| Refusal                                        | When                                                                        |
| ---------------------------------------------- | --------------------------------------------------------------------------- |
| `403` `method_unavailable`                     | The client has no guest method                                              |
| `403` `signup_closed`                          | Sign-up is closed, the mode is not Anyone, or the end-user limit is reached |
| `403` `bot_check_failed`                       | The bot protection check failed                                             |
| `429` `rate_limited`                           | 10 guests from this address in the last hour                                |
| `409` `email_unavailable`                      | The address is taken here, or cannot be used                                |
| `409` `not_a_guest`                            | The user is already an account                                              |
| `403` `signup_restricted`, `waitlist_required` | The address is not allowed to sign up                                       |
| `400` `disposable_email`, `breached_password`  | As for any sign-up                                                          |
| `429` `cooldown`                               | A code went out moments ago. Carries `retryAfter`                           |
| `400` `invalid_code`                           | The code did not match                                                      |
| `410` `code_expired`                           | No code is waiting, it lapsed, or its tries are used                        |

## Next steps

- [Who can sign up](/authentication/signup-access) - why guests need sign-up open to anyone.
- [Connected Accounts](/authentication/connected-accounts) - the other way a guest becomes an account.
- [Deleting accounts](/authentication/account-deletion) - the scrub idle guests go through.
