AuthServer
Email Invitations
Invite users to organizations with expiring email-bound links.
Email invitations are organization membership invites. An invite is bound to one email address, one organization, and one role. It can be accepted once before it expires.
Public signup requests cannot join an existing organization by sending an organizationId. Invitations are the built-in explicit join path for email-bound membership, and acceptance is where the membership is created.
When a user accepts an invite, SqlOS:
Active existing memberships are idempotent. Accepting the invite consumes it but does not downgrade the current role.
var invite = await authService.CreateEmailInvitationAsync(
new SqlOSCreateEmailInvitationRequest(
OrganizationId: org.Id,
Email: "teammate@example.com",
Role: "member",
ClientId: "web",
RedirectUri: "https://app.example.com/auth/callback",
Scope: "openid profile email",
Resource: "https://api.example.com",
CustomFields: new JsonObject
{
["source"] = "members-page"
}),
httpContext,
ct);The result includes InviteUrl. The token is stored hashed in the database.
Invite emails link to:
/sqlos/auth/invitations/accept?token=...Hosted AuthPage renders the invite context and uses the configured credential methods:
For passwordless invitation signup, SqlOS creates the user and accepts the invite directly. It does not send a second OTP code after the user clicked the invite link. If the invited email matches an organization SSO domain, home realm discovery still redirects to SSO before local account creation.
Headless apps should treat invitations as a first-class view, not as a generic signup detour.
| Endpoint | Purpose |
|---|---|
POST /sqlos/auth/headless/invitations/resolve | Validate token and get invitation context before starting OAuth |
GET /sqlos/auth/headless/requests/{requestId} | Load the bound request and invitation context |
POST /sqlos/auth/headless/invitations/signup | Create an invited passwordless account and accept the invite |
Recommended headless lifecycle:
/invitations/resolve./sqlos/auth/authorize request with invitationToken.identify, SSO, password, or OTP sign-in./invitations/signup directly.For a new invited user, call /sqlos/auth/headless/invitations/signup. Calling /signup/email-otp/start creates a second proof step and can strand the invite flow behind an unnecessary code challenge.
For backend-owned flows, call the service directly after your application has established the user identity:
await authService.AcceptEmailInvitationAsync(
new SqlOSAcceptEmailInvitationRequest(
InvitationToken: token,
UserId: user.Id),
httpContext,
ct);For passwordless invite signup from your own backend:
var result = await authService.AcceptEmailInvitationSignupAsync(
new SqlOSAcceptEmailInvitationSignupRequest(
InvitationToken: token,
DisplayName: "Jane Doe",
ClientId: "web",
CustomFields: null),
httpContext,
ct);This creates a verified user, accepts the invite, creates the session, and returns SqlOSLoginResult.
var resent = await authService.ResendEmailInvitationAsync(
new SqlOSResendEmailInvitationRequest(invite.Id),
httpContext,
ct);
var revoked = await authService.RevokeEmailInvitationAsync(
new SqlOSRevokeEmailInvitationRequest(invite.Id, "wrong-email"),
httpContext,
ct);Resending invalidates the previous token. Revoked, expired, and accepted invites cannot be used.
CustomFields travel with the invite and are available to headless signup hooks. Use this for product context such as referral source, plan, template, or billing state.
options.AuthServer.UseHeadlessAuthPage(headless =>
{
headless.OnHeadlessSignupAsync = async (ctx, ct) =>
{
var source = ctx.CustomFields?["source"]?.GetValue<string>();
await SaveSignupProfileAsync(ctx.User.Id, source, ct);
};
});SqlOS creates the default auth.invitation template automatically. Edit it in Dashboard > Communications > Templates to customize organization invitation email copy and layout while keeping the same runtime variables.
Invitations use the same sender and branding system as Email OTP. Configure the built-in look with Email Branding, or provide an advanced message builder:
options.AuthServer.ConfigureInvitations(invites =>
{
invites.BuildMessage = ctx => new SqlOSAuthEmailMessage(
ctx.Email,
$"Join {ctx.OrganizationName}",
$"<a href=\"{ctx.AcceptUrl}\">Accept invite</a>",
$"Accept invite: {ctx.AcceptUrl}");
});