Build Fantastical

YESreplaces $4.75/mosaves $57/yrback to the verdict

0%0 of 20 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort multi-daythe files for this build are in the project pack

RuntimeTypeScript with a desktop shellSyncCalDAV with a well-tested client libraryTimeA library with a full time zone database, never naive datesRecurrenceA recurrence rule library plus your own exception handlingParsingA deterministic grammar first, with a model only for what it cannot handle

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. free 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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
DB_PATHrequired./data/calendar.dbLocal cache of accounts, calendars and events. Rebuildable from the server.
SYNC_INTERVAL_SECONDSrequired300How often to poll for changes. Too frequent is rude to the provider and rarely necessary.
EXPAND_RANGE_DAYSrequired120How far around the visible range to expand recurrence. Larger costs memory and time.
REMINDER_LOOKAHEAD_MINUTESrequired10080How far ahead notifications are scheduled. Must exceed your expansion refresh interval or reminders stop.
DEFAULT_TIMEZONErequiredEurope/LondonUsed when an event carries no zone. Should match the machine unless the user overrides it.

The build, in order

  1. Sync that does not lose events

    Your real calendar appears, and a change made elsewhere shows up.

    1. 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.ts

    2. The collection sync report gives changes since a token. Store etags so you can detect conflicts later.

      Files src/caldav/sync.ts

    3. Fall back to a full sync. Test it by corrupting the stored token deliberately.

      Files src/caldav/sync.ts

    done when · tick each as it passes
    watch 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.
  2. Time zones and recurrence, done properly

    Recurring events land at the right local time, including across a clock change.

    1. Both, always. The coordinated universal time value is derived for querying, not authoritative for recurring events.

      Files src/time.ts

    2. Expanding an infinite rule for all time is a hang waiting to happen. Expand for the range being displayed and cache it.

      Files src/recurrence.ts

    3. A 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 passes
    watch 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.
  3. Natural language entry

    Typing a sentence produces the event you meant, and shows you what it understood.

    1. Dates, times, durations, recurrence keywords, location prepositions and calendar names. Debuggable and testable without a model.

      Files src/parse/grammar.ts

    2. Real sentences you would type, with expected results. This corpus is what stops parser changes regressing.

      Files src/parse/corpus.json

    3. A 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 passes
    watch 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.
  4. Writing back, and conflicts

    Events you create appear on your phone, and a simultaneous edit elsewhere is not silently lost.

    1. The precondition header on every update. A mismatch is information, not a failure to swallow.

      Files src/caldav/write.ts

    2. Both versions side by side with their timestamps. The user chooses; the client does not guess.

      Files src/ui/Conflict.tsx

    3. An outbox with retries and visible failures, drained on reconnection.

      Files src/outbox.ts

    done when · tick each as it passes
    watch 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.
  5. Views that are fast at a real calendar's size

    Day, week and month views render quickly on a calendar with years of history.

    1. The range drives everything. Nothing outside it is computed.

      Files src/views/range.ts

    2. Column assignment so concurrent events share the width rather than hiding each other.

      Files src/views/layout.ts

    3. Five years and twenty series. Performance claims against a week of events are meaningless.

      Files src/fixtures/large.ts

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more scheduling.