payload-invitations
v0.3.0
Published
Payload CMS plugin that replaces the default user creation flow with a streamlined invitation system.
Readme
payload-invitations
Invite-only user onboarding for Payload CMS with email invitations, token-based acceptance, and automatic password setup.
Overview
Payload lets anyone with admin access create users, but there is no built-in way to invite someone by email and let them set their own password. This plugin turns user creation into an invitation flow: admins enter only an email, the plugin sends an invitation link, and the invitee chooses a password on a branded acceptance page. It hooks into Payload's existing email-verification mechanism so there is no extra collection to manage.
Features
- Email-only creation -- admins enter just an email. Password and auth fields are hidden automatically.
- Customizable invitation email -- full control over the email subject and HTML body.
- Branded acceptance page -- invitees set their own password and are logged in immediately.
- Headless support -- use your own frontend for the acceptance page with server-side utilities for token validation and invite acceptance.
Installation
pnpm add payload-invitationsUsage
// payload.config.ts
import { buildConfig } from "payload";
import { invitationsPlugin } from "payload-invitations";
export default buildConfig({
// ...
plugins: [invitationsPlugin()],
});To customize the invitation email:
invitationsPlugin({
generateInvitationEmailHTML: ({ invitationURL, user }) =>
`<p>Hi ${user.name}, <a href="${invitationURL}">accept your invitation</a>.</p>`,
generateInvitationEmailSubject: () => "You're invited!",
})Prerequisites: Your Payload config must have
admin.userset to a valid auth collection and an email adapter configured. The plugin warns and no-ops if either is missing.
Options
| Option | Type | Default | Description |
| ------------------------------- | ----------------------------------------------------------- | ------------------------------------- | ---------------------------------------------------- |
| acceptInvitationURL | string \| AcceptInvitationURLFn | Built-in admin page | Custom URL for the accept-invitation page. See Headless Usage. |
| emailSender | EmailSender \| EmailSenderOption | Payload email adapter defaults | Custom sender address and name for invitation emails. See Custom Email Sender. |
| generateInvitationEmailHTML | (args: { req, invitationURL, user }) => string \| Promise | Simple HTML with an acceptance link | Customize the invitation email body. |
| generateInvitationEmailSubject| (args: { req, invitationURL, user }) => string \| Promise | "You have been invited" | Customize the invitation email subject line. |
Custom Email Sender
By default, invitation emails are sent from the address configured in your Payload email adapter. To use a different sender (e.g., per-tenant branding in a multi-tenant setup), use the emailSender option:
// Static sender
invitationsPlugin({
emailSender: { email: "[email protected]", name: "Acme Corp" },
})For dynamic resolution (e.g., from a tenant document):
invitationsPlugin({
emailSender: async ({ req, user }) => {
const tenant = await req.payload.findByID({
collection: "tenants",
id: user.tenant,
});
return { email: tenant.senderEmail, name: tenant.senderName };
},
})Headless Usage
If you have a custom frontend for accepting invitations, set acceptInvitationURL to point invitation emails to your page instead of the built-in admin view:
invitationsPlugin({
acceptInvitationURL: "https://myapp.com/accept-invite",
})For dynamic URLs, pass a function:
invitationsPlugin({
acceptInvitationURL: ({ token, user, req, defaultURL }) => {
return `https://myapp.com/accept-invite?token=${token}`;
},
})On your custom page, use the server-side utilities to validate tokens and accept invitations:
import { getInviteData, acceptInvite } from "payload-invitations";
// Validate a token and get the invited user's data
const result = await getInviteData({ token, payload });
if (result.success) {
console.log(result.user.email);
} else {
console.log(result.error); // 'INVALID_TOKEN' | 'ALREADY_ACCEPTED'
}
// Accept the invitation (sets password, verifies user, logs in)
const acceptance = await acceptInvite({ token, password, payload });
if (acceptance.success) {
// Set the auth cookie in your response
cookies().set(
acceptance.cookie.name,
acceptance.cookie.value,
acceptance.cookie.options,
);
}Contributing
This plugin lives in the payload-plugins monorepo.
Development
pnpm install
# watch this plugin for changes
pnpm --filter payload-invitations dev
# run the Payload dev app (in a second terminal)
pnpm --filter sandbox devThe sandbox/ directory is a Next.js + Payload app that imports plugins via workspace:* -- use it to test changes locally.
Code quality
- Formatting & linting -- handled by Biome, enforced on commit via husky + lint-staged.
- Commits -- must follow Conventional Commits with a valid scope (e.g.
fix(payload-invitations): ...). - Changesets -- please include a changeset in your PR by running
pnpm release.
Issues & PRs
Bug reports and feature requests are welcome -- open an issue.
License
MIT
