Invites
Overview
An invite brings a person into a tenant by email. inviteUserToTenant is the entry point: when the email already belongs to a user it grants them membership directly, and otherwise it creates a token-bearing invite row the person redeems later. The two outcomes are distinguished by the kind field of the result.
The functions live in @antelopejs-private/cms/interfaces/cms/invites; the UserInvite table and its model live in @antelopejs-private/cms/interfaces/cms/db.
Invite a User
import { inviteUserToTenant } from "@antelopejs-private/cms/interfaces/cms/invites";
const result = await inviteUserToTenant({
tenantId,
email: "[email protected]",
roleIds: ["role-editor"],
asTenantOwner: false,
sendEmail: true,
});
if (result.kind === "added") {
// an account with that email existed: membership granted, no token
return { userId: result.userId };
}
// no account yet: an invite row was created
return { inviteId: result.inviteId, token: result.token };
InviteUserToTenantOptions
| Option | Type | Description |
|---|---|---|
tenantId | string | Tenant the person is invited into. Required. |
email | string | Email to look up and, failing that, to invite. Required. |
firstname | string | null | Invitee's first name, stored on the invite row and used to address the email. Defaults to null. |
lastname | string | null | Invitee's last name. Defaults to null. |
language | string | Language stored on the invite row. Defaults to "en". |
roleIds | string[] | Roles granted on join. Defaults to []. |
asTenantOwner | boolean | Grant tenant ownership on join. Defaults to false. |
skipEmailValidation | boolean | Stored on the invite row so redemption can skip email validation. Defaults to false. |
sendEmail | boolean | Dispatch the admin invite email after the row is created. Defaults to false. |
InviteUserToTenantResult
type InviteUserToTenantResult =
| { kind: "added"; userId: string }
| { kind: "invited"; inviteId: string; token: string };
The "added" branch runs applyTenantOwnership with roleIds and asTenantOwner (Tenancy), so the member hooks fire and no token is generated. The "invited" branch creates the invite row through createUserInviteToken, then dispatches the email when sendEmail is set — fire-and-forget, so the call does not wait on delivery and does not fail when delivery does.
The function is deliberately side-effect-light: it does not throw on duplicates and performs no context-specific pre-checks. Decide in the caller what "already a member" should answer.
Create an Invite Token Directly
createUserInviteToken is the lower-level half — it always creates a row and never touches memberships or email.
import { createUserInviteToken } from "@antelopejs-private/cms/interfaces/cms/invites";
const { inviteId, token } = await createUserInviteToken({
tenantId,
email: "[email protected]",
language: "en",
roleIds: [],
asTenantOwner: false,
skipEmailValidation: false,
});
CreateUserInviteTokenOptions requires tenantId, email, language, roleIds, asTenantOwner and skipEmailValidation; firstname and lastname are optional and default to null. CreateUserInviteTokenResult is { inviteId: string; token: string }.
inviteeDisplayName(firstname?, lastname?) joins the two into the name the invite email addresses the person by, and returns undefined when the invite carries neither. inviteUserToTenant calls it for you; call it yourself when you render your own invite surface from a stored row, so the fallback for a nameless invite matches what the email does.
An existing invite for the same email in the same tenant is deleted first. The call therefore fires, in order, Hook.INVITE_DELETED (only when it replaced one), then Hook.INVITE_BEING_CREATED, then Hook.INVITE_CREATED with the new inviteId and token (Hooks).
Token Constants
| Constant | Value | Meaning |
|---|---|---|
INVITE_TOKEN_LENGTH | 64 | Length of the generated token. |
INVITE_EXPIRY_DAYS | 7 | Days from creation to expiresAt. |
import {
INVITE_EXPIRY_DAYS,
INVITE_TOKEN_LENGTH,
} from "@antelopejs-private/cms/interfaces/cms/invites";
The UserInvite Table
Table user_invites in the cms-tenant schema, so invites are per-tenant like memberships. The table-name constant USER_INVITES_TABLE_NAME is exported next to it.
| Field | Type | Notes |
|---|---|---|
_id | string | Invite id — the inviteId the create functions return. |
createdAt | Date | Creation time, indexed. |
email | string | Indexed. |
firstname | string | null | Invitee's first name, when the invite carried one. |
lastname | string | null | Invitee's last name, when the invite carried one. |
roles_ids | string[] | Related to Role, many. Roles to grant on redemption. |
language | string | Language the invite was issued in. |
token | string | Indexed. The secret handed to the invitee. |
asTenantOwner | boolean | Grant tenant ownership on redemption. |
expiresAt | Date | Indexed. INVITE_EXPIRY_DAYS after creation. |
skipEmailValidation | boolean | Skip email validation when the invite is redeemed. |
Note the field is roles_ids, not roleIds — the membership field of that name lives on TenantMember (Tenancy).
UserInviteModel
UserInviteModel extends BasicDataModel(UserInvite, USER_INVITES_TABLE_NAME):
| Method | Returns |
|---|---|
getByEmail(email) | Promise<UserInvite | undefined> |
getByToken(token) | Promise<UserInvite | undefined> |
getByTokenWithTenant(token) | Promise<{ invite: UserInvite; tenantId: string } | undefined> |
deleteByToken(token) | Deletes every row carrying the token |
import { GetModel } from "@antelopejs/interface-database-decorators";
import { UserInviteModel } from "@antelopejs-private/cms/interfaces/cms/db";
const invites = GetModel(UserInviteModel, tenantId);
const invite = await invites.getByEmail("[email protected]");
if (invite && invite.expiresAt.getTime() < Date.now()) {
await invites.deleteByToken(invite.token);
}
getByTokenWithTenant reports the tenant the row came from, which is what a redemption flow needs: the person following an invite link is not authenticated yet, so there is no tenant claim to bind the handle to.
Next Steps
Hooks covers the invite and membership events these functions fire, and how to react to them.