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

Configuration

The package works with zero configuration — every key has a default. Publish config/events.php only to override:

php artisan vendor:publish --tag=events-config

Every value is read through a strict reader (Support\EventsConfig): a malformed duration, timezone, table name, URL template or class throws InvalidEventsConfigurationException (or the toolkit’s InvalidConfigurationException) naming the key — never a silent fallback.

  • A blank value means the default.
  • Booleans accept on/off, true/false, 1/0 and yes/no.
  • Durations are ISO-8601: PT30M, PT2H, P1D.
  • Set the key types and the table names before migrating — they shape the columns.

The config file

// config/events.php (imports and docblocks omitted)
return [
    'primary_key_type' => env('EVENTS_PRIMARY_KEY_TYPE', 'bigint'),
    'key_type' => env('EVENTS_KEY_TYPE', 'bigint'),

    'tables' => [
        'events' => 'events',
        'venues' => 'event_venues',
        'tiers' => 'event_admission_tiers',
        'rsvps' => 'event_rsvps',
        'hosts' => 'event_hosts',
        'invitations' => 'event_invitations',
        'reminder_deliveries' => 'event_reminder_deliveries',
    ],

    'models' => [
        'event' => Event::class,
        'venue' => Venue::class,
        'tier' => AdmissionTier::class,
        'rsvp' => Rsvp::class,
    ],

    'lifecycle' => EventLifecycle::class,

    'organizers' => ['user' => 'App\\Models\\User'],
    'attendees' => ['user' => 'App\\Models\\User'],

    'defaults' => [
        'timezone' => env('EVENTS_TIMEZONE'),
        'duration' => 'PT2H',
        'visibility' => 'public',
        'max_guests' => 0,
        'guest_list' => 'hidden',
        'show_counts' => true,
        'waitlist' => true,
    ],

    'schedule' => ['max_duration' => 'P31D'],

    'venues' => [
        'geocode_on_create' => env('EVENTS_GEOCODE_VENUES', 'off'),
        'default_timezone' => null,
    ],

    'geo' => ['max_radius_km' => 500, 'default_radius_km' => 25],

    'tags' => ['scope' => null],

    'tiers' => ['default_currency' => env('EVENTS_DEFAULT_CURRENCY')],

    'waitlist' => ['stop_promoting_before' => 'PT1H'],

    'payments' => [
        'collector' => NoPaymentCollector::class,
        'hold_for' => 'PT30M',
        'promotion_hold' => 'PT24H',
    ],

    'check_in' => [
        'opens_before' => 'PT3H',
        'closes_after' => 'PT2H',
        'url' => env('EVENTS_CHECK_IN_URL'),
    ],

    'poster' => [
        'bucket' => 'poster',
        'disk' => null,
        'max_kb' => 10240,
        'min_width' => 600,
        'min_height' => 315,
        'fallback_url' => env('EVENTS_POSTER_FALLBACK'),
        'variants' => [
            'thumb' => [400, 300],
            'card' => [800, 450],
            'square' => [1200, 1200],
            'landscape' => [1200, 900],
            'wide' => [1920, 1080],
        ],
    ],

    'urls' => [
        'resolver' => TemplateEventUrls::class,
        'event' => env('EVENTS_URL'),
        'event_link' => env('EVENTS_LINK_URL'),
        'invitation' => env('EVENTS_INVITATION_URL'),
        'rsvp' => env('EVENTS_RSVP_URL'),
    ],

    'seo' => [
        'json_ld' => [
            'type' => 'Event',
            'unlisted' => 'off',
            'expose_online_url' => 'off',
            'organizer_type' => 'Organization',
        ],
    ],

    'feeds' => [
        'keep_cancelled_for' => 'P30D',
        'max_events' => 500,
        'name' => env('APP_NAME', 'Events'),
    ],

    'resources' => [
        'organizer' => EventOrganizerResource::class,
        'host' => EventHostResource::class,
        'attendee' => EventAttendeeResource::class,
        'invitee' => EventInviteeResource::class,
        'actor' => EventActorResource::class,
        'venue_owner' => VenueOwnerResource::class,
    ],

    'routes' => [
        'enabled' => env('EVENTS_ROUTES') ?: 'off',
        'prefix' => 'api',
        'middleware' => ['api', 'throttle:events-api'],
        'auth_middleware' => ['auth'],
        'max_per_page' => 100,
        'rate_limits' => ['api' => 120, 'rsvp' => 20, 'check_in' => 300, 'feeds' => 60],
    ],

    'notifications' => [
        'enabled' => env('EVENTS_NOTIFICATIONS', 'on'),
        'channels' => ['mail'],
        'queue' => null,
        'notify_actor' => false,
        'types' => [
            'invited' => EventInvitationNotification::class,
            'confirmed' => RsvpConfirmedNotification::class,
            'waitlist_promoted' => WaitlistPromotedNotification::class,
            'reminder' => EventReminderNotification::class,
            'updated' => EventUpdatedNotification::class,
            'postponed' => EventPostponedNotification::class,
            'rescheduled' => EventRescheduledNotification::class,
            'cancelled' => EventCancelledNotification::class,
            'moved_online' => EventMovedOnlineNotification::class,
        ],
        'audiences' => [
            'reminder' => ['going'],
            'updated' => ['going', 'held'],
            'postponed' => ['going', 'held', 'waitlisted', 'interested'],
            'rescheduled' => ['going', 'held', 'waitlisted', 'interested'],
            'cancelled' => ['going', 'held', 'waitlisted', 'interested'],
            'moved_online' => ['going', 'held'],
        ],
    ],

    'reminders' => [
        'offsets' => [],
        'max_lateness' => 'PT1H',
        'all_day_at' => '09:00',
    ],

    'authorization' => [
        'register_policy' => true,
        'policy' => EventPolicy::class,
        'venue_policy' => VenuePolicy::class,
        'abilities' => [
            'create' => null,
            'admin' => 'events.admin',
            'venues' => null,
            'shared_venues' => null,
        ],
    ],

    'slugs' => [
        'history' => env('EVENTS_SLUG_HISTORY', 'off'),
        'max_length' => 120,
    ],
];

