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:
summaryOverridecolorId, or custom RGB withcolorRgbFormathiddenandselecteddefaultRemindersnotificationSettings- Removing or restoring non-primary owned calendars
Events and availability
- Event CRUD:
list,get,insert,update,patch,delete, andinstances - Recurrence with exceptions, using a subset of
RRULE,EXRULE,RDATE, andEXDATE - DST-aware
[start, end)intervals - ETags and extended properties
- Local invitations and RSVP
- Stored reminders
- Synthetic
hangoutsMeetlinks 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
statepreservation - Exact
redirect_urichecks - Code exchange with
S256PKCE - Scope checks
- Refresh tokens,
/userinfo, and revocation
See Local OAuth.
Incremental sync
nextSyncTokenandsyncTokenonevents.list- Frozen paginated snapshots
status: "cancelled"tombstones410 fullSyncRequiredresponses
See Incremental sync.
Watch
events.watch,calendarList.watch, andchannels.stopsyncandexistswebhooks- 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.1unless you set--hostorCALENDARPIT_HOST. - The
Dockerfileexplicitly binds0.0.0.0inside 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.
| Request | Status | Error |
|---|---|---|
| Unsupported Calendar API endpoint | 501 | reason: "unsupportedFeature" |
| Unsupported query parameter, field, or recurrence rule | 400 | reason: "unsupportedFeature" |
events.import | 404 | notFound |
events.quickAdd with its text parameter | 400 | unsupportedFeature |
events.quickAdd without text | 404 | notFound |
| OAuth authorization request with unsupported parameters | 400 | error: "unsupported_feature" |
| Unsupported OAuth endpoint | 501 | error: "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 return501 unsupportedFeature. calendarList.insertonly restores an owned calendar removed bycalendarList.delete.- It never creates a calendar or subscribes to another user's calendar.
- It returns
409 duplicateif the calendar (includingprimary) is already on the list.
Settings, ACL/settings watch, and CalendarList sync tokens
These are unsupported:
- All
settings.*methods return501 unsupportedFeature. - ACL and settings watch subscriptions (
acl.watch,settings.watch) return501 unsupportedFeature. calendarList.listdoes not issuenextSyncToken. It rejectssyncTokenwith400 unsupportedFeature.
calendarList.watch is implemented for the owning user's list, with these limits:
- It rejects any
paramsobject with400 unsupportedFeature. - It sends
existsonly on list writes that change stored settings. - It also sends
existson management calendar creates, updates, or deletes for that user's calendars.
With showDeleted=true:
- Entries removed by
calendarList.deletekeep their stored settings. - Tombstones of management-deleted calendars show default settings. They keep
primary: trueif 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, andtransferOwnershipcolors.getevents.import,quickAdd, andmove
Manage calendars through the admin UI or the management API. See Calendar API compatibility.
Real invitation and reminder email
sendUpdatesrecords notification intent in/__calendarpit/state. It does not send email.- Calendarpit stores reminders,
calendarListdefaultReminders, andnotificationSettings. Nothing fires or sends email. notificationSettingsrejects duplicate notificationtypevalues with400 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_tokenissuance- 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.
- Authorize as a test user on the local consent page and select a calendar.
- Register an
events.watchchannel and receive the initialsyncnotification. - Create an event and verify its busy interval with
freebusy.query. - Update and delete the event, receive
existsnotifications, and refetch state. - Stop the watch channel with
channels.stopand confirm further changes are not delivered. - Revoke the connection and reconnect through OAuth.
- Deny consent and verify that
stateis preserved without losing existing tokens. - Reset with
POST /__calendarpit/resetand repeat the flow with the same fixtures and IDs. - Fetch a full
events.listsnapshot, then consume changes andstatus: "cancelled"deletions withsyncToken. - 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.