SynCal is a production-grade, highly resilient, multi-tenant scheduling intelligence platform. It features robust rate-limit honor rules per provider (Google, Microsoft, iCloud CalDAV, iCal), webhook buffering, and stateful workflow synchronization orchestration powered by Cadence.
SynCal relies on a modern distributed architecture designed for maximum reliability, durability, and scalability:
- Ingress Webhook Gateway (
ingress): Ingests webhook notifications from Google Calendar and Microsoft Graph. It buffers messages to Redis Pub/Sub to prevent overloading the Cadence Cluster. - Pub/Sub Signal Consumer (
consumer): A daemon that consumes buffered events and signals the relevant stateful sync workflows. - API Gateway (
api): Exposes B2B tenant registration APIs, token ingestion endpoints (/v1/accounts/import), OAuth redirection pipelines, and booking slot availability calculators. - Cadence Workflow Engine: Runs long-lived, stateful orchestration workflows for calendar syncing (
CalendarSyncWorkflow) and recurring webhook subscription renewals (WebhookRenewalWorkflow) on an external Cadence cluster. - React Dashboard UI: A dashboard designed following Apple's macOS Human Interface Guidelines (translucent interfaces, status indicators, and SLA/SLO latency graphs).
- Databases:
- PostgreSQL: Main local application store containing tables for tenants, accounts, calendars, and events.
- Redis: Local caching, dynamic sliding-window rate-limiting, and Pub/Sub webhook queuing.
Before running the platform, ensure you have the following installed on your machine:
- Docker (and Docker Compose V2)
- Go 1.23+ (optional, only if running or compiling tests locally without Docker)
- Node.js 18+ (optional, only if running frontend dev server locally)
The project includes a root Makefile that simplifies compiling, launching, checking status, and running tests.
For rapid development, you can run the frontend and backend directly on your host machine without rebuilding Docker containers:
- Start React Frontend (Vite hot-reload dev server):
make start-ui
- Start Go Backend (Runs all 3 microservices concurrently in the background):
make start-backend
Compile the React frontend static assets, the Nginx container, and build all three Go service binaries inside the multi-stage Go image:
make buildLaunch the PostgreSQL database, the Redis service, the worker daemons, and the dashboard portal in the background:
make upNote: On start, the PostgreSQL container will automatically seed the calendar database schema. The backend services (api and consumer) will connect directly to the external Cadence cluster at dev-cadence.auenkr.com:7933 using the domain default.
To view where all user interfaces and APIs are mapped, run:
make webThis prints the following service directory:
- React Admin Console UI: http://localhost:3000
- REST API Gateway: http://localhost:8080
- Webhook Ingress API: http://localhost:8081
- External Cadence Web UI: http://dev-cadence-ui.auenkr.com:8088 (domain:
default)
To tail runtime log outputs from the backend services, consumers, or Cadence workflow worker loops, execute:
make logsExecute the cryptographic envelope, token validation, and scheduling availability intersection tests:
make testTo pause execution and shut down the docker containers:
make downTo wipe all state (including active PostgreSQL database rows and cached Redis limits) to run a clean bootstrap:
make cleanThe full machine-readable spec lives at docs/openapi.yaml (OpenAPI 3.0). Import it into Postman, Insomnia, or Swagger UI to get interactive docs.
| API | Header | When to use |
|---|---|---|
| Tenant API Key | X-API-Key: cs_live_<prefix>.<secret> |
All tenant integrations |
| Admin Bearer | Authorization: Bearer <token> |
Internal dashboard only |
Tenant API keys are generated from the Admin Console → Tenants → Keys. Keys are shown once at creation — store them in your secrets manager immediately.
These are the endpoints your integration code calls using X-API-Key.
| Method | Path | Description |
|---|---|---|
GET |
/v1/oauth/authorize |
Generate a provider OAuth URL to redirect your user to |
GET |
/v1/oauth/callback |
Provider redirect target — handled automatically |
POST |
/v1/accounts/import |
Import an account using existing OAuth tokens (Pattern B) |
POST |
/v1/caldav/connect |
Connect a CalDAV server (iCloud, Nextcloud, Fastmail) |
POST |
/v1/ical/connect |
Connect a read-only iCal .ics feed URL |
GET |
/v1/tenant-portal/accounts |
List all connected accounts for your tenant |
POST |
/v1/tenant-portal/oauth-url |
Server-side helper to build an OAuth authorize path |
OAuth flow (Pattern A — recommended):
1. GET /v1/oauth/authorize?provider=google&tenant_id=<tid>&entity_id=<your-user-id>&redirect_uri=<your-callback>
→ Returns { "url": "https://accounts.google.com/..." }
2. Redirect user to that URL.
3. Provider calls back to /v1/oauth/callback → your redirect_uri receives:
?status=success&account_id=<uuid>&entity_id=<id>&provider=google&email=user@example.com
4. Use account_id to present a calendar picker (see Calendar endpoints below).
Token import (Pattern B):
curl -X POST http://localhost:8080/v1/accounts/import \
-H "X-API-Key: cs_live_a1b2c3d4.zXyWvUt..." \
-H "Content-Type: application/json" \
-d '{
"entity_id": "user-abc-123",
"provider": "google",
"email": "alice@example.com",
"credentials": {
"access_token": "ya29.a0...",
"refresh_token": "1//04..."
}
}'| Method | Path | Description |
|---|---|---|
GET |
/v1/users/:userId/calendars |
List all calendars for a user (by entity_id) ⭐ |
PATCH |
/v1/calendars/:id/sync-direction |
Set inbound / outbound / both ⭐ |
GET |
/v1/calendars/:id/events |
List events (cached or live from provider) |
GET /v1/users/:userId/calendars — List every calendar for a user across all their connected accounts (Google + Outlook etc. in one call):
curl http://localhost:8080/v1/users/user-abc-123/calendars \
-H "X-API-Key: cs_live_a1b2c3d4.zXyWvUt..."{
"user_id": "user-abc-123",
"count": 2,
"calendars": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "alice@gmail.com",
"provider": "google",
"sync_direction": "both",
"is_primary": true,
"bootstrapped": true,
"conflict_count": 0,
"account_id": "...",
"email": "alice@gmail.com",
"account_status": "active"
},
{
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"name": "Work Calendar",
"provider": "outlook",
"sync_direction": "inbound",
"is_primary": true,
"bootstrapped": true,
"conflict_count": 0
}
]
}PATCH /v1/calendars/:id/sync-direction — Change how a calendar syncs:
sync_direction |
Provider → SynCal | SynCal → Provider |
|---|---|---|
inbound |
✅ | ❌ |
outbound |
❌ | ✅ |
both |
✅ | ✅ |
curl -X PATCH http://localhost:8080/v1/calendars/550e8400-.../sync-direction \
-H "X-API-Key: cs_live_..." \
-H "Content-Type: application/json" \
-d '{"sync_direction": "inbound"}'{ "calendar_id": "550e8400-...", "sync_direction": "inbound" }Admin-only: To set direction to
none(fully disable syncing), usePATCH /v1/calendars/:id/directionwith an admin bearer token.
| Method | Path | Description |
|---|---|---|
GET |
/v1/calendars/:id/last-synced |
Last successful inbound & outbound timestamps ⭐ |
GET |
/v1/tenants/metrics/sync |
Aggregate sync health (conflicts, pending, lag percentiles) |
GET /v1/calendars/:id/last-synced — Know exactly when a calendar last synced in each direction:
curl http://localhost:8080/v1/calendars/550e8400-.../last-synced \
-H "X-API-Key: cs_live_..."{
"calendar_id": "550e8400-e29b-41d4-a716-446655440000",
"last_inbound_sync": "2026-06-01T07:45:00Z",
"last_outbound_sync": "2026-06-01T07:44:30Z"
}Both fields are null when no sync of that type has run (e.g. inbound-only calendar → last_outbound_sync: null).
| Method | Path | Description |
|---|---|---|
GET |
/v1/calendars/:id/events |
List events (DB cache; ?live=true fetches from provider) |
GET |
/v1/calendars/:id/events/:eventId |
Get a single event |
POST |
/v1/calendars/:id/events |
Create an event (async, 202 Accepted) |
PATCH |
/v1/calendars/:id/events/:eventId |
Update an event (async, 202 Accepted) |
DELETE |
/v1/calendars/:id/events/:eventId |
Delete an event (async, 202 Accepted) |
Event writes are async — they return 202 Accepted with a job_id. The outbound worker
pushes the change to the provider while honouring per-provider rate limits. Calendars with
sync_direction=inbound reject write requests with 422 Unprocessable Entity.
# Create an event
curl -X POST http://localhost:8080/v1/calendars/550e8400-.../events \
-H "X-API-Key: cs_live_..." \
-H "Content-Type: application/json" \
-d '{
"title": "Quarterly Review",
"start_time": "2026-06-15T14:00:00Z",
"end_time": "2026-06-15T15:00:00Z",
"timezone": "America/New_York"
}'{
"job_id": "abc12345-...",
"calendar_id": "550e8400-...",
"operation": "create",
"status": "queued"
}| Method | Path | Description |
|---|---|---|
GET |
/v1/calendars/:id/conflicts |
List events in conflict state |
POST |
/v1/calendars/:id/events/:eventId/resolve |
Resolve a conflict |
GET |
/v1/calendars/:id/pending |
List events waiting for tenant ACK (pending_inbound) |
POST |
/v1/calendars/:id/events/:eventId/ack |
Acknowledge a pending inbound event |
Resolve strategies: provider_wins · local_wins · last_write_wins · manual
curl -X POST http://localhost:8080/v1/calendars/550e8400-.../events/evt-uuid.../resolve \
-H "X-API-Key: cs_live_..." \
-d '{"strategy": "provider_wins"}'These are only accessible after signing in to the dashboard at http://localhost:3000.
| Method | Path | Description |
|---|---|---|
GET / POST |
/v1/tenants |
List / create tenants |
PATCH / DELETE |
/v1/tenants/:id |
Update / delete a tenant |
POST / GET |
/v1/tenants/:id/keys |
Generate / list API keys |
GET |
/v1/tenants/:id/accounts |
List accounts with calendar details |
DELETE |
/v1/tenants/:id/accounts/:accountId |
Disconnect an account |
GET / POST |
/v1/accounts/:id/calendars/available |
Browse / add provider calendars |
DELETE |
/v1/calendars/:id |
Stop tracking a calendar + unregister webhook |
PATCH |
/v1/calendars/:id/direction |
Set direction incl. none |
POST |
/v1/calendars/:id/force-sync |
Trigger an immediate sync |
POST |
/v1/calendars/:id/re-register-webhook |
Re-register provider webhook (after URL change) |
POST / GET |
/v1/tenants/:id/oauth-configs |
Configure / list BYO OAuth credentials |
GET / POST |
/v1/tenant-webhooks |
List / register outbound webhook endpoints |
DELETE |
/v1/tenant-webhooks/:id |
Remove an outbound webhook |
PATCH |
/v1/accounts/:id/sync-strategy |
Set per-account sync strategy (fcfs / round_robin) |
PATCH |
/v1/tenants/:id/sync-strategy |
Set tenant-default sync strategy |
| Method | Path | Description |
|---|---|---|
GET / POST |
/v1/tenants/:id/provider-rate-limits |
Configure per-provider quotas |
GET / POST |
/v1/tenants/:id/rate-limits |
Configure tenant & user RPM limits |
GET |
/v1/system/rate-buckets |
View active Redis rate-limit buckets |
DELETE |
/v1/system/rate-buckets/reset |
Reset a stuck concurrency counter |
| Method | Path | Description |
|---|---|---|
GET |
/v1/system/metrics |
Platform-wide SLO metrics (scoped with ?tenant_id=) |
GET |
/v1/system/metrics/rates |
Rolling sync success rates (30 min / 1 hr / 7 d / 30 d) |
GET |
/v1/system/metrics/windows |
Time-window sync breakdown table |
GET |
/v1/system/metrics/provider-breakdown |
Per-provider sync stats |
GET |
/v1/system/metrics/provider-status |
Provider success / failure / rate-limited pie data |
GET |
/v1/system/metrics/provider-windows |
Provider × window matrix |
GET |
/v1/system/metrics/calendar-sync-rates |
Per-calendar sync rates across all windows |
GET |
/v1/system/metrics/kafka |
Kafka consumer lag for the webhook topic |
GET |
/v1/system/sync-feed |
Recent sync activity feed |
GET |
/v1/system/dlq |
Dead-letter queue items |
POST |
/v1/system/dlq/:id/retry |
Retry a failed DLQ item |
DELETE |
/v1/system/dlq/:id |
Discard a DLQ item |
GET |
/v1/system/calendars/:id/events |
Admin event viewer with optional live provider diff |
| Method | Path | Description |
|---|---|---|
POST |
/v1/webhooks/google |
Google Calendar push notification receiver |
POST |
/v1/webhooks/outlook |
Microsoft Graph push notification receiver |
Three new tenant-facing endpoints were added in the latest release:
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/v1/users/:userId/calendars |
API Key | List all calendars for a user across every connected account |
PATCH |
/v1/calendars/:id/sync-direction |
API Key | Set inbound / outbound / both for a specific calendar |
GET |
/v1/calendars/:id/last-synced |
API Key | Retrieve the last successful sync timestamp per direction |
See the sections above for full request/response examples.
- backend/Dockerfile - Multi-stage runner building all backend services.
- backend/migrations/ - Database schema init scripts.
- backend/cmd/ - Microservice endpoints (api, ingress, consumer).
- backend/internal/ - Authentication middleware, dynamic credential resolvers, rate limiters, locks, and scheduling availability code.
- backend/internal/workflows/ - Cadence workflow state loops and provider integration activities.
- frontend/Dockerfile - Nginx SPA production container.
- frontend/nginx.conf - Config mapping deep routing endpoints.
- frontend/src/ - React components, sidebar navigation, consoles, and recharts panels.