---
title: "Time and timezones — Events for Laravel | Roundly"
description: "UTC storage, wall-clock input in each event’s timezone, DST gaps and overlaps, durations and doors, all-day and multi-day events and time scopes."
url: https://roundly-consulting.com/open-source/docs/events-for-laravel/time-and-timezones
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)

# Time and timezones

Every instant is stored in UTC — starts\_at, ends\_at, doors\_open\_at, the lifecycle stamps, RSVP times, created\_at — whatever app.timezone says, and every event carries its own IANA timezone. The UtcDateTime cast and StoresUtcTimestamps keep it that way even on an app running in America/New\_York:

```php
$event = Events::make('Jazz Night')->organizedBy($user)
    ->at($venue)                                // the venue's timezone: Europe/Bratislava
    ->startsAt('2026-11-14 20:00')              // wall clock there, stored as 19:00 UTC
    ->duration('PT3H')
    ->create();

$event->starts_at;          // CarbonImmutable in UTC
$event->startsAtLocal();    // CarbonImmutable in the event's own timezone
$event->starts_on;          // '2026-11-14' — the local calendar day

Events::make('Webinar')->organizedBy($user)->timezone('America/New_York')
    ->startsAt('2026-11-14T20:00:00+01:00')     // an offset (or Z, or a Carbon) is that instant
    ->create();
```

## Input

| You pass | Read as |
| --- | --- |
| `'2026-11-14 20:00'` | Wall clock in the event’s timezone (no offset). |
| `'2026-11-14T20:00:00+01:00', '…Z'` | That instant. |
| CarbonInterface | That instant. |

- The event’s timezone is, in order: an explicit ->timezone(), the venue’s, events.defaults.timezone, app.timezone.
- Setters can come in any order — ->startsAt()->timezone() and ->timezone()->startsAt() give the same instant — because the schedule is resolved on create().
- A wall-clock time inside a DST gap (02:30 on the spring-forward night) is refused with InvalidEventScheduleException, never silently moved. A time in a DST overlap (02:30 on the fall-back night) takes the earlier offset.
- starts\_on and ends\_on hold the local calendar days — ends\_on inclusive — derived on save, so “on this date” queries need no database timezone tables.
- API responses render times in ISO-8601 with the event’s own offset; notices show them in the event’s timezone with its abbreviation.

## End, duration and doors

\->endsAt(), or ->duration('3 hours'), 'PT3H' or a CarbonInterval; neither means events.defaults.duration. An event lasts at most events.schedule.max\_duration (P31D) and must end after it starts. ->doorsOpenAt() must fall within the 12 hours before the start; an all-day event has no doors time — giving it one is InvalidEventScheduleException, never silently dropped. A reschedule clears the doors time, because the old one could fall outside the new window — set it again with update().

## All-day and multi-day events

```php
Events::make('Festival')->organizedBy($user)->timezone('Europe/Bratislava')
    ->allDay('2026-07-17', through: '2026-07-19')
    ->create();
// starts and ends at local midnights; ends_on is 2026-07-19 (inclusive)
// calendar files use the exclusive end day (20260720), JSON-LD the inclusive one
```

An all-day event starts and ends at local midnights — the start of the first day and the start of the day after the last — correct even where a DST switch happens at midnight. It has no doors time and is reminded at events.reminders.all\_day\_at (09:00 local).

## Changing times

- On a draft, update() and edit() change anything. On an announced event a new start goes through reschedule(): attendees are told and the originally announced start is kept as previous\_starts\_at.
- A timezone change keeps a timed event’s instants — its local wall clock moves — and an all-day event’s days: its local midnights are re-derived in the new zone, so calendar files and JSON-LD keep the same dates.
- allDayThrough alone moves an all-day event’s last day (null makes it a one-day event); on a timed event it is InvalidEventScheduleException.
- A start, end or duration given to an all-day draft turns it into a timed event.

## Time scopes

```php
use RoundlyConsulting\Events\Models\Event;

Event::query()->upcoming();                              // not started yet
Event::query()->ongoing();                               // started and not ended
Event::query()->past();                                  // ended
Event::query()->between('2026-11-01', '2026-11-30');     // overlapping the range
Event::query()->startingBetween($from, $to);             // starting inside it (from inclusive, to exclusive)
Event::query()->onLocalDate('2026-11-14');               // taking place that day, each on its own calendar
```

The scopes compare in UTC. Naked strings in between() and startingBetween() are read in events.defaults.timezone (else the app’s); a Carbon is an instant. onLocalDate() reads each event on its own calendar, and a string must be a real Y-m-d date — 2027-02-30 is InvalidEventScheduleException, never read as another day.

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