Help

Scope and limitations

What Calendarpit covers, what it leaves out, and the current project status.

Calendarpit is a local Google Calendar-compatible API and OAuth server. It lets you develop and test your app without a real Google account, a Google Cloud project, or network access to Google.

It implements the subset of the following that local development, CI, and end-to-end tests need:

  • Google Calendar API v3
  • OAuth 2.0
  • Incremental sync
  • Webhook notifications

What is covered

Everything in this section is implemented.

Runtime and state

  • One Bun process serves the API, the OAuth flow, and the admin UI.
  • The default address is http://127.0.0.1:4173.
  • Storage is SQLite or :memory:.

See Run and configure and Storage and restarts.

Test users and calendars

  • Passwordless test users own calendars.
  • Each calendar has an IANA time zone.
  • Each user has a primary calendar, reachable as primary.
  • You read calendars with calendars.get.
  • You manage each user's list with calendarList (list, get, insert, patch, update, delete).

Per-user list settings include:

  • summaryOverride
  • colorId, or custom RGB with colorRgbFormat
  • hidden and selected
  • defaultReminders
  • notificationSettings
  • Removing or restoring non-primary owned calendars

Events and availability

  • Event CRUD: list, get, insert, update, patch, delete, and instances
  • Recurrence with exceptions, using a subset of RRULE, EXRULE, RDATE, and EXDATE
  • DST-aware [start, end) intervals
  • ETags and extended properties
  • Local invitations and RSVP
  • Stored reminders
  • Synthetic hangoutsMeet links
  • freebusy.query

A series without COUNT or UNTIL is unbounded. If a calendar contains one, listing or expanding that calendar requires timeMax whenever you use timeMin or singleEvents=true.

See Calendar API compatibility and Events, recurrence and time.

OAuth

  • Local consent page with a test-user selector
  • Approval and denial, with state preservation
  • Exact redirect_uri checks
  • Code exchange with S256 PKCE
  • Scope checks
  • Refresh tokens, /userinfo, and revocation

See Local OAuth.

Incremental sync

  • nextSyncToken and syncToken on events.list
  • Frozen paginated snapshots
  • status: "cancelled" tombstones
  • 410 fullSyncRequired responses

See Incremental sync.

Watch

  • events.watch, calendarList.watch, and channels.stop
  • sync and exists webhooks
  • A callback host allowlist
  • Up to 3 delivery attempts
  • Channel expiry
  • POST /__calendarpit/deliveries/drain

See Watch notifications.

Admin UI

  • Day and week views
  • A basic event editor
  • User and calendar management
  • Connection revocation
  • Watch and delivery logs
  • Masked request history
  • Fixture and reset controls

