Invitations
Invitations are the preferred way to onboard new users and grant app access to existing BluAuth identities. Instead of creating a user record, generating a temporary password, and communicating the credentials out-of-band, you send a themed email with a secure link. Depending on the case, the invitee either claims the invite with a new BluAuth account or signs in with an identity they already use. Nothing sensitive travels through side channels.
Invitations live at /admin/invitations. The list shows every pending, accepted, revoked, and expired invitation in the tenant.
Why invitations over direct user creation
| Concern | Invitation | Direct create |
|---|---|---|
| Temporary password to communicate | None | Yes — must be handed off securely |
| User sets their own password | Yes | Only after first sign-in (force-reset) |
| Audit trail | invitation.created + .accepted | user.created |
| Works for external users | Yes (email works regardless of domain) | Yes |
| Pre-assigns role and client access | Yes | Yes |
For anything other than scripted migrations and machine accounts, use invitations. See the users guide for the exceptions.
Creating an invitation
From /admin/invitations:
- Click New Invitation.
- Enter the recipient's Email.
- Optionally set a Recipient Name — appears in the email greeting ("Hi Alex,").
- Set a Client — which OAuth client the invitee will be onboarded against. The bound theme drives the invitation email's branding and the hosted login experience at the link destination.
- Set a Role (
useroradmin) — pre-assigned at acceptance. - Set an Expiration — defaults to 7 days. Maximum 30 days.
- Optionally set Required Providers — provider slugs such as
tpauthwhen the invite must be claimed through a specific upstream provider. - Click Send.
BluAuth:
- Creates an
invitationsrow with a one-time accept token (a cryptographically random 256-bit token hashed before storage). - Resolves the theme bound to the selected client.
- Renders the themed invitation email — logo, primary color, heading from the client's theme.
- Sends via AWS SES.
The invitation immediately appears in the list with status pending.
The invitee experience
- The invitee receives an email titled "You're invited to {client name}", branded with the client's theme.
- The email body includes a greeting (using the recipient name, if provided), a sentence explaining who invited them (the admin's name), and a prominent Accept invitation button.
- They click the button → they land at
/invite?token=.... - BluAuth validates the token:
- If valid, the page renders the themed hosted login surface and lets them either set a BluAuth password or continue with one of the client-approved identity providers.
- If the invitation has required providers, BluAuth only shows those providers and hides the password path.
- If expired, they see an "invitation expired" screen with a Request a new invitation link that emails the tenant admin.
- If already consumed, they see an "already accepted" screen and are redirected to sign in.
- On submit or successful provider sign-in, BluAuth:
- Creates a user record with their email, the pre-assigned role, and their new password when they choose the password path (hashed with the configured policy — see users guide → password policies).
- Or, when the user already has a BluAuth identity, links the invitation to that existing user instead of forcing a second account.
- Marks the invitation
accepted. - Logs them in.
- Redirects to the client's default post-login URL.
The whole experience is themed — the email, the acceptance page, the post-acceptance login session. See theme studio for how the theme resolves.
Pre-populated data at acceptance
When the invitee accepts, BluAuth pre-populates:
- Email — locked as the invited address for the invitation itself.
- Role — pre-assigned from the invitation, applied server-side. The invitee can't change it.
- Name — populated from the invitation's
recipientNamefield if set; otherwise the invitee fills it in. - Email verified — the invited email is treated as verified because the user proved control of the invitation link. If they claim the invitation through a provider whose login email differs, BluAuth stores the invited address as a verified alias on that user.
- Bound client — the client the invitation was created against is automatically recognized in the user's first session.
If the password path is available, the invitee chooses their password (and optionally their display name if not pre-set). If the invite requires a provider, the password path is not available.
Provider-targeted invitations
Some invitations are intentionally restricted to one or more providers.
Use this when:
- the user's company requires a specific IdP for access
- the invited operational email is different from the provider-returned email
- you want the identity proof to happen through one corporate provider
Examples:
- require
tpauthfor TransPerfect users - require Google for a Google Workspace-based client
BluAuth validates two things before allowing the invite to continue:
- the chosen provider must be linked to the invited client
- the chosen provider must be one of the invitation's required providers, if any were specified
Existing users versus new users
An invitation does not always mean "create a brand-new account."
There are two normal outcomes:
- Existing BluAuth user — the invite is mostly an access-grant and context-carrying flow
- New BluAuth user — the invite is a full claim/onboarding flow
This matters for support teams. If someone already uses BluAuth for another app, they should not be told to create a second account just because they were added to a new client.
Different invited email and provider email
This is supported when BluAuth can prove the authenticated user owns the invited address.
If the invite was sent to one address but the required provider returns another, BluAuth can still accept the invitation when:
- the invited address is already a verified alias on that BluAuth user, or
- the invite token proves control of the invited address and BluAuth safely attaches it as a verified alias
That is how a user invited at clay@blu-team.com can still sign in through TP Auth as clay.levering@transperfect.com without creating a duplicate identity.
Lifecycle and state
An invitation moves through these states:
| State | Trigger | Next states |
|---|---|---|
pending | Invitation created | accepted, revoked, expired |
accepted | Invitee sets password and account is created | — (terminal) |
revoked | Admin revoked before acceptance | — (terminal) |
expired | expiresAt passed without acceptance | — (terminal) |
Expired invitations are not automatically deleted — they sit in the database with expired status to preserve the audit trail. Filter the list to hide them if the surface gets noisy.
Tracking
The list view shows:
- Email — who the invitation is for.
- Client — which OAuth client they'll land in.
- Role — pre-assigned role.
- Status — pending / accepted / revoked / expired, with a status chip.
- Created — when the admin sent it.
- Expires — when it stops working.
- Inviter — which admin sent it.
Filter by status, client, or inviter. Sort by created-at (newest first by default) or expires-at.
Revoking an invitation
Click Revoke on any pending invitation to invalidate its token immediately. The invitee will see the "invitation expired" screen if they later click the link.
- Accepted invitations can't be revoked — they're already consumed. Use the users list to deactivate or delete the resulting user record if you need to roll back.
- Expired invitations don't need revoking — they're already unusable.
- Revocation is logged —
invitation.revokedfires, the list shows who revoked it and when.
Re-sending
If the invitee reports they didn't receive the email (spam filter, typo, etc.), use Resend on the invitation row. BluAuth re-renders the email and sends again via SES. The accept token stays the same — re-sending does not extend expiration or invalidate the existing link. If the expiration has passed, revoke and re-issue instead.
The invitation URL format
Accept URLs look like:
https://auth.blutools.io/invite?token={invitationId}.{acceptToken}
invitationId— the public UUID of the invitation row (visible in the admin UI).acceptToken— the one-time secret; validates the link.
If you need to send the URL out-of-band (rare — you should be sending the email), you can copy it from the admin UI's Copy accept URL action. This bypasses SES. Useful when:
- The tenant's email is broken and you're mid-triage.
- You're onboarding a VIP and want to hand them the URL in a secure messenger.
Never paste the URL in public channels — it's a one-time credential.
Bulk invitations
The UI supports bulk invitations via the Import CSV action on /admin/invitations. Upload a CSV with these columns:
email,name,role,client,expiresInDays
alex@acme.com,Alex Kim,user,bucket-indexer-prod,7
jordan@acme.com,Jordan Ng,admin,bucket-indexer-prod,14
sam@partner.co,,user,partner-portal,30
- Required:
email,client(slug). - Optional:
name,role(defaults touser),expiresInDays(defaults to 7). - Empty cells are fine — defaults fill in.
On upload:
- BluAuth validates every row. Invalid rows (bad email, unknown client, unknown role) surface inline with a row number.
- Valid rows preview with the counts per client and per role.
- Click Send all to issue invitations in a batch. Each invitation creates its own email and audit event — there's no bulk-email combining.
The API alternative (POST /api/admin/invitations) accepts one invitation per call — script against it with an admin session cookie if you need the bulk flow outside the UI.
Events
Every invitation lifecycle event publishes to the webhook stream:
invitation.created— payload includes invitee email, client, role, expires-at.invitation.accepted— payload includes the created user's ID and the accept timestamp.invitation.revoked— payload includes the revoker and revoke reason (if provided).invitation.expired— fired by a scheduled job when an invitation crosses itsexpiresAt.
See the webhook events reference for payload shapes. Downstream apps can subscribe to invitation.accepted to trigger tenant-side onboarding (create a workspace, seed default content, etc.).
Email rendering
Invitation emails render against the theme bound to the client the invitation is for:
- Logo — the theme's logo (or the default BluAuth shield if none is set).
- Heading — "You're invited to {client name}".
- Body — a short introductory paragraph using the inviter's name and optional recipient name.
- Button — styled with the theme's primary color, labeled Accept invitation.
- Footer — the theme's footer text, with a fallback to "Powered by BluAuth".
Subject line, body copy, and button label are not currently exposed as per-tenant overrides. File an issue if you need tenant-customizable email copy.
Common failure modes
| Symptom | Likely cause |
|---|---|
| Invitee reports no email received | Spam filter, bad email address, or SES deliverability issue. Check the admin events panel for email.delivery.bounced. |
| Invitee clicks link, sees "already accepted" | Someone already accepted — check the audit log for a recent invitation.accepted event. |
| Invitee clicks link, sees "invitation expired" | Link is past expiresAt, or you revoked it. Re-issue. |
| Invitation created but email never sent | SES outbound quota hit or SES identity not verified. Check service health. |
| Bulk CSV upload fails with "unknown client" | The client column values must match client slugs exactly (not names). |
Next
- Users — manage the user records that invitations create.
- Clients — the OAuth client drives email branding and post-accept routing.
- Theme studio overview — design the invitation email and the acceptance page.