Keys, tables and models

KeyEnvDefaultPurpose
primary_key_typeEVENTS_PRIMARY_KEY_TYPEbigintKey of the events table and of every column pointing at an event — lifecycle subjects, tag taggables, media owners, slug history. bigint, uuid or ulid; must equal lifecycle.key_type and tags.key_type (and sluggable.key_type with slug history on). Set it before migrating.
key_typeEVENTS_KEY_TYPEbigintEvery column pointing at your models — organizer, attendee, co-host, invitee, inviter, venue owner, added_by, checked_in_by. Must equal your users’ key type and lifecycle.actor_key_type (and tags.key_type with organizer-scoped labels). Set it before migrating.
tables.events … tables.reminder_deliveries—events, event_venues, event_admission_tiers, event_rsvps, event_hosts, event_invitations, event_reminder_deliveriesThe seven table names, each ^[a-z_][a-z0-9_]{0,63}$. Rename before migrating — events is a common host table name.
models.event / venue / tier / rsvp—Event, Venue, AdmissionTier, RsvpA subclass of the package’s model; blank = the package’s. Anything else throws, naming the key.
lifecycle—Lifecycle\EventLifecycleThe status workflow: a LifecycleDefinition implementing Contracts\EventLifecycle — see Statuses and lifecycle.
organizers—['user' => 'App\Models\User']API alias => class implementing Organizer — also the classes managedBy() asks in SQL, so list every organizer class, teams included. A class that does not exist is skipped.
attendees—['user' => 'App\Models\User']API alias => attendee class; events:doctor warns about listed classes that cannot be notified. A class that does not exist is skipped.

Defaults for new events

KeyEnvDefaultPurpose
defaults.timezoneEVENTS_TIMEZONEnullUsed after an explicit timezone and the venue’s; blank = app.timezone. An invalid IANA name throws.
defaults.duration—PT2HISO-8601, when neither an end nor a duration is given — ends_at is never null.
defaults.visibility—publicpublic, unlisted or private.
defaults.max_guests—0Guests per RSVP.
defaults.guest_list—hiddenhidden, attendees or public.
defaults.show_counts—trueShow the going counts to viewers.
defaults.waitlist—trueA full event waitlists instead of refusing.
schedule.max_duration—P31DThe longest event.

Venues, geography and tags

KeyEnvDefaultPurpose
venues.geocode_on_createEVENTS_GEOCODE_VENUESoffQueue geocoding for every new venue without coordinates.
venues.default_timezone—nullA venue without a timezone takes this, else app.timezone.
geo.max_radius_km—500Largest near-me radius (0.001–20,038).
geo.default_radius_km—25Radius when none is given (≤ max_radius_km).
tags.scope—nullnull = one global label catalogue; organizer = one per organizer.

Ticket types, waitlist, payments, check-in

KeyEnvDefaultPurpose
tiers.default_currencyEVENTS_DEFAULT_CURRENCYnullA plain-number price is read in this currency; blank = money.default_currency.
waitlist.stop_promoting_before—PT1HNo automatic promotion this close to the start.
payments.collector—Payments\NoPaymentCollectorYour Contracts\CollectsAdmissionPayment.
payments.hold_for—PT30MHow long a paid seat is held for the payer — longer than zero.
payments.promotion_hold—PT24HHow long a seat promoted from the waitlist is held — longer than zero.
check_in.opens_before—PT3HDoor scans open this long before the start (or the doors time).
check_in.closes_after—PT2HAnd close this long after the end.
check_in.urlEVENTS_CHECK_IN_URLnullQR payload template with {code} (http or https); else the bare code.

