Coordination server for managing Purdue Hackers hardware and systems. Controls doorbells, phones, LED signs, Discord message feeds, and event attendance tracking — all through a unified REST + WebSocket API built on Cloudflare Workers.
Successor to the original API.
- Runtime: Cloudflare Workers
- Framework: Hono
- Database: Cloudflare D1 (SQLite) via Drizzle ORM
- State: Durable Objects (Discord, Doorbell, Phonebell, Sign)
- Validation: Zod
- Language: TypeScript
bun install
bun run db:apply:local
bun devServer runs at http://localhost:3000.
Create a .env.local file
PHONE_API_KEY=
DISCORD_API_KEY=
DOOR_OPENER_API_KEY=
SIGN_PROVISION_KEY=
DSAI_SIGN_API_KEY=
BIDC_SIGN_API_KEY=
| Command | Description |
|---|---|
bun run lint |
Lint with oxlint |
bun run format |
Format with oxfmt |
bun run typecheck |
Type-check with tsc |
bun run test |
Run tests (Vitest + Workers pool) |
bun run db:generate |
Generate Drizzle migrations |
bun run db:push |
Push schema to remote D1 |
bun run db:apply:remote |
Apply migrations to remote D1 |
bun run cf:deploy |
Deploy to Cloudflare |
Returns API info.
Response:
{ "ok": true, "readme": "...", "version": 3 }CRUD for attendance topics and increment/decrement counters. No authentication required.
List all topics with current counts.
Response:
{
"topics": [
{
"id": "uuid",
"name": "Hack Night",
"description": "Weekly hack night",
"createdAtMs": 1711234567890,
"updatedAtMs": 1711234567890,
"count": 42
}
]
}Create a new topic.
Request body:
{
"name": "Hack Night",
"description": "Weekly hack night"
}name— required, 1-120 characters, must be uniquedescription— optional, 0-1000 characters
Response (201): { "topic": { ... } }
Errors: 400 invalid body, 409 name already exists
Get a single topic with its current count.
Response (200): { "topic": { ... } }
Errors: 404 not found
Update a topic's name and/or description.
Request body:
{
"name": "New Name",
"description": "New description"
}Response (200): { "topic": { ... } }
Errors: 400 invalid body, 404 not found, 409 name conflict
Delete a topic and all its events.
Response (200): { "ok": true, "topicId": "uuid" }
Errors: 404 not found
Increment the attendance count by 1.
Response (200): { "ok": true, "topicId": "uuid", "count": 43 }
Errors: 404 not found
Decrement the attendance count by 1.
Response (200): { "ok": true, "topicId": "uuid", "count": 41 }
Errors: 404 not found, 409 count cannot go negative
Real-time Discord message feed via WebSocket. Bot connections require authentication; dashboard connections are receive-only.
Connect the Discord bot. Requires WebSocket upgrade.
Auth: After connecting, send an auth message:
{ "token": "<DISCORD_API_KEY>" }Server responds with { "auth": "complete" } or { "auth": "rejected" } (closes with code 1008).
Messages from bot:
{
"id": "string",
"channel": { "id": "string", "name": "string" },
"author": { "id": "string", "name": "string", "avatarHash": "string|null" },
"timestamp": "ISO 8601",
"content": { "markdown": "string", "html": "string" },
"attachments": ["url", "..."]
}Messages are broadcast to all connected dashboard clients.
Publish a Discord message to all connected dashboard clients. The body uses the same shape as the WebSocket message above.
Auth: Authorization: Bearer <DISCORD_API_KEY>
Response (200): { "ok": true }
Errors: 400 invalid JSON or message shape, 403 invalid API key
Subscribe to receive Discord messages. No authentication. Receives all messages sent by authenticated bots.
Controls the physical doorbell. Supports both WebSocket (real-time) and HTTP.
Connect to doorbell state. Receives broadcasts when ringing state changes.
Messages:
| Type | Direction | Fields |
|---|---|---|
set |
Client → Server | ringing: boolean |
status |
Server → Client | ringing: boolean |
ping / pong |
Both | — |
diagnostic |
Client → Server | level, kind, message |
Get current doorbell state via HTTP.
Response (200): { "ringing": true } or { "ringing": false }
Trigger the doorbell.
Response (200): { "ok": true }
Errors: 400 already ringing
Manages the physical phone system — two phones (outside/inside), door opener, and WebRTC signaling for audio.
Connect the outside phone. Authenticated via handshake protocol.
Messages:
{ "type": "Dial", "number": "string" }
{ "type": "Hook", "state": true }Connect the inside phone. Same protocol as outside.
Connect the door opener device. Receives unlock commands from the phone state machine.
WebRTC signaling relay between phones for peer-to-peer audio.
Trigger the door opener via HTTP.
Auth: Authorization: Bearer <DOOR_OPENER_API_KEY>
Response (204): No content
Errors: 403 invalid API key
Manages LED sign devices — provisioning, device listing, and WiFi configuration.
One-time provisioning of the sign system. Returns the provision key. Can only be called once.
Response (200): { "key": "<SIGN_PROVISION_KEY>" }
Errors: 403 already provisioned or provisioning disabled
List all connected sign devices.
Response (200): { "devices": ["device-name-1", "device-name-2"] }
Get WiFi networks from a specific sign device. 10-second timeout.
Response (200): { "networks": [...] }
Set WiFi configuration for a sign device.
Request body:
{
"networks": [
{
"ssid": "PurdueHackers",
"password": "secret",
"network_type": "personal"
}
]
}Supports "personal" and "enterprise" network types. Enterprise networks accept optional enterprise_email and enterprise_username fields.
Response (200): { "ok": true }
Connect a sign device. Requires authentication after connecting:
{ "type": "auth", "key": "<DSAI_SIGN_API_KEY or BIDC_SIGN_API_KEY>" }Messages:
| Type | Direction | Fields |
|---|---|---|
auth |
Device → Server | key |
status |
Device → Server | — |
ping / pong |
Both | — |
wifi_networks |
Device → Server | request_id, networks |
wifi_ack |
Device → Server | request_id |
src/
├── actors/ # Durable Objects (stateful WebSocket actors)
│ ├── discord/ # Discord bot ↔ dashboard message relay
│ ├── doorbell/ # Doorbell state management
│ ├── phonebell/ # Phone system state machine + signaling
│ └── sign/ # Sign device registry + WiFi config
├── db/ # Drizzle ORM schema and connection
├── lib/ # Shared utilities and types
├── protocol/ # Zod message schemas for WebSocket protocols
├── server/ # Hono route definitions
├── services/ # Business logic (AttendanceService)
└── index.ts # Worker entry point
Each Durable Object maintains persistent WebSocket connections and state. The attendance system uses D1 (SQLite) for durable storage via Drizzle ORM.