Test control

  • Startup JSON fixtures, with optional fixed IDs
  • /__calendarpit/* management endpoints that inspect state, load fixtures, reset to the baseline, and manage data

See Fixtures and Test control API.

Guarantees

Loopback by default

  • The server binds to 127.0.0.1 unless you set --host or CALENDARPIT_HOST.
  • The Dockerfile explicitly binds 0.0.0.0 inside the container.
  • Webhook callbacks only target loopback hosts unless you configure CALENDARPIT_WATCH_HOSTS.

Explicit errors for unsupported features

Unsupported features return an explicit error.

RequestStatusError
Unsupported Calendar API endpoint501reason: "unsupportedFeature"
Unsupported query parameter, field, or recurrence rule400reason: "unsupportedFeature"
events.import404notFound
events.quickAdd with its text parameter400unsupportedFeature
events.quickAdd without text404notFound
OAuth authorization request with unsupported parameters400error: "unsupported_feature"
Unsupported OAuth endpoint501error: "unsupportedFeature"

Calendarpit rejects an unsupported recurrence rule with 400 unsupportedFeature. It does not store the event without the rule.

No real Google calls

  • Calendarpit never sends API, OAuth, or webhook requests to Google.
  • The admin UI loads the Roboto Mono font from Google Fonts, with a system-font fallback.

Deterministic reset

POST /__calendarpit/reset restores the startup fixture with the same IDs and calendar-list ETags. It clears:

  • Tokens
  • Channels
  • History
  • Calendar-list settings
  • Deleted-calendar tombstones
  • Edits

Behavior across restarts depends on the storage mode:

  • SQLite: The reset baseline, calendar-list settings, and tombstones survive restarts.
  • :memory:: Set fixed IDs in the fixture to keep IDs stable across restarts.

What is out of scope

Calendarpit does not implement the following.

ACLs, shared calendars, and delegation

  • Users only access their own calendars (accessRole: "owner").
  • All acl.* methods return 501 unsupportedFeature.
  • calendarList.insert only restores an owned calendar removed by calendarList.delete.
    • It never creates a calendar or subscribes to another user's calendar.
    • It returns 409 duplicate if the calendar (including primary) is already on the list.

Settings, ACL/settings watch, and CalendarList sync tokens

These are unsupported:

  • All settings.* methods return 501 unsupportedFeature.
  • ACL and settings watch subscriptions (acl.watch, settings.watch) return 501 unsupportedFeature.
  • calendarList.list does not issue nextSyncToken. It rejects syncToken with 400 unsupportedFeature.

calendarList.watch is implemented for the owning user's list, with these limits:

  • It rejects any params object with 400 unsupportedFeature.
  • It sends exists only on list writes that change stored settings.
  • It also sends exists on management calendar creates, updates, or deletes for that user's calendars.

With showDeleted=true:

  • Entries removed by calendarList.delete keep their stored settings.
  • Tombstones of management-deleted calendars show default settings. They keep primary: true if the calendar was deleted while primary.

Calendar writes, colors, and events.import/quickAdd/move

These methods are not implemented:

  • calendars.insert, update, patch, delete, clear, and transferOwnership
  • colors.get
  • events.import, quickAdd, and move

Manage calendars through the admin UI or the management API. See Calendar API compatibility.

Real invitation and reminder email

  • sendUpdates records notification intent in /__calendarpit/state. It does not send email.
  • Calendarpit stores reminders, calendarList defaultReminders, and notificationSettings. Nothing fires or sends email.
  • notificationSettings rejects duplicate notification type values with 400 invalidParameter.

Real Google Meet

Conference requests generate deterministic https://meet.calendarpit.invalid/<id> URLs. No real meeting is created.

Full OIDC

These are not supported:

  • Passwords
  • Real Google login
  • id_token issuance
  • JWKS and discovery endpoints

Other protocols and real Google sync

Microsoft Graph, CalDAV, and syncing with real Google Calendar are excluded.

Dragging events in the admin UI

  • Moving or resizing events by dragging is not implemented.
  • The admin views show a recurring series (not its occurrences) only on its start date. Stored exceptions appear on their own dates.
  • The admin editor covers only basic fields.

Saving an event in the admin editor replaces it with only the basic fields. This removes:

  • Recurrence, which also cancels stored exceptions
  • Attendees, which also cancels their invitation copies
  • Conference data
  • Color
  • Visibility

Custom reminders reset to the default (reminders: { useDefault: true }). Set those fields through the API.

Fixtures store these fields as-is. They do not create invitation copies or synthetic meeting links. See Fixtures.

A workflow Calendarpit reproduces

Calendarpit reproduces this end-to-end integration workflow. For the Google SDK sample flow, see SDK examples.

  1. Authorize as a test user on the local consent page and select a calendar.
  2. Register an events.watch channel and receive the initial sync notification.
  3. Create an event and verify its busy interval with freebusy.query.
  4. Update and delete the event, receive exists notifications, and refetch state.
  5. Stop the watch channel with channels.stop and confirm further changes are not delivered.
  6. Revoke the connection and reconnect through OAuth.
  7. Deny consent and verify that state is preserved without losing existing tokens.
  8. Reset with POST /__calendarpit/reset and repeat the flow with the same fixtures and IDs.
  9. Fetch a full events.list snapshot, then consume changes and status: "cancelled" deletions with syncToken.
  10. Exercise all-day events (including dateTime: null), recurring exceptions, local RSVP, and synthetic meeting links.

Current status

  • Distribution: Docker images, the Homebrew tap, and standalone executables are available. See Installation for setup.
  • Standalone executables: They embed Bun, the API, and the admin UI. Release targets are macOS arm64/x64, Linux glibc arm64/x64, and Windows x64. Each target requires native validation before publication.
  • Docker: The image configures a non-root user, a persistent volume, a health check, and memory mode. Release checks cover amd64/arm64 startup and storage lifecycles.

On this page