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-configEvery 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
| Key | Env | Default | Purpose |
|---|---|---|---|
primary_key_type | EVENTS_PRIMARY_KEY_TYPE | bigint | Key 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_type | EVENTS_KEY_TYPE | bigint | Every 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_deliveries | The 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, Rsvp | A subclass of the package’s model; blank = the package’s. Anything else throws, naming the key. |
lifecycle | — | Lifecycle\EventLifecycle | The 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
| Key | Env | Default | Purpose |
|---|---|---|---|
defaults.timezone | EVENTS_TIMEZONE | null | Used after an explicit timezone and the venue’s; blank = app.timezone. An invalid IANA name throws. |
defaults.duration | — | PT2H | ISO-8601, when neither an end nor a duration is given — ends_at is never null. |
defaults.visibility | — | public | public, unlisted or private. |
defaults.max_guests | — | 0 | Guests per RSVP. |
defaults.guest_list | — | hidden | hidden, attendees or public. |
defaults.show_counts | — | true | Show the going counts to viewers. |
defaults.waitlist | — | true | A full event waitlists instead of refusing. |
schedule.max_duration | — | P31D | The longest event. |
Venues, geography and tags
| Key | Env | Default | Purpose |
|---|---|---|---|
venues.geocode_on_create | EVENTS_GEOCODE_VENUES | off | Queue geocoding for every new venue without coordinates. |
venues.default_timezone | — | null | A venue without a timezone takes this, else app.timezone. |
geo.max_radius_km | — | 500 | Largest near-me radius (0.001–20,038). |
geo.default_radius_km | — | 25 | Radius when none is given (≤ max_radius_km). |
tags.scope | — | null | null = one global label catalogue; organizer = one per organizer. |
Ticket types, waitlist, payments, check-in
| Key | Env | Default | Purpose |
|---|---|---|---|
tiers.default_currency | EVENTS_DEFAULT_CURRENCY | null | A plain-number price is read in this currency; blank = money.default_currency. |
waitlist.stop_promoting_before | — | PT1H | No automatic promotion this close to the start. |
payments.collector | — | Payments\NoPaymentCollector | Your Contracts\CollectsAdmissionPayment. |
payments.hold_for | — | PT30M | How long a paid seat is held for the payer — longer than zero. |
payments.promotion_hold | — | PT24H | How long a seat promoted from the waitlist is held — longer than zero. |
check_in.opens_before | — | PT3H | Door scans open this long before the start (or the doors time). |
check_in.closes_after | — | PT2H | And close this long after the end. |
check_in.url | EVENTS_CHECK_IN_URL | null | QR payload template with {code} (http or https); else the bare code. |
Poster
| Key | Env | Default | Purpose |
|---|---|---|---|
poster.bucket | — | poster | The media-library bucket. |
poster.disk | — | null | Blank = media-library’s default disk. |
poster.max_kb | — | 10240 | Largest upload, at least 1. |
poster.min_width / min_height | — | 600 / 315 | Smallest dimensions, each at least 1. |
poster.fallback_url | EVENTS_POSTER_FALLBACK | null | posterUrl() for an event without a poster. |
poster.variants | — | thumb 400×300, card 800×450, square 1200×1200, landscape 1200×900, wide 1920×1080 | name => [width, height], positive integers; webp, cover, queued. JSON-LD uses square, landscape and wide. |
URLs, search engines, feeds
| Key | Env | Default | Purpose |
|---|---|---|---|
urls.resolver | — | Support\TemplateEventUrls | Your Contracts\EventUrls, to call route(). |
urls.event | EVENTS_URL | null | Event page template: {uuid}, {slug}, {locale}. |
urls.event_link | EVENTS_LINK_URL | null | Unlisted and private events, uuid only: {uuid}, {locale}. |
urls.invitation | EVENTS_INVITATION_URL | null | Accept link: {token}, {locale}. |
urls.rsvp | EVENTS_RSVP_URL | null | The attendee’s ticket page: {uuid}, {rsvp}, {locale}. |
seo.json_ld.type | — | Event | A schema.org type name (letters only). |
seo.json_ld.unlisted | — | off | Structured data for unlisted events too. |
seo.json_ld.expose_online_url | — | off | The join link instead of the page URL in VirtualLocation. |
seo.json_ld.organizer_type | — | Organization | Or Person. |
feeds.keep_cancelled_for | — | P30D | Cancelled events stay in the public feed this long. |
feeds.max_events | — | 500 | 1–10,000. |
feeds.name | APP_NAME | Events | The feed’s calendar name; blank = Events. |
API resources and routes
| Key | Env | Default | Purpose |
|---|---|---|---|
resources.organizer / host / attendee / invitee / actor / venue_owner | — | {id, type} resources | Any instantiable JsonResource subclass; it receives the model — see JSON API. |
routes.enabled | EVENTS_ROUTES | off | A switch that is not set stays off — also a published routes array without it. |
routes.prefix | — | api | URL 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 | — | 100 | 1–1000 (the default page is 15 events, 25 attendees or venues). |
routes.rate_limits.api / rsvp / check_in / feeds | — | 120 / 20 / 300 / 60 | Per minute, each at least 1. |
Notifications and reminders
| Key | Env | Default | Purpose |
|---|---|---|---|
notifications.enabled | EVENTS_NOTIFICATIONS | on | Every notice on or off. |
notifications.channels | — | ['mail'] | For example ['mail', 'database']. |
notifications.queue | — | null | The queue the notices go to. |
notifications.notify_actor | — | false | Tell the person who made the change too. |
notifications.types.<type> | — | the package’s classes | A subclass of the package’s class, or false (off). |
notifications.audiences.<type> | — | see Notifications | Who 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 | — | PT1H | A reminder later than this is dropped. |
reminders.all_day_at | — | 09:00 | Local time (HH:MM) all-day events are reminded at. |
Authorization and slugs
| Key | Env | Default | Purpose |
|---|---|---|---|
authorization.register_policy | — | true | Off: register your own policies (a missing class is never a boot failure). |
authorization.policy / venue_policy | — | Policies\EventPolicy / Policies\VenuePolicy | Swap with a subclass. |
authorization.abilities.create | — | null | A Gate ability that may create events; null = any authenticated user. |
authorization.abilities.admin | — | events.admin | Manage any event; undefined = nobody. |
authorization.abilities.venues | — | null | A Gate ability that may create venues. |
authorization.abilities.shared_venues | — | null | A Gate ability that creates and manages shared (ownerless) venues; null = admins only. |
slugs.history | EVENTS_SLUG_HISTORY | off | A changed slug keeps working and redirects (needs sluggable-migrations). |
slugs.max_length | — | 120 | 16–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=offShow 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.