# Events

2026-08-08. The fifth application, and the first one where the *data* is the
hard part rather than the plumbing. Blog proved the pattern — registries,
lazy comment topics, visibility in the service — so what follows is only the
parts where Events genuinely differs.

---

## The job

Somebody announces that a thing is happening at a time. Other people say
whether they are coming. It lives at its own address, has a page people can
read, and shows up on a calendar.

## 🚨 Time is the whole problem

Everything else here is Blog with different nouns. Time is not, and it is worth
being precise before writing a line:

**An event has a start and an end, and the end is optional.** "Friday at 8" is
a real thing people announce. An event with no end is not an error and must not
be stored as an end equal to its start — a calendar drawing a zero-length block
is a calendar with an invisible event on it.

**An event has a timezone, and it is not the reader's.** This is the one that
gets built wrong. A meetup in Chicago starts at 7pm *in Chicago*, whoever is
reading. So an event stores:

- `starts_at` and `ends_at` in **UTC**, which is what makes ordering, "what is
  next" and overlap queries correct;
- `timezone`, the zone it was announced in, which is what makes the *displayed*
  time right and survives daylight saving.

Storing a local time and a zone would mean every comparison had to convert
first. Storing UTC alone would mean an event announced for 7pm Chicago silently
becomes 6pm when the clocks change, because the offset was baked in at the
moment somebody typed it.

**An all-day event is not a time.** "The 14th" is a date, and rendering it as
"14 August, 00:00" is wrong in a way people notice immediately. A flag, and
the time is not shown.

🚨 **Convoro renders every other timestamp in the reader's timezone** — that is
what `Presence::format()` is for. Events are the exception, and the exception
has to be visible in the interface rather than merely correct underneath: the
time is shown in the event's zone, with the zone named next to it, plus what
that is for the reader when the two differ. Anything less produces somebody
turning up an hour late holding a screenshot that agreed with them.

## Going, maybe, not going

Three answers, not two. "Maybe" is the honest answer to most invitations and
leaving it out means everybody who is unsure says yes.

One row per person per event, changed rather than added to — the same shape as
a reaction, and for the same reason: the question "who is coming" has one
answer per person.

**Capacity is a limit, not a queue.** If an event is full, saying yes fails and
says so. A waiting list is a real feature with real questions — who gets
promoted, when, and who tells them — and it is not this note.

## What it registers

- **Content type**, so events are searchable and 🚨 filtered per viewer.
- **Widget kinds** — "What's coming up" and "Events this month".
- **Widget areas** on the calendar and on an event.
- **Personal data**, for events and for who said they were going.
- **A menu item**, because an application that ships without one is an
  application nobody finds.

## Comments reuse posts, again

Through `discussions`, exactly as Blog does. That service exists because of
Blog and this is the second caller — which is the test of whether it was the
right shape. It is: `open('event', $id, ...)`, and nothing about it knew what a
blog was in the first place.

## What this note does not decide

- **Recurring events.** Genuinely hard — the exception cases ("every Tuesday
  except the 3rd") are where calendar software goes to die — and a community
  that needs them will say so.
- **Reminders before an event.** Wanted, needs the mail queue and a decision
  about who is opted in.
- **iCal export.** Cheap and obvious once events exist; deliberately not built
  before there is anything to export.
