[data-reveal]{opacity:1!important;transform:none!important}
CMS Interfaces

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

OptionTypeDescription
tenantIdstringTenant the person is invited into. Required.
emailstringEmail to look up and, failing that, to invite. Required.
firstnamestring | nullInvitee's first name, stored on the invite row and used to address the email. Defaults to null.
lastnamestring | nullInvitee's last name. Defaults to null.
languagestringLanguage stored on the invite row. Defaults to "en".
roleIdsstring[]Roles granted on join. Defaults to [].
asTenantOwnerbooleanGrant tenant ownership on join. Defaults to false.
skipEmailValidationbooleanStored on the invite row so redemption can skip email validation. Defaults to false.
sendEmailbooleanDispatch 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

ConstantValueMeaning
INVITE_TOKEN_LENGTH64Length of the generated token.
INVITE_EXPIRY_DAYS7Days 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.

FieldTypeNotes
_idstringInvite id — the inviteId the create functions return.
createdAtDateCreation time, indexed.
emailstringIndexed.
firstnamestring | nullInvitee's first name, when the invite carried one.
lastnamestring | nullInvitee's last name, when the invite carried one.
roles_idsstring[]Related to Role, many. Roles to grant on redemption.
languagestringLanguage the invite was issued in.
tokenstringIndexed. The secret handed to the invitee.
asTenantOwnerbooleanGrant tenant ownership on redemption.
expiresAtDateIndexed. INVITE_EXPIRY_DAYS after creation.
skipEmailValidationbooleanSkip 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):

MethodReturns
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.