Build Guide
How to Build a Calendar App
Calendar apps are deceptively hard — timezone handling alone has broken teams of senior engineers at Google and Microsoft.
Building a calendar forces you to understand date/time arithmetic, recurring event logic, iCal format, conflict detection, and Google Calendar API OAuth — skills that appear in almost every enterprise SaaS.
Data model
The core tables you'll need before writing any UI.
Build order
The sequence that minimises rewrites — build in this order.
Month view grid
Build a 7×5 CSS grid for the month view. Populate each cell with the day number. Show today highlighted. Use date-fns to calculate the first day of the month and pad with days from the previous month.
Create and show events
Add a click handler on each day cell that opens a modal to create an event. Store events with start_at and end_at as UTC ISO strings. Render event pills on the correct day cells.
Week and day views
Build a week view: 7 columns with hourly time slots. Position events as absolute divs based on their start time offset and height based on duration. Day view is the same with one column.
Timezone handling
All events are stored in UTC. Use date-fns-tz to convert to the user's timezone for display. When creating an event, convert the user's local time back to UTC before saving. Never store local times.
Recurring events
Store recurring events as RRULE strings (RFC 5545 standard). Use the rrule.js library to expand them into occurrences for the current view range. Never materialise all occurrences in the DB — generate them on the fly.
Google Calendar OAuth + sync
Register a Google OAuth app. Request the calendar.events scope. On connect: fetch all events from the Google Calendar API and import them. On new event creation in your app, create it via the Google API and store the google_event_id.
Invites and RSVP
On event create, allow adding attendee emails. Send invite emails via Resend with Accept/Decline links. Each link hits /api/rsvp/[token]?status=accepted which updates the attendee row and sends a confirmation.
Done when
Observable behaviors that confirm V1 is complete — verify each one before you ship.
- ✓
Creating an event places it on the correct day cell without a page refresh
- ✓
A weekly recurring event appears on all correct days in the current month view
- ✓
An event created at 9am EST displays as 2pm GMT when viewed by a UK user
- ✓
Google Calendar event appears on the provider's calendar within 30 seconds of creation
- ✓
App opens with 3 seeded events this week — calendar is never empty on first login
First Run Requirement
Seed 3 events for the current week (Team standup, Dentist, Weekly review) using relative dates (today+1, today+3, etc.) — not hardcoded dates — so they always land in the current week.
Build it with AI — Architect Prompt
Paste this into Claude, Cursor, Windsurf, or any AI coding tool. It includes the full context of this guide — data model, build order, done conditions, and pitfalls — so the AI starts with everything it needs.
<context>
App: Calendar App
Difficulty: Intermediate
Estimated build time: 1–2 weeks
Tech stack (intermediate): Next.js + Supabase + FullCalendar + Google Calendar API + date-fns-tz
Data model:
User: id, email, timezone, google_calendar_token
Event: id, user_id, title, description, start_at (UTC), end_at (UTC), all_day (bool), recurrence_rule (RRULE string), google_event_id, color, created_at
Attendee: event_id, email, status (accepted/declined/pending)
FIRST RUN REQUIREMENT:
Seed 3 events for the current week (Team standup, Dentist, Weekly review) using relative dates (today+1, today+3, etc.) — not hardcoded dates — so they always land in the current week.
DONE WHEN — verify each before marking V1 complete:
□ Creating an event places it on the correct day cell without a page refresh
□ A weekly recurring event appears on all correct days in the current month view
□ An event created at 9am EST displays as 2pm GMT when viewed by a UK user
□ Google Calendar event appears on the provider's calendar within 30 seconds of creation
□ App opens with 3 seeded events this week — calendar is never empty on first login
Recommended build order:
1. Define API contract + schema + seed data (always first)
2. Month view grid — Build a 7×5 CSS grid for the month view. Populate each cell with the day number. Show today highlighted. Use date-fns to calculate the first day of the month and pad with days from the previous month.
3. Create and show events — Add a click handler on each day cell that opens a modal to create an event. Store events with start_at and end_at as UTC ISO strings. Render event pills on the correct day cells.
4. Week and day views — Build a week view: 7 columns with hourly time slots. Position events as absolute divs based on their start time offset and height based on duration. Day view is the same with one column.
5. Timezone handling — All events are stored in UTC. Use date-fns-tz to convert to the user's timezone for display. When creating an event, convert the user's local time back to UTC before saving. Never store local times.
6. Recurring events — Store recurring events as RRULE strings (RFC 5545 standard). Use the rrule.js library to expand them into occurrences for the current view range. Never materialise all occurrences in the DB — generate them on the fly.
7. Google Calendar OAuth + sync — Register a Google OAuth app. Request the calendar.events scope. On connect: fetch all events from the Google Calendar API and import them. On new event creation in your app, create it via the Google API and store the google_event_id.
8. Invites and RSVP — On event create, allow adding attendee emails. Send invite emails via Resend with Accept/Decline links. Each link hits /api/rsvp/[token]?status=accepted which updates the attendee row and sends a confirmation.
9. End-to-end verification — walk every Done When condition above (always last)
Known pitfalls to avoid:
- Materialising all recurring event occurrences in the database — a "repeat forever" event would generate infinite rows. Store the RRULE string and use rrule.js to expand it for the current view window only.
- Comparing dates without timezone awareness — new Date("2026-04-09") in JavaScript treats the string as UTC midnight, which renders as the previous day in western timezones. Always use date-fns-tz for timezone-aware comparisons.
- Fetching all events for all time on load — query only events within the current view range (month start to month end + padding). A user with 5 years of events should not have them all loaded on the first render.
</context>
<role>
You are a Senior Full-Stack Engineer and product architect who has shipped production Calendar Apps before. You know exactly where developers get stuck and how to structure the project to avoid rewrites.
</role>
<task id="step-1-clarify">
Before writing any code or spec, ask me 3–5 clarifying questions that will meaningfully change the architecture. Focus on: scale expectations, auth requirements, platform (web / mobile / both), must-have vs nice-to-have features for the MVP, and any hard constraints (budget, deadline, existing infrastructure).
<example>
BAD: "What tech stack do you want to use?" — too broad, doesn't change architecture decisions.
GOOD: "Do you need real-time sync across devices, or is single-device with periodic refresh acceptable? This decides whether we use WebSockets or simple REST polling and significantly affects infrastructure complexity."
</example>
⚠️ Do NOT start planning or writing code until I answer. Present the questions, then stop and wait.
</task>
<task id="step-2-architect">
After I answer your questions, produce the following in order:
1. TECHNICAL SPECIFICATION
- System architecture using → to show data and event flow
- Core data model (refer to data model in <context>)
- API contract — list ALL routes with request/response shapes BEFORE any implementation
WHY: agreeing on contracts first prevents rewrites when frontend and backend shapes diverge
- External APIs and integration points
2. MVP PLAN in three phases:
SETUP ✦ Step 1 (always): API contract + schema migration + seed data
Done when: schema is migrated, seed script runs, app starts with demo data, zero manual setup.
CORE FEATURES — build in this exact order:
2. Month view grid
3. Create and show events
4. Week and day views
5. Timezone handling
6. Recurring events
7. Google Calendar OAuth + sync
8. Invites and RSVP
DONE WHEN — one observable condition per feature:
□ Creating an event places it on the correct day cell without a page refresh
□ A weekly recurring event appears on all correct days in the current month view
□ An event created at 9am EST displays as 2pm GMT when viewed by a UK user
□ Google Calendar event appears on the provider's calendar within 30 seconds of creation
□ App opens with 3 seeded events this week — calendar is never empty on first login
STRETCH GOALS — post-launch additions that are explicitly out of V1 scope.
3. BLOCKER ANALYSIS
Flag: API rate limits, auth complexity, scope risks, cold-start problems, top 2–3 failure modes.
FIRST RUN REQUIREMENT: Seed 3 events for the current week (Team standup, Dentist, Weekly review) using relative dates (today+1, today+3, etc.) — not hardcoded dates — so they always land in the current week.
✓ Done when: app launches with seed data and the full user journey works without any manual setup.
<self_check>
Before presenting your output, verify:
□ Every answer I gave to clarifying questions is reflected in the spec
□ Step 1 is ALWAYS: API contract + schema + seed — never UI first
□ Done When criteria are observable user behaviors, not internal states
□ The plan is realistic for one person to ship within 1–2 weeks
□ All known pitfalls from <context> are addressed in the spec
</self_check>
</task>
<task id="step-2.5-agents-md">
Generate an AGENTS.md file at the project root. This file is read automatically by Claude Code, Cursor, Windsurf, and all major AI coding tools at the start of every session.
Include:
- Project overview (2–3 sentences)
- Tech stack with exact version numbers
- Folder structure with one-line descriptions per directory
- Key architectural decisions and the WHY behind each
- Coding conventions: camelCase components, kebab-case files, max 200 lines per file, one concern per file
- Available commands: dev, build, test, lint, db:migrate, db:seed
- MVP scope boundaries — features explicitly out of V1
Note in the file: "Cursor users: symlink .cursorrules → AGENTS.md. Claude Code users: symlink CLAUDE.md → AGENTS.md."
</task>
<task id="step-3-implement">
Implement the full project in the exact order from step 2. For each step:
- Write complete code (no placeholders or TODOs)
- Confirm the step works before moving to the next
<constraints>
- Step 1 is ALWAYS: define all API routes + TypeScript request/response interfaces + run schema migration
WHY: agreeing on contracts before writing a single component prevents shape mismatches that require rewrites
- Final step is ALWAYS: start the app and walk every Done When condition from <context> end-to-end
WHY: a spec that passes unit tests but breaks the core user journey is not done
- Max 200 lines per file. WHY: every file must fit in one AI context window for complete reasoning
- One concern per file. WHY: mixing auth logic into API routes makes it impossible to reuse — extract to lib/auth.ts
- TypeScript strict mode, no `any` types. WHY: catches data shape mismatches at compile time, not in production
- All database queries in server components or API routes only. WHY: keeps credentials server-side
- All environment variables documented in .env.example. WHY: next developer sets up in under 5 minutes
</constraints>
</task>How to use this prompt
- 1.Copy the prompt above
- 2.Open Claude, Cursor (Cmd+L), or Windsurf
- 3.Paste the prompt and send — the AI will ask 3–5 clarifying questions
- 4.Answer the questions, then it generates your full project spec
- 5.Continue in the same session to implement step by step
Common mistakes
What trips up most developers building this for the first time.
Materialising all recurring event occurrences in the database — a "repeat forever" event would generate infinite rows. Store the RRULE string and use rrule.js to expand it for the current view window only.
Comparing dates without timezone awareness — new Date("2026-04-09") in JavaScript treats the string as UTC midnight, which renders as the previous day in western timezones. Always use date-fns-tz for timezone-aware comparisons.
Fetching all events for all time on load — query only events within the current view range (month start to month end + padding). A user with 5 years of events should not have them all loaded on the first render.
Recommended tech stack
Pick the level that matches your experience.
React + FullCalendar.js — full calendar UI in an afternoon with a pre-built component
Next.js + Supabase + FullCalendar + Google Calendar API + date-fns-tz
Next.js + Supabase + custom calendar renderer + CalDAV server + conflict detection
Take it further — related ideas
Each comes with revenue math, a full build guide, and a prompt to paste into Claude or Cursor.