Skip to content

orkait/event-calendar

Repository files navigation

Orkait Scheduling API

High-performance scheduling/availability API with bitset-based storage

Cloudflare Workers TypeScript Hono D1


Overview

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.

Why This API?

  • 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

Two-Layer Architecture

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                  │
└─────────────────────────────────────────────────────────────┘

Key Concepts

  • 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

Quick Start

Prerequisites

  • Node.js 18 or higher
  • Cloudflare account (free tier works - only needed for production)
  • Wrangler CLI installed globally
npm install -g wrangler

Installation

# Clone the repository
git clone https://github.com/orkait/scheduling-api.git
cd scheduling-api

# Install dependencies
npm install

Development Mode Options

You can run the API in two modes:

Option 1: Memory Adapter (Recommended for Quick Start)

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 ⚠️ Note: Data resets on restart (not persistent)

Option 2: D1 Adapter (Local Database)

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 ⚠️ Note: Requires D1 setup

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


API Usage

Base URL

Local: http://localhost:8787/api
Production: https://your-worker.workers.dev/api

Typical Workflow

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

API Endpoints

Availability Management

Set Availability

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 Availability

Get declared availability for a week:

GET /api/availability?userId=user_123&year=2026&week=10

Response

{
 "success": true,
 "data": [
  { "day": 1, "startSlot": 36, "endSlot": 68 },
  { "day": 2, "startSlot": 36, "endSlot": 68 }
 ]
}

Clear Availability

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 Slot Availability

Check if a specific slot is available:

GET /api/availability/check?userId=user_123&year=2026&week=10&day=1&slot=40

Response

{
 "success": true,
 "isAvailable": true
}

Find Common Availability

Find overlapping availability between multiple users:

POST /api/availability/overlap
Content-Type: application/json

{
  "userIds": ["user_123", "user_456"],
  "year": 2026,
  "week": 10
}

Free Slots Query

Get Bookable Time

Query truly free time (availability minus existing bookings):

GET /api/free-slots?userId=user_123&year=2026&week=10&day=1

Response

{
 "success": true,
 "data": [
  { "day": 1, "startSlot": 36, "endSlot": 48 },
  { "day": 1, "startSlot": 52, "endSlot": 68 }
 ]
}

Bookings

Create Booking

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 Booking

GET /api/bookings/:id

Update Booking

PUT /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 Booking

DELETE /api/bookings/:id

List Bookings

GET /api/bookings?userId=user_123&year=2026&week=10

Understanding Time Slots

The 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


Complete Example

1. Set User Availability

# 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
  }'

2. Query Free Slots

# 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 }] }

3. Create Booking

# 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"
  }'

4. Query Free Slots Again

# 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
# ]}

5. Attempt Booking Outside Availability

# 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" }

Development

Project Structure

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

Available Scripts

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

Performance

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 -

Authentication

The API supports 3 authentication methods. All /api/* endpoints (except /api/health) require authentication.

1. API Key (Recommended for Server-to-Server)

curl -H "X-API-Key: your-api-key" https://api.example.com/api/availability

Configure keys in environment variable:

API_KEYS=key1:user1,key2:user2,key3:user3

2. JWT Bearer Token (Recommended for Web Apps)

curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." https://api.example.com/api/availability

JWT Requirements:

  • Algorithm: HS256
  • Secret: Set via JWT_SECRET environment variable
  • Required claims: sub (user ID), exp (expiration timestamp)
  • Optional claims: roles (array of role strings), iat (issued at)

3. Cloudflare Access (Recommended for CF-Protected Apps)

curl -H "CF-Access-JWT-Assertion: eyJhbGciOiJSUzI1NiIs..." https://api.example.com/api/availability

When CF_ACCESS_TEAM_DOMAIN is set, the signature is verified against Cloudflare's public keys.

Development Mode

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

Security Features

  • 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

Documentation


License

MIT License - see LICENSE file for details


Built with ❤️ by Orkait

WebsiteDocumentation

About

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.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors