Build Fantastical
YESreplaces $4.75/mosaves $57/yrback to the verdict
A calendar client that connects to existing accounts over CalDAV, parses a typed sentence into a correctly timed event, renders day, week and month views, handles recurrence and exceptions properly, and writes back so the event appears on every other device the user owns.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the build and the desktop shell.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - accountfree with an existing account
Why Provider quirks are the substance of Phase 1 and cannot be discovered against a local test server.
Get it Use an account you already have. Some providers require an application-specific password rather than your main one, so generate that first.
Verify
A client can list calendars and fetch events - have readyfree
Why Phases 1 and 4 both test propagation and conflicts, which need a genuinely separate client.
Get it Your phone or another computer with the same account configured in its own calendar application.
Verify
An event created on one appears on the other - decidefree
Why Recurrence across clock changes is the single hardest correctness problem here, and a library without full zone data cannot get it right.
Get it Choose a library with complete zone support rather than one that relies on the platform, and pin its version so a zone data update is a deliberate upgrade.
Verify
A test event across a daylight saving transition behaves correctly - have readyfree
Why Phase 5's performance work needs it, and every naive approach feels fine on a week of events.
Get it Generate it, including several recurring series and some overlapping events.
Verify
The fixture loads and month view can be measured against it
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- accounts(id, name, url, username, credential_ref, sync_token, last_synced_at) - calendars(id, account_id, href, name, color, read_only, ctag) - events(id, calendar_id, uid, etag, summary, location, starts_at_utc, ends_at_utc, timezone, all_day, rrule, rdates, exdates, sequence) - occurrences(event_id, occurrence_start_utc, overridden_event_id) · expanded for the visible range only, never for all time - pending_changes(id, event_id, operation, payload, attempts, last_error) · the outbox that makes offline editing safe - Every timestamp is stored in coordinated universal time alongside its original zone, because a recurring event at nine in the morning stays at nine when the clocks change and a fixed instant does not.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DB_PATH | required | ./data/calendar.db | Local cache of accounts, calendars and events. Rebuildable from the server. |
SYNC_INTERVAL_SECONDS | required | 300 | How often to poll for changes. Too frequent is rude to the provider and rarely necessary. |
EXPAND_RANGE_DAYS | required | 120 | How far around the visible range to expand recurrence. Larger costs memory and time. |
REMINDER_LOOKAHEAD_MINUTES | required | 10080 | How far ahead notifications are scheduled. Must exceed your expansion refresh interval or reminders stop. |
DEFAULT_TIMEZONE | required | Europe/London | Used when an event carries no zone. Should match the machine unless the user overrides it. |
The build, in order
Sync that does not lose events
Your real calendar appears, and a change made elsewhere shows up.
Principal discovery then calendar listing. Providers differ here more than the specification suggests, so test against the account you actually use.
Files
src/caldav/discover.tsThe collection sync report gives changes since a token. Store etags so you can detect conflicts later.
Files
src/caldav/sync.tsFall back to a full sync. Test it by corrupting the stored token deliberately.
Files
src/caldav/sync.ts
done when · tick each as it passeswatch out- Servers invalidate sync tokens without warning, and a client that treats an invalid token as an error stops syncing silently until someone notices missing events. Detect the specific response and fall back to a full synchronisation rather than failing.
Time zones and recurrence, done properly
Recurring events land at the right local time, including across a clock change.
Both, always. The coordinated universal time value is derived for querying, not authoritative for recurring events.
Files
src/time.tsExpanding an infinite rule for all time is a hang waiting to happen. Expand for the range being displayed and cache it.
Files
src/recurrence.tsA fixture series spanning a clock change in a zone that observes one. This test catches the bug that otherwise ships.
Files
src/recurrence.test.ts
done when · tick each as it passeswatch out- A weekly meeting at nine in the morning must stay at nine after the clocks change, which means storing the local time and the zone rather than a fixed instant. Storing only coordinated universal time makes every recurring event drift by an hour twice a year, and the bug is reported months after you introduced it.
Natural language entry
Typing a sentence produces the event you meant, and shows you what it understood.
Dates, times, durations, recurrence keywords, location prepositions and calendar names. Debuggable and testable without a model.
Files
src/parse/grammar.tsReal sentences you would type, with expected results. This corpus is what stops parser changes regressing.
Files
src/parse/corpus.jsonA preview of date, time, duration and calendar before saving. Not a confidence-gated preview; always.
Files
src/ui/Preview.tsx
done when · tick each as it passeswatch out- A parser that saves without showing its interpretation will silently create events on the wrong day, and the user finds out when they miss something. Show the parsed result as a preview every time, including when confidence is high, because the preview costs nothing and the failure is expensive.
Writing back, and conflicts
Events you create appear on your phone, and a simultaneous edit elsewhere is not silently lost.
The precondition header on every update. A mismatch is information, not a failure to swallow.
Files
src/caldav/write.tsBoth versions side by side with their timestamps. The user chooses; the client does not guess.
Files
src/ui/Conflict.tsxAn outbox with retries and visible failures, drained on reconnection.
Files
src/outbox.ts
done when · tick each as it passeswatch out- Writing without the etag precondition means a change made on the phone thirty seconds earlier is silently overwritten, and neither device shows an error. Always send the conditional header and handle the failure by asking, because the alternative is losing someone's edit without telling them.
Views that are fast at a real calendar's size
Day, week and month views render quickly on a calendar with years of history.
The range drives everything. Nothing outside it is computed.
Files
src/views/range.tsColumn assignment so concurrent events share the width rather than hiding each other.
Files
src/views/layout.tsFive years and twenty series. Performance claims against a week of events are meaningless.
Files
src/fixtures/large.ts
done when · tick each as it passeswatch out- Expanding every recurrence rule for every event on each render makes month view take seconds on a real calendar. Expand for the visible range only, cache the expansion, and invalidate the cache when the underlying event changes.
Notifications, invitations and distributionproduct builder
Reminders fire when you are not looking at the application, and invitations do not break.
Reschedule when an event changes and when the visible range advances, so a recurring reminder does not stop after the cached window.
Files
src/notify.tsKeep the original data and modify only what you understand. Compare raw round trips as a test.
Files
src/caldav/roundtrip.test.tsPlatform signing, and a note about which providers you have tested against.
Files
README.md
done when · tick each as it passeswatch out- A calendar client that drops fields it does not understand corrupts events for every other client the user has, and attendee lists and conferencing details are the usual casualties. Preserve unknown properties on round trip rather than rewriting the event from your own model.
That is the whole plan for Fantastical. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Widgets, watch complications and system-level integration.
- Meeting proposals and scheduling links.
- Provider-specific compatibility across every calendar server.
- Weather, travel time and contextual features.
- native Apple integrations, widgets, sync, proposals, and years of calendar edge cases
- multiple calendar providers
- team routing and round-robin
- payments
- timezone edge-case maturity
- Multiple account support with a unified view, which mostly works already if the model stayed neutral
- Time zone display for events in another zone, which travellers need constantly
- A quick entry window on a global hotkey, since the parser is the valuable part
Need the files? The project pack on the verdict page hands your agent the whole brief · more scheduling.