Poster

KeyEnvDefaultPurpose
poster.bucket—posterThe media-library bucket.
poster.disk—nullBlank = media-library’s default disk.
poster.max_kb—10240Largest upload, at least 1.
poster.min_width / min_height—600 / 315Smallest dimensions, each at least 1.
poster.fallback_urlEVENTS_POSTER_FALLBACKnullposterUrl() for an event without a poster.
poster.variants—thumb 400×300, card 800×450, square 1200×1200, landscape 1200×900, wide 1920×1080name => [width, height], positive integers; webp, cover, queued. JSON-LD uses square, landscape and wide.

URLs, search engines, feeds

KeyEnvDefaultPurpose
urls.resolver—Support\TemplateEventUrlsYour Contracts\EventUrls, to call route().
urls.eventEVENTS_URLnullEvent page template: {uuid}, {slug}, {locale}.
urls.event_linkEVENTS_LINK_URLnullUnlisted and private events, uuid only: {uuid}, {locale}.
urls.invitationEVENTS_INVITATION_URLnullAccept link: {token}, {locale}.
urls.rsvpEVENTS_RSVP_URLnullThe attendee’s ticket page: {uuid}, {rsvp}, {locale}.
seo.json_ld.type—EventA schema.org type name (letters only).
seo.json_ld.unlisted—offStructured data for unlisted events too.
seo.json_ld.expose_online_url—offThe join link instead of the page URL in VirtualLocation.
seo.json_ld.organizer_type—OrganizationOr Person.
feeds.keep_cancelled_for—P30DCancelled events stay in the public feed this long.
feeds.max_events—5001–10,000.
feeds.nameAPP_NAMEEventsThe feed’s calendar name; blank = Events.

API resources and routes

KeyEnvDefaultPurpose
resources.organizer / host / attendee / invitee / actor / venue_owner—{id, type} resourcesAny instantiable JsonResource subclass; it receives the model — see JSON API.
routes.enabledEVENTS_ROUTESoffA switch that is not set stays off — also a published routes array without it.
routes.prefix—apiURL prefix.
routes.middleware—['api', 'throttle:events-api']Every route.
routes.auth_middleware—['auth']Write routes; the package also refuses writes without a signed-in Eloquent user.
routes.max_per_page—1001–1000 (the default page is 15 events, 25 attendees or venues).
routes.rate_limits.api / rsvp / check_in / feeds—120 / 20 / 300 / 60Per minute, each at least 1.

Notifications and reminders

KeyEnvDefaultPurpose
notifications.enabledEVENTS_NOTIFICATIONSonEvery notice on or off.
notifications.channels—['mail']For example ['mail', 'database'].
notifications.queue—nullThe queue the notices go to.
notifications.notify_actor—falseTell the person who made the change too.
notifications.types.<type>—the package’s classesA subclass of the package’s class, or false (off).
notifications.audiences.<type>—see NotificationsWho hears about an event-wide change, by RSVP status.
reminders.offsets—[]ISO-8601 offsets before the start; at most five, each PT5M–P31D. An event’s own list replaces it.
reminders.max_lateness—PT1HA reminder later than this is dropped.
reminders.all_day_at—09:00Local time (HH:MM) all-day events are reminded at.

Authorization and slugs

KeyEnvDefaultPurpose
authorization.register_policy—trueOff: register your own policies (a missing class is never a boot failure).
authorization.policy / venue_policy—Policies\EventPolicy / Policies\VenuePolicySwap with a subclass.
authorization.abilities.create—nullA Gate ability that may create events; null = any authenticated user.
authorization.abilities.admin—events.adminManage any event; undefined = nobody.
authorization.abilities.venues—nullA Gate ability that may create venues.
authorization.abilities.shared_venues—nullA Gate ability that creates and manages shared (ownerless) venues; null = admins only.
slugs.historyEVENTS_SLUG_HISTORYoffA changed slug keeps working and redirects (needs sluggable-migrations).
slugs.max_length—12016–255.

Environment

The common knobs are env-driven, so you rarely publish the config at all:

EVENTS_KEY_TYPE=uuid
EVENTS_TIMEZONE=Europe/Bratislava
EVENTS_DEFAULT_CURRENCY=EUR
EVENTS_GEOCODE_VENUES=on
EVENTS_URL=https://example.com/{locale}/events/{slug}
EVENTS_LINK_URL=https://example.com/{locale}/e/{uuid}
EVENTS_INVITATION_URL=https://example.com/invitations/{token}
EVENTS_RSVP_URL=https://example.com/events/{uuid}/tickets/{rsvp}
EVENTS_CHECK_IN_URL=https://example.com/check-in/{code}
EVENTS_ROUTES=on
EVENTS_NOTIFICATIONS=on
EVENTS_SLUG_HISTORY=off

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.