Registration & invitations
Registration has three modes — open, invite_only and closed (the default); closed also refuses accepting invitations, so use invite_only for invitation-only sign-up. Invitations are off until enabled — until then every invitations() call throws LoginMethodDisabled (404 method_disabled):
'registration' => [
'mode' => env('AUTHENTICATION_REGISTRATION', 'closed'), // open|invite_only|closed
'require_password' => true, // when login.password is on
'login_after' => true,
'rules' => null, // class-string<ProvidesRegistrationRules>|null
'creator' => CreateAccount::class, // class-string<CreatesAccounts>
],
'invitations' => [
'enabled' => false,
'ttl' => 604_800, // 7 days
'lock_email' => true,
'replace_pending' => true,
'allow_existing_email' => false,
'resend_cooldown' => 60,
'max_sends' => 5,
'preview_payload_keys' => [], // payload keys exposed by the preview endpoint
'ability' => 'authentication.invitations.manage', // Gate ability for management routes
],Registering
use RoundlyConsulting\Auth\DataTransferObjects\RegistrationData;
use RoundlyConsulting\Auth\Enums\RegistrationStatus;
use RoundlyConsulting\Auth\Facades\Authentication;
use RoundlyConsulting\Auth\Http\Responses\LoginResponse;
$guard = Authentication::guard('users');
$result = $guard->register(new RegistrationData(
email: $request->input('email'),
password: $request->input('password'),
context: $guard->contextFrom($request),
attributes: $request->input('attributes', []),
));
if ($result->status === RegistrationStatus::Authenticated) {
return LoginResponse::make($result->login, $request); // tokens, or a challenge
}
// VerificationRequired or Accepted — answer 202, identical for new and taken addresses- closed answers registration_closed; invite_only answers invitation_required.
- Throttled per IP. The email, the password (per the policy) and your extra attributes are validated — only keys your rules name reach the creator.
- An address already in use: in the enumeration-safe modes (verification required for login, or login_after off) that address gets an account-exists mail and the caller gets exactly the status a new account would; otherwise email taken.
- AccountRegistered fires; when verification is not off, a verification mail goes out.
- Registration never skips required two-factor enrolment, also with two_factor.after_email_login off.
- The result is authenticated (possibly a challenge), verification_required (202) or accepted (202).
Your own fields and creator
registration.rules adds host fields to sign-up:
use RoundlyConsulting\Auth\Contracts\ProvidesRegistrationRules;
use RoundlyConsulting\Auth\Guards\GuardConfig;
// config: 'guards' => ['users' => ['registration' => ['rules' => ProfileRules::class]]]
final class ProfileRules implements ProvidesRegistrationRules
{
public function rules(GuardConfig $guard): array
{
return ['name' => ['required', 'string', 'max:100']];
}
}registration.creator swaps account creation: implement CreatesAccounts::create(GuardConfig $guard, NewAccountData $data): Account. NewAccountData carries the email, password, locale, timezone, emailVerified flag and the allow-listed attributes. The same creator runs on invitation acceptance.
Invitations
use RoundlyConsulting\Auth\DataTransferObjects\AcceptInvitationData;
use RoundlyConsulting\Auth\DataTransferObjects\InvitationData;
use RoundlyConsulting\Auth\Facades\Authentication;
$guard = Authentication::guard('users');
$invitations = $guard->invitations();
// Invite — payload is your own role/meta JSON, surfaced on acceptance
$link = $invitations->create(new InvitationData(
email: '[email protected]',
payload: ['role' => 'editor'],
invitedBy: $admin,
locale: 'sk',
));
$link->invitation; // the Invitation model
$link->url; // the plaintext link, returned once — mailed to the invitee unless send: false
// Accept — creates a verified account and logs it in, like register()
$result = $invitations->accept(new AcceptInvitationData(
token: $request->input('token'),
password: $request->input('password'),
context: $guard->contextFrom($request),
)); // RegistrationResult — ->login holds the tokens or a challenge- Creating normalises the address, refuses an existing account’s address unless allow_existing_email, and with replace_pending revokes the pending invitation for the address.
- Every send issues a fresh token, so the previous link dies; resend() returns the new InvitationLink and honours resend_cooldown and max_sends.
- send: false creates the invitation and returns its link without mailing it — deliver $link->url yourself. link() mints a fresh copyable link without mailing; the previous link, a mailed one too, dies. Neither counts as a send: send_count, last_sent_at, the resend cooldown, max_sends and InvitationSent track real mails only.
- Preview returns the pending invitation — email, guard, expires_at and only the preview_payload_keys.
- Accepting claims the invitation atomically, creates the account verified (with lock_email off, a different address is stored unverified and gets a verification mail), fires AccountRegistered and InvitationAccepted, then logs in.
- Status is derived: pending, accepted, revoked or expired.
Managing invitations
use RoundlyConsulting\Auth\DataTransferObjects\InvitationData;
use RoundlyConsulting\Auth\Enums\InvitationStatus;
use RoundlyConsulting\Auth\Events\InvitationAccepted;
use RoundlyConsulting\Auth\Facades\Authentication;
$invitations = Authentication::guard('users')->invitations();
$invitations->paginate(InvitationStatus::Pending, 20); // newest first
$invitations->find($id); // ?Invitation of this guard
$invitations->resend($invitation); // InvitationLink — mails a fresh link; the previous one dies
$invitations->link($invitation); // a fresh copyable link, nothing mailed or counted
$invitations->revoke($invitation); // idempotent
// Create without mailing — deliver $link->url yourself (no send is counted)
$link = $invitations->create(new InvitationData('[email protected]', send: false));
Invitation::query()->where('guard', 'users')->withStatus(InvitationStatus::Pending)->get();
Event::listen(fn (InvitationAccepted $e) => $e->account->assignRole($e->invitation->payloadValue('role')));Another guard’s invitation — as a model or an id — is InvitationNotFound, and only a pending invitation gets a new link. Over HTTP, POST invitations answers 201 with data and the link as url (shown once, mailed or not), and resend answers {"status": "sent", "url": …}. The management routes (the invitations.manage group) require routes.invitations_management and the Gate ability in invitations.ability. Read $invitation->payload in an InvitationAccepted listener to assign roles.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.