---
title: "Organizers, co-hosts and invitations — Events for Laravel | Roundly"
description: "Users and teams as organizers, who manages an event, co-hosts, organizer transfer and invite-only events with single-use, hashed tokens."
url: https://roundly-consulting.com/open-source/docs/events-for-laravel/organizers-invitations
language: en
---

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

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

# Organizers, co-hosts and invitations

## Organizers

The organizer is any model implementing Contracts\\Organizer — a user, a team, a venue company. Concerns\\OrganizesEvents provides the defaults:

| Method | Default |
| --- | --- |
| `eventOrganizerName(): string` | The name attribute, else the key (JSON-LD, calendar files, notices). |
| `eventOrganizerUrl(): ?string` | null (JSON-LD organizer.url). |
| `allowsEventManagementBy(Model $user): bool` | Only itself. |
| `scopeManagingEventsFor(Builder $query, Model $user)` | Only itself. |
| `eventOrganizerEmail(): ?string` | Optional, not in the contract — when present, calendar files carry ORGANIZER;CN=…:mailto:…. |

It also adds organizedEvents(), coHostedEvents() and organizeEvent($title), which is Events::make($title)->organizedBy($this):

```php
$user->organizedEvents()->upcoming()->get();
$user->coHostedEvents()->with('event')->get();
Event::query()->managedBy($user)->get();       // every event the user manages, drafts included
```

## Teams as organizers

Override the two management methods together — membership in PHP and in SQL — so managedBy() and the policy agree:

```php
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Events\Concerns\OrganizesEvents;
use RoundlyConsulting\Events\Contracts\Organizer;

final class Team extends Model implements Organizer
{
    use OrganizesEvents;

    public function allowsEventManagementBy(Model $user): bool
    {
        return $this->members()->whereKey($user->getKey())->exists();
    }

    public function scopeManagingEventsFor(Builder $query, Model $user): void
    {
        $query->whereHas('members', fn (Builder $members) => $members->whereKey($user->getKey()));
    }
}
```

Then list every organizer class under events.organizers. The SQL side — managedBy(), and through it visibleTo(), browse() and the API’s filter\[mine\]=organizing — asks each listed class, plus the user’s own class, for scopeManagingEventsFor(). A team class missing from the list still passes the policy in PHP, but its members never see its drafts and private events in lists. The teams-for-laravel version is on Recipes.

## Who manages an event

Managers are the organizer’s approved users, each co-host’s approved users and holders of events.authorization.abilities.admin (events.admin; undefined means nobody; an admin must be an Authenticatable). Organizing as a team needs the team’s approval for the actor, else InvalidOrganizerException.

## Co-hosts and transfer

```php
Events::for($event)->by($organizer)->hosts()->add($partnerTeam);   // manageHosts
Events::for($event)->hosts()->remove($partnerTeam);
Events::for($event)->hosts()->all();   // EventHost rows with ->host
Events::for($event)->hosts()->has($partnerTeam);

Events::for($event)->by($organizer)->transferTo($newOrganizer, keepAsHost: true);
```

- A co-host must be an Organizer (remove() takes any model). Co-hosts manage the event — ticket types, attendees, check-in — but cannot add or remove co-hosts or transfer the event: those are the organizer’s (manageHosts, transfer). CoHostAdded and CoHostRemoved fire; co-hosts can also be named at creation with ->coHostedBy().
- transferTo() hands the event over and fires OrganizerTransferred. The new organizer must allow the actor unless the actor is an admin. A new organizer who was a co-host stops being one; keepAsHost makes the old organizer a co-host. Transferring to the current organizer changes nothing.
- With events.tags.scope = organizer the event’s labels move into the new organizer’s catalogue in the same transaction — found there by name, or created with every translation.

## Invitations

```php
$invitation = Events::for($event)->by($organizer)->invitations()->send($user, 'See you there!');   // invite
$invitation->plainToken;   // once — the accept link's token; only its hash is stored

Events::for($event)->invitations()->revoke($user);
Events::for($event)->invitations()->all();      // ->pending(); invite with by($actor)
```

- On a private event an invitation is the admission; on a public or unlisted one it is an “invite a friend” notice.
- Sending again mints a new token and the old one dies — a revoked or declined invitation becomes pending again, an accepted one stays accepted. A message is at most 1000 characters.
- A link expires at the event’s current start ($invitation->expiresAt(), read from the schedule, never stored): a reschedule moves it, and while the event is postponed it does not expire.
- Invitations sent while the event is a draft go out when it is published — each pending one gets a fresh token and InvitationSent fires again; a token taken from plainToken on the draft stops working then.
- Revoking clears the token. On a private event it also takes away the invitee’s place unless they are already going: an interested, waitlisted or held RSVP becomes not\_going, with the seats back and the waitlist promoted.
- InvitationSent carries the token for the mail. The queued invited notice is encrypted (ShouldBeEncrypted), so jobs and failed\_jobs never hold it in the clear, and the database channel never stores it — encrypt your own queued listeners of InvitationSent the same way.

## Answering from the link

```php
use RoundlyConsulting\Events\Enums\RsvpResponse;

Events::invitations()->find($token);                              // the live invitation, or null
Events::invitations()->accept($token, RsvpResponse::Going, by: $request->user());   // RSVPs the invitee
Events::invitations()->decline($token, by: $request->user());
Events::invitations()->asSystem()->accept($token);               // trusted PHP: imports, seeders, tests
```

The token is a bearer secret used once, and the attendee is always the invitation’s own invitee, never the caller:

| Invitee | Who may answer |
| --- | --- |
| An account (an Authenticatable, e.g. your User) | Only that user, signed in: by must be the invitee. Signed out or signed in as someone else is refused — a forwarded mail cannot answer for them, nor see their check-in code. |
| Not an account (a Guest model — see Recipes) | Whoever holds the link: the token is the credential. |
| Any, from asSystem() | Trusted code, no user needed. |

The check runs before anything is written, so a refused answer leaves the token usable for the real invitee. The answer and the invitation write share one transaction, with the invitation re-read under its lock: a revocation lands either before (the answer is refused) or after (it stands) — never in between. Unknown, malformed, revoked, expired and used tokens and someone else’s link all read the same: InvalidInvitationException, a uniform 404. InvitationAccepted, InvitationDeclined and InvitationRevoked fire.

## Attendees without accounts

Any model can attend — list it under events.attendees, with the same key type as your users. Recipes has a Guest model for people your app knows only by name and e-mail.

## 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/events-for-laravel.md)
