Self-hosted calendar sync that creates busy blocks across calendars. Supports Google Calendar and CalDAV (iCloud, Fastmail, etc).
- Docker & Docker Compose
- Google Cloud project with Calendar API enabled (for Google Calendar)
- iCloud app-specific password (for iCloud)
git clone https://github.com/pkieszcz/calendar-sync.git
cd calendar-sync
cp .env.example .env./scripts/generate-encryption-key.sh
# Paste the output into your .env file- Go to Google Cloud Console
- Create a project and enable the Google Calendar API
- Create OAuth 2.0 credentials (Web application)
- Add
http://localhost:8080/api/auth/google/callbackas an authorized redirect URI - Add
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETto.env
make up
# or: docker compose up --build -d- Frontend: http://localhost:3000
- Backend API: http://localhost:8080
Google Calendar:
- Click "Connect Google" on the Accounts page
- Complete the OAuth flow
iCloud (CalDAV):
- Generate an app-specific password at appleid.apple.com
- Click "Add CalDAV" on the Accounts page
- Enter:
- Name: anything (e.g. "iCloud")
- Server URL:
https://caldav.icloud.com - Username: your Apple ID email
- Password: the app-specific password
- Click "Show Calendars" then "Refresh" to discover calendars
- Click "Show Calendars" on each account
- Check the calendars you want to sync (e.g. one from Google, one from iCloud)
- Sync rules are created automatically between calendars from different accounts
- Events sync as "Busy" blocks every 60 seconds
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Frontend │───>│ Backend │───>│ Postgres │
│ Next.js │ │ Go │ │ │
└──────────┘ └────┬─────┘ └──────────┘
│
┌──────┴──────┐
│ Sync Engine │ (polls every 30s)
└──────┬──────┘
┌─────────┴─────────┐
v v
┌────────────┐ ┌────────────┐
│ Google │ │ CalDAV │
│ Calendar │ │ (iCloud) │
└────────────┘ └────────────┘
- Backend: Go 1.23, Chi router, pgx v5, golang-migrate
- Frontend: Next.js (App Router), TypeScript, Tailwind CSS
- Database: PostgreSQL 16
- Credentials: AES-256-GCM encrypted at rest
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL connection string |
ENCRYPTION_KEY |
Yes | 32-byte hex-encoded AES key |
GOOGLE_CLIENT_ID |
No | Google OAuth client ID |
GOOGLE_CLIENT_SECRET |
No | Google OAuth client secret |
GOOGLE_REDIRECT_URL |
No | OAuth redirect (default: http://localhost:8080/api/auth/google/callback) |
FRONTEND_URL |
No | Frontend URL for CORS (default: http://localhost:3000) |
# Run everything
make up
# Logs
make logs
# Stop
make down
# Run backend locally
cd backend && go run ./cmd/server
# Run frontend locally
cd frontend && npm run dev| Method | Endpoint | Description |
|---|---|---|
| GET | /api/health |
Health check |
| GET/POST | /api/accounts |
List/create accounts |
| DELETE | /api/accounts/:id |
Delete account |
| GET | /api/accounts/:id/calendars |
List calendars |
| POST | /api/accounts/:id/calendars/refresh |
Refresh from provider |
| PUT | /api/calendars/:id/sync |
Toggle sync on/off |
| GET | /api/events?start=&end= |
List events for calendar view |
| POST | /api/sync-rules/:id/trigger |
Manually trigger sync |
| GET | /api/sync-status |
Sync status overview |
Apache 2.0