A production-ready scheduling API built for Cloudflare Workers that uses bitset-based storage for ultra-fast availability tracking and conflict detection. Perfect for building interview scheduling, appointment booking, or any calendar-based application.
- Lightning Fast: 84 bytes per week storage, O(1) slot lookups
- Global Edge: Deployed on Cloudflare's network for <25ms latency worldwide
- Type Safe: Full TypeScript with Zod validation
- Two-Layer Architecture: Separate availability (capacity) from bookings (appointments)
- Multi-User Scheduling: Find common availability across multiple users instantly
- Production Ready: Complete error handling, CORS, logging, and monitoring
This API separates Availability from Bookings:
┌─────────────────────────────────────────────────────────────┐
│ LAYER 1: AVAILABILITY │
│ User declares: "I'm free Mon-Fri 9am-5pm" │
│ Stored in bitset (1 = available, 0 = not available) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ FREE SLOTS = Availability AND NOT Bookings │
│ Query: "Show me truly bookable time" │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ LAYER 2: BOOKINGS │
│ Guest books: "Interview at Tue 2pm" │
│ Validation: Must be within available time │
└─────────────────────────────────────────────────────────────┘
- Availability: When an user CAN work (their capacity/working hours)
- Bookings: Actual scheduled appointments/interviews
- Free Slots: Available time minus existing bookings = truly bookable time
- Node.js 18 or higher
- Cloudflare account (free tier works - only needed for production)
- Wrangler CLI installed globally
npm install -g wrangler# Clone the repository
git clone https://github.com/orkait/scheduling-api.git
cd scheduling-api
# Install dependencies
npm installYou can run the API in two modes:
Best for: Rapid development, testing, no database setup needed
# Create local environment file
cp .dev.vars.example .dev.vars
# Edit .dev.vars and set:
# STORAGE_ADAPTER=memory
# Start development server
npm run dev:memory✅ Pros: Zero setup, fast, perfect for development
Best for: Testing persistence, production-like environment
# Login to Cloudflare
wrangler login
# Create local D1 database
wrangler d1 create orkait_cal_local
# Update wrangler.toml with database_id
# Run migrations
npm run db:migrate:local
# Start development server with persistent storage
npm run dev:d1✅ Pros: Data persists across restarts
The API will be available at http://localhost:8787
💡 Tip: For quick API testing, use Option 1 (Memory Adapter). Switch to Option 2 when you need persistent data.
📚 For detailed deployment guide: See DEPLOYMENT.md
Local: http://localhost:8787/api
Production: https://your-worker.workers.dev/api
1. User sets availability
PUT /api/availability → "I'm free Mon-Fri 9am-5pm"
2. Guest queries free slots
GET /api/free-slots → Shows available time minus existing bookings
3. Guest books interview
POST /api/bookings → Creates appointment within available time
Mark time slots as available (user declaring working hours):
PUT /api/availability
Content-Type: application/json
{
"userId": "user_123",
"year": 2026,
"week": 10,
"day": 1,
"startSlot": 36,
"endSlot": 68
}Response (200)
{
"success": true,
"data": [{ "day": 1, "startSlot": 36, "endSlot": 68 }]
}Get declared availability for a week:
GET /api/availability?userId=user_123&year=2026&week=10Response
{
"success": true,
"data": [
{ "day": 1, "startSlot": 36, "endSlot": 68 },
{ "day": 2, "startSlot": 36, "endSlot": 68 }
]
}Remove availability for a time range:
DELETE /api/availability
Content-Type: application/json
{
"userId": "user_123",
"year": 2026,
"week": 10,
"day": 1,
"startSlot": 36,
"endSlot": 68
}Check if a specific slot is available:
GET /api/availability/check?userId=user_123&year=2026&week=10&day=1&slot=40Response
{
"success": true,
"isAvailable": true
}Find overlapping availability between multiple users:
POST /api/availability/overlap
Content-Type: application/json
{
"userIds": ["user_123", "user_456"],
"year": 2026,
"week": 10
}Query truly free time (availability minus existing bookings):
GET /api/free-slots?userId=user_123&year=2026&week=10&day=1Response
{
"success": true,
"data": [
{ "day": 1, "startSlot": 36, "endSlot": 48 },
{ "day": 1, "startSlot": 52, "endSlot": 68 }
]
}Create a new appointment (validates against availability):
POST /api/bookings
Content-Type: application/json
{
"userId": "user_123",
"year": 2026,
"week": 10,
"day": 1,
"startSlot": 48,
"endSlot": 52,
"guestId": "guest_456",
"title": "Technical Interview"
}Success Response (201)
{
"success": true,
"data": {
"id": "evt_550e8400-e29b-41d4-a716-446655440000",
"userId": "user_123",
"year": 2026,
"week": 10,
"day": 1,
"startSlot": 48,
"endSlot": 52,
"guestId": "guest_456",
"title": "Technical Interview",
"createdAt": 1736764200000,
"version": 1
}
}Error: Outside Available Hours (400)
{
"success": false,
"error": "Time slot is outside available hours",
"conflicts": [{ "day": 1, "startSlot": 80, "endSlot": 84 }]
}Error: Booking Conflict (409)
{
"success": false,
"error": "Time slot conflicts with existing bookings",
"conflicts": [{ "day": 1, "startSlot": 48, "endSlot": 52 }]
}GET /api/bookings/:idPUT /api/bookings/:id
Content-Type: application/json
{
"startSlot": 52,
"endSlot": 56
}Optimistic Locking: Include version in request body to prevent race conditions:
{
"startSlot": 52,
"endSlot": 56,
"version": 1
}If the booking was modified by another request, you'll receive a conflict error.
DELETE /api/bookings/:idGET /api/bookings?userId=user_123&year=2026&week=10The API uses 15-minute slots:
| Time | Slot | Time | Slot |
|---|---|---|---|
| 00:00 | 0 | 12:00 | 48 |
| 09:00 | 36 | 15:00 | 60 |
| 10:00 | 40 | 17:00 | 68 |
| 11:00 | 44 | 23:45 | 95 |
Days: 0=Monday, 1=Tuesday, ..., 6=Sunday
Weeks: ISO 8601 week numbers (1-53)
Years: 2025-2036 supported
# User is available Monday 9am-5pm
curl -X PUT http://localhost:8787/api/availability \
-H "Content-Type: application/json" \
-d '{
"userId": "user_1",
"year": 2026,
"week": 10,
"day": 0,
"startSlot": 36,
"endSlot": 68
}'# Guest checks available times
curl "http://localhost:8787/api/free-slots?userId=user_1&year=2026&week=10&day=0"
# Response: All 9am-5pm is free (no bookings yet)
# { "data": [{ "day": 0, "startSlot": 36, "endSlot": 68 }] }# Guest books 10am-11am
curl -X POST http://localhost:8787/api/bookings \
-H "Content-Type: application/json" \
-d '{
"userId": "user_1",
"year": 2026,
"week": 10,
"day": 0,
"startSlot": 40,
"endSlot": 44,
"guestId": "guest_1",
"title": "Technical Interview"
}'# Another guest checks available times
curl "http://localhost:8787/api/free-slots?userId=user_1&year=2026&week=10&day=0"
# Response: 10am-11am is now booked
# { "data": [
# { "day": 0, "startSlot": 36, "endSlot": 40 }, // 9am-10am
# { "day": 0, "startSlot": 44, "endSlot": 68 } // 11am-5pm
# ]}# Try to book at 8pm (outside available hours)
curl -X POST http://localhost:8787/api/bookings \
-H "Content-Type: application/json" \
-d '{
"userId": "user_1",
"year": 2026,
"week": 10,
"day": 0,
"startSlot": 80,
"endSlot": 84
}'
# Response: 400 Bad Request
# { "error": "Time slot is outside available hours" }src/
├── routes/ # HTTP endpoints
│ ├── availability.routes.ts # Availability management
│ ├── free-slots.routes.ts # Free slots query
│ ├── bookings.routes.ts # Booking management
│ └── index.ts
├── services/ # Business logic
│ └── booking.service.ts # Two-layer architecture
├── controllers/ # Core bitset logic
│ ├── utils/
│ │ ├── bitset.ts
│ │ ├── timeslot.ts
│ │ ├── merge.ts
│ │ ├── event.ts
│ │ └── helper.ts
│ └── types.ts
├── adapters/ # Storage layer
│ ├── adapter.ts
│ ├── d1_adapter.ts
│ ├── memory_adapter.ts
│ └── adapter_types.ts
├── middleware/ # Request pipeline
├── schemas/ # Zod validation
└── server.ts # Main entry point
npm run dev # Start local dev server
npm run build # Compile TypeScript
npm test # Run tests
npm run type-check # Check TypeScript types
npm run db:migrate:local # Run migrations locally
npm run deploy # Deploy to production| Operation | Time | Storage |
|---|---|---|
| Check single slot | ~1 ns | - |
| Set availability | ~1 μs | - |
| Get free slots | ~5 μs | 84 bytes/week |
| Create booking | ~10 μs | ~250 bytes/event |
| Cold start | ~10 ms | - |
| Total latency | 15-25 ms | - |
The API supports 3 authentication methods. All /api/* endpoints (except /api/health) require authentication.
curl -H "X-API-Key: your-api-key" https://api.example.com/api/availabilityConfigure keys in environment variable:
API_KEYS=key1:user1,key2:user2,key3:user3
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." https://api.example.com/api/availabilityJWT Requirements:
- Algorithm: HS256
- Secret: Set via
JWT_SECRETenvironment variable - Required claims:
sub(user ID),exp(expiration timestamp) - Optional claims:
roles(array of role strings),iat(issued at)
curl -H "CF-Access-JWT-Assertion: eyJhbGciOiJSUzI1NiIs..." https://api.example.com/api/availabilityWhen CF_ACCESS_TEAM_DOMAIN is set, the signature is verified against Cloudflare's public keys.
When ENVIRONMENT=development or ENVIRONMENT=test, you can use the X-User-Id header to simulate any user:
curl -H "X-User-Id: alice" http://localhost:8787/api/availability- JWT Validation: Enforced expiration, signature verification, and subject validation
- CF Access Verification: Full RS256 signature verification using Cloudflare's public keys
- Optimistic Locking: Version-based concurrency control prevents race conditions
- SQL Injection Prevention: All queries use parameterized statements
- Secure ID Generation: Uses
crypto.randomUUID()for unpredictable booking IDs
- ARCHITECTURE.md - Detailed technical architecture
- DATABASE.md - Database schema and management
- DEPLOYMENT.md - Deployment guide
- Cloudflare Workers Docs
- Hono Framework
- D1 Database
MIT License - see LICENSE file for details
Built with ❤️ by Orkait