A practical experiment in building a self-hosted calendar synchronisation tool using Claude Code as the primary development assistant. From architecture to deployment, here’s how AI-assisted development worked in practice.
Project Overview
This project is a self-hosted calendar synchronisation tool that syncs busy times between multiple Google calendars. The primary motivation was twofold: (1) the practical need to keep personal and work calendars in sync, and (2) an experiment to build an entire application using Claude Code as the primary development tool.
The application runs locally or on a cloud server, syncs calendars bidirectionally, and gives the user full control over their data.
Development Approach: Claude Code as Primary Developer
The entire codebase was developed through conversations with Claude Code. This included:
- Architecture decisions and code structure
- Python backend implementation with FastAPI
- Frontend JavaScript/HTML development
- Terraform infrastructure as code
- Unit test creation
- Production debugging and OAuth troubleshooting
- Git workflow and deployment automation
All work was tracked in a PROJECT_JOURNAL.md file that Claude Code maintained throughout the development process, documenting decisions, bugs encountered, and solutions implemented.
Technical Stack
| Component | Technology |
|---|---|
| Backend | Python 3.11, FastAPI |
| Authentication | Google OAuth 2.0, JWT tokens |
| Scheduling | APScheduler |
| Frontend | Vanilla JavaScript, HTML, CSS |
| Database | File-based JSON storage |
| Infrastructure | Terraform, IONOS Cloud |
| Containerisation | Docker, Docker Compose |
| Reverse Proxy | Nginx with Let’s Encrypt SSL |
Application Architecture
┌─────────────────────────────────────────────────────────────┐
│ Calendar Sync Application │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ FastAPI │ │ APScheduler │ │ Web UI │ │
│ │ Backend │ │ (30 min) │ │ (Vanilla JS)│ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Sync Engine │ │
│ │ - Event filtering (all-day, declined, free) │ │
│ │ - Sync marker system ([AUTO-SYNC] prefix) │ │
│ │ - Bidirectional loop prevention │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Google Calendar API │ │
│ │ - Per-user OAuth tokens │ │
│ │ - Read/write calendar access │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Backend Components
Core Files Structure
app/
├── main.py # FastAPI application, routes, startup
├── models.py # Pydantic models for all data structures
├── google_auth.py # OAuth manager, credential storage
├── sync_engine.py # Event sync logic, filtering, markers
├── scheduler.py # APScheduler background jobs
├── auth/
│ ├── dependencies.py # JWT authentication dependencies
│ └── jwt_handler.py # Token creation and verification
├── routers/
│ ├── auth_router.py # Login, logout, OAuth callbacks
│ ├── user_router.py # Calendar connections
│ └── rules_router.py # Sync rule CRUD operations
├── services/
│ └── rule_validator.py # Graph-based cycle detection
└── storage/
├── user_store.py # User data persistence
└── config_store.py # Configuration persistence
Sync Engine Features
- Event Filtering: Excludes all-day events, declined events, tentative events, and “free” time blocks
- Sync Markers: All synced events get
[AUTO-SYNC]prefix to prevent re-syncing - Bidirectional Support: A↔B sync allowed (protected by markers); circular chains A→B→C→A blocked via DFS graph validation
- Per-User Credentials: Tokens stored in
data/users/{user_id}/calendar_tokens/
API Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/status |
Dashboard status, scheduler info |
| POST | /api/sync |
Trigger manual sync |
| GET | /api/history |
Sync history log |
| GET | /api/rules |
List sync rules |
| POST | /api/rules |
Create sync rule |
| PUT | /api/rules/{id} |
Update sync rule |
| DELETE | /api/rules/{id} |
Delete sync rule |
| POST | /api/rules/validate |
Validate rule without saving |
| GET | /api/user/calendars |
List connected calendars |
| POST | /api/user/calendars/connect |
Start OAuth flow |
| GET | /api/auth/login |
Google OAuth login |
| POST | /api/auth/refresh |
Refresh JWT token |
Infrastructure: IONOS Cloud with Terraform
The infrastructure was provisioned using Terraform with the IONOS Cloud provider. This was part of the experiment to see how Claude Code would handle infrastructure as code.
Terraform Module Structure
infrastructure/terraform/
├── modules/
│ └── server/
│ ├── main.tf # Server, datacenter, networking
│ ├── variables.tf # Configurable parameters
│ ├── outputs.tf # IP addresses, IDs
│ └── cloud-init.yaml # Server bootstrap script
└── environments/
├── shared/ # Shared resources
├── development/ # Dev environment config
└── production/ # Prod environment config
Resources Provisioned
- IONOS Datacenter: Logical container for resources
- vCPU Server: Ubuntu 24.04 LTS, configurable cores/RAM/disk
- Public IP Block: Static IP for DNS
- LAN: Public network access
- Firewall Rules: SSH (22), HTTP (80), HTTPS (443), App (8000)
- Cloud-init: Automated Docker, nginx, certbot installation
Server Configuration
The cloud-init script bootstraps the server with:
- Docker and Docker Compose
- Nginx reverse proxy
- Let’s Encrypt SSL via Certbot
- Automatic app deployment from GitHub
Unit Tests
Test files cover the core sync functionality:
test_rule_validator.py (12 tests)
- Cycle detection in rule graphs
- Bidirectional rule validation
- Disabled rule exclusion from validation
- Error messages for circular chains
test_sync_engine.py (11 tests)
- Sync marker filtering
- Event filtering (all-day, declined, free)
- Event creation and updates
- Cleanup of deleted source events
test_rules_api.py (18 tests)
- CRUD operations on sync rules
- Validation endpoint
- Available calendars endpoint
- Toggle enable/disable
Test Fixtures (conftest.py)
- Mock Google Calendar client
- Test user fixtures
- Sample event data
OAuth Implementation Details
Login Flow
- User clicks “Sign in with Google”
- Redirect to Google OAuth consent screen
- Callback receives authorisation code
- Exchange code for tokens via HTTP POST
- Create user record, issue JWT tokens
- Redirect to dashboard with tokens in URL
Calendar Connection Flow
- User clicks “Connect Calendar”
- OAuth flow with calendar scope
- Tokens stored in user’s directory
- Calendar list fetched via Google API
Data Storage
data/
├── users/
│ └── {user-id}/
│ ├── user.json # User profile
│ ├── sync_rules.json # User's sync rules
│ └── calendar_tokens/
│ ├── personal@gmail.com.json
│ └── work@company.com.json
├── logs/
│ └── calendar-sync.log
└── sync_history.json
Deployment
Docker Compose Configuration
- Single container running FastAPI with Uvicorn
- Volume mounts for config and data persistence
- Automatic restart policy
Production Deployment Commands
ssh deploy@server
cd ~/apps/calendar-sync-prod
git pull origin main
docker compose build --no-cache
docker compose up -d
Development Timeline
| Date | Work Completed |
|---|---|
| Nov 25, 2025 | Initial sync prototype |
| Nov 29, 2025 | Project structure, GitHub setup, Claude Code upgrade |
| Dec 15, 2025 | Sync rules management UI with loop prevention |
| Dec 15, 2025 | OAuth fixes, scope mismatch handling |
| Dec 15, 2025 | Per-user credential system |
| Dec 15, 2025 | Dashboard status endpoint fix |