NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Events for Laravel

Privacy and authorization

One rule (Support\EventVisibility) feeds both Event::scopeVisibleTo($viewer) — and with it Events::visibleTo() and Events::browse() — and EventPolicy::view, so a list and a detail page never disagree:

WhoDraftPublicUnlistedPrivate
Managers✓✓✓✓
Invitees (pending, accepted)—✓✓✓
Attendees (active RSVP)—✓✓✓
Anyone with the uuid link—✓✓—
Lists, search, feeds, slug lookups—✓managers, invitees, attendeesmanagers, invitees, attendees

Unlisted events are link-only: found by uuid, never by slug for strangers, never listed.

What each surface hides

SurfaceRule
browse(), the API index, searchVisible and listed and announced.
Route-model binding, Events::find()By uuid only, never the sequential id or the slug; no visibility rule — authorize view.
The APIA hidden event is a 404, exactly like a missing one, on every endpoint.
The join link (online_url)Encrypted at rest; only managers and going attendees (viewOnlineUrl) — API, calendar files, notices; never in feeds or JSON-LD unless expose_online_url.
Guest listguest_list: hidden (managers), attendees (going attendees), public (viewers). Interested people, the check-in record and the status / checked_in / tier filters are managers-only.
CountsManagers, or viewers when show_counts — the event’s counts and seats_left and each ticket type’s seats_left; sort=popularity ranks hidden counts as zero. Whether an event is full is public; the numbers are not.
Check-in codesEncrypted, hash-indexed, never in managers’ lists, domain events, logs or about; the QR is never cached.
Calendar filesEvents::ics() skips events the viewer may not see; a single event’s ics($viewer) throws the uniform EventNotFoundException; drafts are never exported.
JSON-LD and URL templatesPublic, announced events only (unlisted with seo.json_ld.unlisted), never private; unlisted and private events never get a {slug} URL.
NoticesRecipients filtered by visibility when sent and again when delivered; no other attendee’s data.
Public filters?organizer= (feed) and filter[organizer] (index) never look the organizer row up — no existence oracle for your users’ keys.
API writesNever without a signed-in Eloquent user (401), whatever events.routes.auth_middleware holds.

The policy

Policies\EventPolicy and Policies\VenuePolicy are registered unless events.authorization.register_policy is off. Events::for($event)->by($user) checks the policy before every call — the writes and the reads that reveal people; without by() the call is trusted code:

AbilityWho
viewAnyAnyone, guests included.
viewThe visibility rule above.
createAuthenticated, plus events.authorization.abilities.create when set.
update, delete, restore, publish, unpublish, postpone, reschedule, cancel, complete, transition, moveOnline, manageTiers, invite, manageAttendees, checkIn, viewWaitlistManagers.
manageHosts, transferThe organizer’s approved users, or an admin.
rsvpview + the phase and window rules + the private-event invitation rule.
viewAttendeesManagers; going attendees with guest_list = attendees; viewers with public.
viewInterestedManagers.
viewCountsManagers, or viewers with show_counts.
viewOnlineUrlManagers and going attendees.

An undefined ability denies — with events.admin undefined, nobody is an admin. Swap the policy with events.authorization.policy, extending the package’s class. With registration off and no policy of yours, by() still never silently allows: the configured class is asked directly.

Venues

Anyone views a venue, and any organizer may hold an event at any venue. Creating needs an authenticated user, plus abilities.venues when set. An owned venue is managed by its owner’s approved users and admins and created only by someone the owner allows, so nobody plants a venue into another team’s list. A shared venue sits under other organizers’ events, so only admins and holders of abilities.shared_venues create, edit, geocode or delete one.

Visitor IPs

Geolocation::locateRequest($request), the usual near-me starting point, sends the visitor’s IP address to geolocation-for-laravel’s IP providers. Mention it in your privacy notice, or ask for the location instead.

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

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