How to Add a Calendar View to an AI-Built App
The grid is the easy part. Recurrence, time zones and all-day events break later, and they break because of how the records were stored.
Adding a calendar view to an app looks like a UI problem and is actually a data problem. The grid is the easy part, and an AI agent will produce a decent one quickly. What breaks three weeks later is recurrence, time zones and all-day events, and those break because of how the records were stored, not how they were drawn.
Get the storage right and the view is straightforward. Get it wrong and every fix makes the next bug worse.
Store instants, not wall-clock strings
The single most consequential choice is how a start time is stored.
Store a timestamp with time zone, in UTC, plus a separate column naming the event's own time zone, for example Europe/Zurich. You need both, and the reason is not obvious until it bites.
The UTC instant tells you when the event happens on a universal clock, which is what you sort and filter by. The named zone tells you what the user meant, which is what you need when the rules change. If someone schedules a 9am weekly meeting in Zurich in January, that is 08:00 UTC. After the clocks change in March it is 07:00 UTC. The meeting is still at 9am. If you only stored the UTC instant, your calendar now shows it an hour early forever.
Store an offset like +01:00 instead of a zone name and you have the same bug, because an offset is a fact about one moment while a zone is a rule that changes. The IANA time zone database is the thing those names refer to, and it updates several times a year.
All-day events are the exception and they catch everyone. A birthday on 14 March is not an instant, it is a date. Store it as a plain date with a flag, and never convert it to UTC, or users east of you will see it on the 13th.
create table events (
id uuid primary key default gen_random_uuid(),
title text not null,
starts_at timestamptz, -- null for all-day
ends_at timestamptz,
event_date date, -- used when all_day is true
all_day boolean not null default false,
time_zone text not null default 'UTC', -- IANA name, never an offset
rrule text, -- RFC 5545 recurrence rule, null if one-off
recurrence_end timestamptz
);Do not invent a recurrence format
Every developer who builds a calendar reaches the point of writing their own repeat system: a repeat_type column with values like daily, weekly, monthly. It works until someone wants the last Friday of every month, or every other Tuesday, or weekly except for one cancelled instance.
That problem is solved. RFC 5545 defines the recurrence rule format that every calendar application already speaks, and it expresses all of the above:
FREQ=MONTHLY;BYDAY=-1FR last Friday of each month
FREQ=WEEKLY;INTERVAL=2;BYDAY=TU every other Tuesday
FREQ=DAILY;COUNT=10 ten days runningStore the rule string. Use a library to expand it into dates. The practical gain is that import and export to other calendars becomes nearly free, and you never have a conversation about whether your custom format supports fortnightly.
An AI agent asked to "add recurring events" will almost always invent the enum version, because it is simpler and it demos fine. Name RFC 5545 in the prompt.
Expand occurrences at read time
A recurring event is one row. The calendar needs many boxes. The question is when you turn one into many.
Expanding at write time, by inserting a row for every occurrence, seems simpler and goes wrong immediately: a weekly event with no end date is infinite rows, and editing the series means rewriting all of them.
Expand at read time instead. The view asks for a date window, you fetch rules that could overlap it, expand them within that window only, and merge with the one-off events. The window bounds the work, so a ten-year-old weekly meeting costs the same to render as a new one.
The piece you must add is an exceptions table. When a user deletes or moves a single occurrence of a series, that is not a change to the rule. Record it separately:
create table event_exceptions (
event_id uuid references events(id) on delete cascade,
original_start timestamptz not null, -- the occurrence being overridden
cancelled boolean default false,
new_start timestamptz, -- set when moved rather than cancelled
primary key (event_id, original_start)
);Without this table, "delete just this one" has no correct implementation, and it is the first thing a real user tries.
Then add the calendar view
With that storage in place, the UI is genuinely simple.
The view sends a start and end date for the visible window, plus the viewer's time zone.
The server returns occurrences already resolved to instants, each with its own time zone name attached.
The client renders in the viewer's zone, and shows the event's zone when it differs. A meeting at 15:00 your time that is 09:00 for the organiser should say so.
Paginate by window, never by row count. Month, week and day views are three window sizes against one endpoint.
Decide early whether the week starts on Monday or Sunday, and make it a user setting rather than a constant. It is a one-line change now and a layout rewrite later.
Before you ship
Create an event that crosses a daylight saving boundary and confirm it still shows the right local time afterwards.
Create an all-day event and view it from a different time zone.
Delete one occurrence of a weekly series and confirm the rest survive.
Move one occurrence and confirm the series rule did not change.
Render a month containing a hundred overlapping events and see what the layout does.
Those five tests catch nearly everything that reaches production in calendar features.
Related
The storage thinking here is the same discipline as planning your data model before building with AI, and handling time zones goes deeper on the parts that apply beyond calendars. If the calendar drives reminders, pair it with scheduled tasks. For a full worked feature, building a booking app with AI uses most of this, and how to build an app with AI is the starting point.
Questions people ask
Should I store event times in UTC?
Store the UTC instant and the IANA time zone name. The instant alone loses what the user meant once the clocks change.
How should recurring events be stored?
As an RFC 5545 recurrence rule string on one row, expanded into occurrences at read time, with a separate exceptions table for single occurrences that were moved or cancelled.
Why does my all-day event show on the wrong day?
Because it was stored as a timestamp and converted to UTC. All-day events are dates, not instants, and should never be converted.
Should I generate a row per occurrence?
No. Infinite series cannot be materialised, and editing a series means rewriting every row. Expand within the requested date window instead.
Can AI build a calendar view correctly?
It builds the grid well. It tends to store wall-clock strings and invent a custom repeat enum. Specify timestamptz plus IANA zone names and RFC 5545 in the prompt.
How did this land?
About the author

Developer Advocate
Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.


