---
title: "Registration & invitations — Auth for Laravel | Roundly"
description: "Open, invite-only or closed sign-up with your own fields and account creator, plus invitations with payloads, previews and resend limits."
url: https://roundly-consulting.com/open-source/docs/auth-for-laravel/registration-invitations
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Auth for Laravel](https://roundly-consulting.com/open-source/docs/auth-for-laravel.md)

# 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):

```php
'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

```php
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 in the holder’s locale (at most once per 10 minutes, a cooldown shared with invitation acceptance and email change) 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:

```php
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

```php
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: 'new.hire@example.com',
    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. Inside resend\_cooldown it answers too\_many\_attempts with the seconds left; past max\_sends it throws InvitationSendLimitReached (422 invitation\_send\_limit) — final, since the count never resets, so revoke the invitation and create a new one. Concurrent resends mail once.
- 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. An address already in use rolls the claim back with a uniform invalid\_invitation and gets the account-exists mail (in the holder’s locale, else the invitation’s), under the same per-address cooldown as registration.
- Status is derived: pending, accepted, revoked or expired.

## Managing invitations

```php
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('ada@example.com', 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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

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

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/auth-for-laravel.md)
