Castio — Platform Design Document
Date: 2026-03-06 Status: Draft — pending user approval
Table of Contents
- Product Overview
- System Architecture
- Diagnostics & Observability
- User System & Authentication
- Role-to-Preset Mapping
- Meeting Lifecycle
- RTK Feature Mapping
- Post-Meeting Experience
- Webhook & Analytics Pipeline
- App Pages & Navigation
- Branding & Theming
- Organization Configuration
- Future Features
- Deployment App (Separate Product)
1. Product Overview
1.1 What This Is
A self-hosted, white-label video/voice collaboration platform deployed entirely within the customer's own Cloudflare account. The platform supports three meeting types — group video calls, audio-only rooms, and webinars with stage management — plus RTMP/HLS livestreaming. All real-time media, participant state, recording, transcription, chat, and polls are handled by Cloudflare RealtimeKit. We build everything RealtimeKit does not provide: user management, scheduling, analytics, diagnostics, branding, and abuse protection.
1.2 Deployment Model
- Self-hosted: Runs on the customer's Cloudflare account (Workers, D1, R2, KV)
- Zero vendor connection: No traffic routes to our infrastructure at runtime
- Optional phone-home diagnostics: Health telemetry can be sent to a vendor endpoint; enabled by default (pre-configured by deployment app), toggled per-org
- Single-command deployment:
wrangler deployfrom repo (deployment app is a separate future product)
1.3 What RealtimeKit Handles (We Do NOT Rebuild)
| Capability | RTK Component | Our Role |
|---|---|---|
| Media routing (video/audio/screen share) | SFU + Core SDK | Wire up SDK, expose in UI |
| Participant state & permissions | Presets + REST API | Create presets matching our roles, call REST API |
| Meeting lifecycle (join/leave/active/inactive) | REST API + SDK events | Proxy REST calls, listen to SDK events |
| Chat (public + private DMs + pin) | Core SDK + UI Kit | Render `rtk-chat` components |
| Polls | Core SDK + UI Kit | Render `rtk-polls` components |
| Stage management (webinars) | Presets + UI Kit | Render stage components, configure presets |
| Waiting rooms | Presets + UI Kit | Configure preset, render waiting screen |
| Breakout rooms | Connected Meetings API | Render `rtk-breakout-rooms-manager` (web only, beta) |
| Recording (composite) | REST API + Recording SDK | Start/stop via API, configure cloud storage |
| Transcription (Whisper Large v3 Turbo) | AI feature + webhooks | Enable via preset, display `rtk-ai-transcriptions` |
| AI meeting summaries | AI feature + webhooks | Consume `meeting.summary` webhook |
| Virtual backgrounds | `@cloudflare/realtimekit-virtual-background` | Include addon |
| Plugins (whiteboard, doc sharing) | Plugin system + UI Kit | Enable via preset |
| Simulcast | Core SDK config | Set simulcast options |
| TURN relay (ICE fallback) | Managed by RTK internally | No action needed |
| Message broadcasting | Server-side API | Call from Worker when needed |
| Collaborative stores | Server-side API | Use for custom real-time state sync |
1.4 What We Build (Not in RTK)
| Feature | Why Custom | Storage |
|---|---|---|
| **User system** (accounts, auth, roles) | RTK has participants, not users | D1 |
| **Meeting scheduling** (instant, scheduled, recurring, permanent rooms) | RTK meetings have no time concept | D1 |
| **Post-meeting experience** (recording playback, transcript viewer, AI summary display) | RTK stores data 7 days, we persist references | D1 + R2 |
| **Analytics** (session counts, durations, participant stats, recording stats) | RTK provides basic daywise/livestream analytics via REST API; we supplement with granular per-meeting and custom-dimension analytics | D1 (webhook-fed + RTK analytics API) |
| **Diagnostics & observability** (error tracking, health checks, Sentry) | Enterprise requirement, not in RTK | Sentry + Workers Observability + D1 (2 tables) |
| **Org configuration** (settings, branding, policies) | Platform-level concern | D1 + R2 |
| **Abuse protection** (rate limiting, capacity caps, guest controls) | Platform-level concern | KV + D1 |
1.5 Meeting Types and Preset Mapping
Meeting type is determined by which preset is applied to participants at join time. Our platform defines these presets:
| Our Role | RTK Preset Type | Key Permissions |
|---|---|---|
| **Owner** | Video + all host controls | Full admin: kick, mute others, manage stage, create polls, manage breakouts |
| **Admin** | Video + all host controls | Same as Owner (platform-level distinction only — RTK treats identically) |
| **Host** | Video + host controls | Kick, mute others, manage stage, admit from waiting room |
| **Member** | Video + standard | Audio, video, screen share, chat, polls, waiting room bypass |
| **Guest** | Video + restricted | Audio only, public chat only, no screen share, must go through waiting room |
| **Webinar Viewer** | Webinar + viewer | View stage, public chat, polls — no media publishing |
| **Webinar Panelist** | Webinar + stage | Can be added to stage by host, audio/video when on stage |
| **Livestream Viewer** | Livestream + viewer | HLS playback only, chat |
1.6 Scope Boundaries
In scope (v1, web-first):
- All Tier 1 features from gap analysis (meeting lifecycle, chat, polls, stage, recording, transcription, AI summaries, virtual backgrounds, plugins, simulcast, webhooks)
- RTMP/HLS livestreaming (sparse docs — discovery risk accepted)
- Breakout rooms (beta, web only)
- Full diagnostics/observability layer
Low priority (groundwork only):
- Realtime Agents (DO binding + route stubs only; SDK is experimental/transitional)
Needs research:
Per-track recording— MOVED TO BUILD.POST /recordings/trackAPI is documented with layers system for mapping audio/video tracks to output destinations. Not raw RTP export.
Future / dropped:
- Noise cancellation (third-party client-side lib — future)
- SIP interconnect (does not exist in RTK — dropped)
2. System Architecture
2.1 High-Level Component Diagram
┌─────────────────────────────────────────────────────────────┐
│ Customer's Cloudflare Account │
│ │
│ ┌──────────────┐ ┌────────────────────────────────────┐ │
│ │ React SPA │◄──►│ Cloudflare Worker (API + Webhooks)│ │
│ │ (Pages/R2) │ │ routes: /api/*, /webhooks/* │ │
│ │ RTK UI Kit │ └──────┬──────┬──────┬───────┬───────┘ │
│ │ RTK Core SDK│ │ │ │ │ │
│ └──────┬───────┘ │ │ │ │ │
│ │ ▼ ▼ ▼ ▼ │
│ │ ┌─────┐ ┌────┐ ┌────┐ ┌──────┐ │
│ │ │ D1 │ │ R2 │ │ KV │ │ DO │ │
│ │ │(DB) │ │(fs)│ │(ch)│ │(agt) │ │
│ │ └─────┘ └────┘ └────┘ └──────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ RealtimeKit │ ◄── SDK connects directly from │
│ │ (SFU + services)│ browser to RTK infrastructure │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ (optional)
▼
┌──────────────────┐
│ Sentry (SaaS) │ ◄── Error tracking from Worker + SPA (vendor DSN + optional customer DSN)
└──────────────────┘2.2 Cloudflare Worker — Route Architecture
Single Worker handling all server-side logic. Routes organized by concern:
Auth Routes
| Method | Route | Purpose |
|---|---|---|
| POST | `/api/auth/register` | Create user account |
| POST | `/api/auth/login` | Authenticate, return JWT |
| POST | `/api/auth/refresh` | Refresh JWT |
| POST | `/api/auth/logout` | Invalidate session |
| GET | `/api/auth/me` | Current user profile |
User & Org Routes
| Method | Route | Purpose |
|---|---|---|
| GET | `/api/users` | List org users (admin+) |
| PATCH | `/api/users/:userId` | Update user role/profile |
| DELETE | `/api/users/:userId` | Remove user from org |
| GET | `/api/org/settings` | Get org configuration |
| PATCH | `/api/org/settings` | Update org settings (owner/admin) |
| POST | `/api/org/branding` | Upload logo/assets to R2 |
Meeting Routes
| Method | Route | Purpose |
|---|---|---|
| POST | `/api/meetings` | Create meeting (calls RTK REST: `POST /meetings`) |
| GET | `/api/meetings` | List meetings with filters |
| GET | `/api/meetings/:meetingId` | Get meeting details |
| PATCH | `/api/meetings/:meetingId` | Update meeting (title, schedule, settings) |
| DELETE | `/api/meetings/:meetingId` | Deactivate meeting |
| POST | `/api/meetings/:meetingId/join` | Create RTK participant, return `authToken` + meeting config |
| GET | `/api/meetings/:meetingId/participants` | List participants |
| POST | `/api/meetings/:meetingId/recording/start` | Start composite recording via RTK API |
| POST | `/api/meetings/:meetingId/recording/stop` | Stop recording via RTK API |
| GET | `/api/meetings/:meetingId/recordings` | List recordings for meeting |
| GET | `/api/meetings/:meetingId/transcripts` | Get transcripts (from RTK or cached) |
| GET | `/api/meetings/:meetingId/summary` | Get AI summary |
Scheduling Routes
| Method | Route | Purpose |
|---|---|---|
| POST | `/api/schedule` | Create scheduled meeting (stores time + recurrence in D1) |
| GET | `/api/schedule` | List upcoming scheduled meetings |
| PATCH | `/api/schedule/:scheduleId` | Update schedule |
| DELETE | `/api/schedule/:scheduleId` | Cancel scheduled meeting |
Analytics Routes
| Method | Route | Purpose |
|---|---|---|
| GET | `/api/analytics/overview` | Dashboard stats (sessions, participants, duration) |
| GET | `/api/analytics/meetings` | Per-meeting analytics |
| GET | `/api/analytics/usage` | Usage trends over time |
Diagnostics Routes
| Method | Route | Purpose |
|---|---|---|
| GET | `/api/diagnostics/health` | System health check (probe results) |
| GET | `/api/diagnostics/webhooks` | Webhook delivery status/failures (paginated) |
| GET | `/api/diagnostics/webhooks/:id` | Single webhook delivery detail |
| GET | `/api/diagnostics/active-meetings` | Currently active meetings |
| POST | `/api/diagnostics/phone-home` | Manual trigger for phone-home report |
| PATCH | `/api/diagnostics/settings` | Update diagnostics settings (toggle Sentry, phone-home, customer DSN) |
Webhook Receiver Route
| Method | Route | Purpose |
|---|---|---|
| POST | `/webhooks/rtk` | Receive all RTK webhook events |
Agent Routes (Groundwork Only)
| Method | Route | Purpose |
|---|---|---|
| POST | `/api/agents/:meetingId/init` | Initialize agent DO for meeting |
| POST | `/api/agents/:meetingId/deinit` | Tear down agent |
| ALL | `/agentsInternal/*` | DO internal pipeline (forwarded to DO fetch) |
2.3 D1 Database Schema
`users`
CREATE TABLE users (
id TEXT PRIMARY KEY, -- ulid
org_id TEXT NOT NULL, -- tenant isolation
email TEXT NOT NULL,
display_name TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('owner','admin','host','member','guest')),
password_hash TEXT NOT NULL,
avatar_url TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
last_login_at TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
UNIQUE(org_id, email)
);
CREATE INDEX idx_users_org ON users(org_id);
CREATE INDEX idx_users_email ON users(org_id, email);`meetings`
CREATE TABLE meetings (
id TEXT PRIMARY KEY, -- ulid
org_id TEXT NOT NULL,
rtk_meeting_id TEXT, -- RealtimeKit meeting ID (from REST API response)
title TEXT NOT NULL,
description TEXT,
meeting_type TEXT NOT NULL CHECK(meeting_type IN ('video','audio','webinar','livestream')),
status TEXT NOT NULL DEFAULT 'inactive' CHECK(status IN ('active','inactive')),
created_by TEXT NOT NULL REFERENCES users(id),
is_permanent_room INTEGER NOT NULL DEFAULT 0,
max_participants INTEGER,
waiting_room_enabled INTEGER NOT NULL DEFAULT 1,
recording_auto_start INTEGER NOT NULL DEFAULT 0,
transcription_enabled INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_meetings_org ON meetings(org_id);
CREATE INDEX idx_meetings_status ON meetings(org_id, status);
CREATE INDEX idx_meetings_rtk ON meetings(rtk_meeting_id);`schedules`
CREATE TABLE schedules (
id TEXT PRIMARY KEY, -- ulid
org_id TEXT NOT NULL,
meeting_id TEXT NOT NULL REFERENCES meetings(id),
starts_at TEXT NOT NULL, -- ISO 8601
ends_at TEXT, -- ISO 8601, nullable for open-ended
timezone TEXT NOT NULL DEFAULT 'UTC',
recurrence_rule TEXT, -- iCalendar RRULE string, null = one-time
created_by TEXT NOT NULL REFERENCES users(id),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_schedules_org_time ON schedules(org_id, starts_at);
CREATE INDEX idx_schedules_meeting ON schedules(meeting_id);`sessions`
CREATE TABLE sessions (
id TEXT PRIMARY KEY, -- ulid
org_id TEXT NOT NULL,
meeting_id TEXT NOT NULL REFERENCES meetings(id),
rtk_session_id TEXT, -- RTK session ID from webhook/SDK
started_at TEXT NOT NULL DEFAULT (datetime('now')),
ended_at TEXT,
participant_count INTEGER NOT NULL DEFAULT 0,
peak_participant_count INTEGER NOT NULL DEFAULT 0,
duration_seconds INTEGER, -- computed on session end
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_sessions_org ON sessions(org_id);
CREATE INDEX idx_sessions_meeting ON sessions(meeting_id);
CREATE INDEX idx_sessions_started ON sessions(org_id, started_at);`participants`
CREATE TABLE participants (
id TEXT PRIMARY KEY, -- ulid
org_id TEXT NOT NULL,
session_id TEXT NOT NULL REFERENCES sessions(id),
meeting_id TEXT NOT NULL REFERENCES meetings(id),
user_id TEXT REFERENCES users(id), -- null for guests
rtk_participant_id TEXT, -- RTK participant ID
display_name TEXT NOT NULL,
role TEXT NOT NULL,
joined_at TEXT NOT NULL DEFAULT (datetime('now')),
left_at TEXT,
duration_seconds INTEGER,
is_guest INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_participants_session ON participants(session_id);
CREATE INDEX idx_participants_meeting ON participants(meeting_id);
CREATE INDEX idx_participants_user ON participants(user_id);`recordings`
CREATE TABLE recordings (
id TEXT PRIMARY KEY, -- ulid
org_id TEXT NOT NULL,
meeting_id TEXT NOT NULL REFERENCES meetings(id),
session_id TEXT REFERENCES sessions(id),
rtk_recording_id TEXT,
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','recording','processing','ready','failed','expired')),
storage_provider TEXT NOT NULL DEFAULT 'r2' CHECK(storage_provider IN ('r2','aws','azure','digitalocean','gcs','sftp')),
storage_url TEXT, -- R2 key or external URL
file_size_bytes INTEGER,
duration_seconds INTEGER,
codec TEXT,
started_at TEXT,
stopped_at TEXT,
expires_at TEXT, -- RTK default: 7 days
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_recordings_org ON recordings(org_id);
CREATE INDEX idx_recordings_meeting ON recordings(meeting_id);
CREATE INDEX idx_recordings_status ON recordings(org_id, status);`transcripts`
CREATE TABLE transcripts (
id TEXT PRIMARY KEY, -- ulid
org_id TEXT NOT NULL,
meeting_id TEXT NOT NULL REFERENCES meetings(id),
session_id TEXT REFERENCES sessions(id),
content TEXT NOT NULL, -- full transcript text or JSON
format TEXT NOT NULL DEFAULT 'text' CHECK(format IN ('text','json','vtt')),
language TEXT DEFAULT 'en',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_transcripts_meeting ON transcripts(meeting_id);`meeting_summaries`
CREATE TABLE meeting_summaries (
id TEXT PRIMARY KEY, -- ulid
org_id TEXT NOT NULL,
meeting_id TEXT NOT NULL REFERENCES meetings(id),
session_id TEXT REFERENCES sessions(id),
summary_text TEXT NOT NULL,
summary_type TEXT, -- e.g., 'action_items', 'key_points', 'full'
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_summaries_meeting ON meeting_summaries(meeting_id);`org_settings`
Note: This is a simplified overview. See Section 12.3 for the full canonical schema with all branding, policy, and watermark columns.
CREATE TABLE org_settings (
id TEXT PRIMARY KEY DEFAULT 'default', -- single-row table
org_name TEXT NOT NULL,
org_slug TEXT NOT NULL UNIQUE,
rtk_app_id TEXT, -- RealtimeKit App ID
logo_url TEXT, -- R2 key
brand_color TEXT DEFAULT '#2563EB',
default_meeting_type TEXT DEFAULT 'video',
max_meeting_duration_minutes INTEGER DEFAULT 480,
max_participants_per_meeting INTEGER DEFAULT 200,
guest_access_enabled INTEGER NOT NULL DEFAULT 1,
waiting_room_default INTEGER NOT NULL DEFAULT 1,
recording_storage_provider TEXT DEFAULT 'r2',
recording_storage_config TEXT, -- JSON: bucket, credentials (encrypted)
transcription_default INTEGER NOT NULL DEFAULT 0,
phone_home_enabled INTEGER NOT NULL DEFAULT 0,
phone_home_url TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);Diagnostics tables (2 tables) — see [Section 3](#3-diagnostics--observability) for full schema.
diagnostic_webhook_deliveries— webhook processing status trackingdiagnostic_health_snapshots— periodic health probe results
2.4 R2 Storage
| Bucket | Contents | Access Pattern |
|---|---|---|
| `{org}-branding` | Logos, custom backgrounds, favicon | Read on page load, write on settings update |
| `{org}-recordings` | Composite recordings (if using R2 over external) | Write from RTK, read on playback |
| `{org}-exports` | Analytics exports, transcript downloads | Write on demand, read once |
2.5 KV Namespaces
| Namespace | Key Pattern | Value | TTL |
|---|---|---|---|
| `sessions` | `session:{jwt_id}` | `{userId, orgId, role, exp}` | JWT expiry (e.g., 24h) |
| `rate-limits` | `rl:{orgId}:{route}:{minute}` | Request count (integer) | 60s |
| `meeting-state` | `active:{orgId}:{meetingId}` | `{sessionId, participantCount, startedAt}` | None (deleted on session end) |
| `cache` | `cache:{orgId}:{resource}:{id}` | Cached API responses | 30-300s depending on resource |
2.6 Durable Objects (Groundwork Only)
Reserved for Realtime Agents. Not built in v1 but wrangler config includes the binding:
// wrangler.jsonc (relevant section)
{
"durable_objects": {
"bindings": [
{ "class_name": "RealtimeAgent", "name": "REALTIME_AGENT" }
]
},
"migrations": [
{ "new_sqlite_classes": ["RealtimeAgent"], "tag": "v1" }
]
}2.7 Worker Bindings Summary
// wrangler.jsonc bindings
{
"d1_databases": [
{ "binding": "DB", "database_name": "rtk-platform", "database_id": "..." }
],
"r2_buckets": [
{ "binding": "R2_BRANDING", "bucket_name": "rtk-branding" },
{ "binding": "R2_RECORDINGS", "bucket_name": "rtk-recordings" },
{ "binding": "R2_EXPORTS", "bucket_name": "rtk-exports" }
],
"kv_namespaces": [
{ "binding": "KV_SESSIONS", "id": "..." },
{ "binding": "KV_RATE_LIMITS", "id": "..." },
{ "binding": "KV_MEETING_STATE", "id": "..." },
{ "binding": "KV_CACHE", "id": "..." }
],
"vars": {
"RTK_API_BASE": "https://api.cloudflare.com/client/v4/accounts",
"PHONE_HOME_URL": ""
},
"secrets": ["ACCOUNT_ID", "RTK_API_TOKEN", "JWT_SECRET", "SENTRY_DSN_VENDOR", "SENTRY_DSN_CUSTOMER", "SENTRY_RELEASE"]
}2.8 Frontend Architecture
- React SPA deployed to Cloudflare Pages (or served from R2 behind Worker)
- RTK UI Kit (
@cloudflare/realtimekit-react-ui) for all meeting UI — 136 pre-built components - RTK Core SDK (
@cloudflare/realtimekit-react) for programmatic control and custom UI where needed - Routing: Client-side (React Router). Meeting room is a single route (
/meeting/:meetingId) that loads the full RTK UI Kit experience - State management: RTK SDK manages all meeting state; React context for app-level state (auth, settings)
- Addons loaded: Virtual background, recording, AI transcription
2.9 Data Flow: Meeting Join Sequence
User clicks "Join Meeting"
→ SPA calls POST /api/meetings/:meetingId/join (with JWT)
→ Worker validates JWT, checks user role + meeting permissions
→ Worker calls RTK REST API: POST /realtime/kit/{APP_ID}/meetings/{MEETING_ID}/participants
with preset matching user's role
→ RTK returns { participantId, authToken }
→ Worker stores participant record in D1
→ Worker updates KV meeting-state (increment participant count)
→ Worker returns { authToken, meetingConfig, preset_name } to SPA
→ SPA initializes RTK SDK: RealtimeKitClient.init({ authToken })
→ SDK connects to RTK SFU (direct browser → Cloudflare edge)
→ Meeting UI renders via RTK UI Kit components2.10 Data Flow: Webhook Processing
RTK fires webhook event (e.g., recording.statusUpdate)
→ POST /webhooks/rtk
→ Worker validates webhook signature
→ Worker classifies event type
→ Worker routes to handler:
├── recording.statusUpdate → update recordings table, notify via WS if active
├── meeting.chatSynced → store chat export reference
├── meeting.summary → store summary in D1
├── meeting.started → create session record, update KV meeting-state
├── meeting.ended → close session record, compute duration, update analytics
├── meeting.participantJoined → update participant count
├── meeting.participantLeft → update participant record, compute duration
├── meeting.transcript → store transcript in D1
└── livestreaming.statusUpdate → update livestream state
→ Handler writes to D1 + KV as needed
→ Handler writes to diagnostic_webhook_deliveries (success or failure); errors captured by Sentry automatically
→ Worker returns 200 OK (or 500 + logs failure for retry)3. Diagnostics & Observability
3.1 Design Philosophy
Diagnostics is a core architectural layer, not an afterthought. The guiding principle: know about issues before customers call. The code is always present — orgs can disable reporting but the instrumentation is never stripped.
We achieve this by layering three systems that each do what they're best at:
| Layer | Tool | What it handles | Why not something else |
|---|---|---|---|
| **Error tracking & investigation** | Sentry (`@sentry/cloudflare` + `@sentry/react`) | Error capture, deduplication, grouping, stack traces, release tracking, alerting | Purpose-built for this; replacing it with D1 tables would be rebuilding Sentry poorly |
| **Request tracing & I/O monitoring** | Cloudflare Workers Observability | Auto-traces every `fetch()`, KV, R2, D1, DO call. Zero code needed. OpenTelemetry-compliant. | Built into the platform — literally one config line. Traces exported to Sentry via OTLP. |
| **Operational data we own** | D1 tables (2 tables) | Webhook delivery tracking, health check snapshots | Only we know about webhook processing flows and health probe results — no external tool captures this |
What we do NOT build:
D1 error tables— Sentry is the error store. Nodiagnostic_errorstable.D1 RTK API call logs— Workers Observability auto-traces everyfetch()to RTK API with latency, status, headers. Nodiagnostic_rtk_api_callstable.Custom RTK API call wrapper for diagnostics— Workers Observability instrumentsfetch()automatically. Our RTK API client is a thin wrapper focused on auth/retries, not diagnostic logging.
3.2 Sentry Integration
Sentry is the backbone of error tracking. Two SDKs, two contexts, one Sentry project.
Packages & Versions
- Worker:
@sentry/cloudflare(v10.x, production-ready, official Sentry package) - SPA:
@sentry/react(mature, full-featured including Session Replay) - Replaces: toucan-js (deprecated;
@hono/sentryalso deprecated in favor of@sentry/cloudflare) - Requirement:
compatibility_flags = ["nodejs_compat"]in wrangler.toml (for AsyncLocalStorage)
DSN Ownership Model
Each deployment has up to two Sentry DSNs:
| DSN | Secret Name | Purpose | Default |
|---|---|---|---|
| **Vendor DSN** | `SENTRY_DSN_VENDOR` | Our (platform vendor) Sentry project. We see errors across ALL customer deployments. | Pre-configured by deployment app. Enabled by default. |
| **Customer DSN** | `SENTRY_DSN_CUSTOMER` | Customer's own Sentry project. They see only their own errors. | Empty. Customer adds via org settings. |
Behavior:
- If both DSNs are set → errors go to both (dual-client pattern)
- If customer disables vendor DSN → errors go only to customer's Sentry (or nowhere if they have no DSN)
- If neither is set → errors log to
console.erroronly (captured by Workers Observability logs) - Customer can toggle vendor DSN on/off from org settings page at any time
- Customer can add/change their own DSN from org settings page at any time
- DSNs are write-only (can only submit events, cannot read/modify/delete) — safe to embed
Sentry project structure (vendor side):
- ONE Sentry project for all customer deployments (not per-customer)
- Events tagged with
deployment_idandorg_idfor filtering - Use Sentry's search/filter to isolate per-deployment views
- Sentry Team plan ($26/mo base, ~$0.00015/event overage) — handles scale well
Worker-Side Init
import * as Sentry from "@sentry/cloudflare";
// The entire Worker handler is wrapped with Sentry's withSentry()
export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN_VENDOR,
environment: env.ENVIRONMENT || "production",
release: env.SENTRY_RELEASE, // git SHA, set at deploy time
tracesSampleRate: 0.1, // 10% of transactions
beforeSend(event) {
// Strip PII: redact email, IP, auth tokens
event = redactPII(event);
// Forward to customer DSN if configured
if (env.SENTRY_DSN_CUSTOMER && env.SENTRY_CUSTOMER_ENABLED !== "false") {
forwardToCustomerSentry(event, env.SENTRY_DSN_CUSTOMER);
}
// Check if vendor DSN is disabled by org
if (env.SENTRY_VENDOR_ENABLED === "false") return null; // drop event
return event;
},
}),
{
async fetch(request, env, ctx) {
// Sentry automatically captures unhandled exceptions,
// attaches request data, and traces fetch() sub-requests
return router.handle(request, env, ctx);
},
async scheduled(event, env, ctx) {
ctx.waitUntil(runHealthCheck(env));
ctx.waitUntil(cleanupOldDiagnostics(env));
},
}
);What withSentry() gives us automatically (zero additional code):
- Unhandled exception capture with full stack traces
- Request context (URL, method, headers) attached to every event
- Breadcrumbs for
console.log/warn/errorcalls - Distributed tracing across fetch sub-requests
SPA-Side Init
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: config.sentryDsnVendor, // fetched from Worker config endpoint
environment: config.environment,
release: config.release,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({ maskAllText: false, blockAllMedia: false }),
],
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0.1, // 10% of sessions
replaysOnErrorSampleRate: 1.0, // 100% of sessions with errors
beforeSend(event) {
event = redactPII(event);
if (config.sentryDsnCustomer) {
forwardToCustomerSentry(event, config.sentryDsnCustomer);
}
if (!config.sentryVendorEnabled) return null;
return event;
},
});Dual-DSN implementation (SPA): Use a custom transport wrapper that POSTs the event envelope to both DSN endpoints. This is simpler and more reliable than running two BrowserClient instances (which Sentry docs call "not recommended" due to integration conflicts).
function makeDualTransport(vendorDsn: string, customerDsn?: string) {
const vendorTransport = Sentry.makeFetchTransport({ dsn: vendorDsn });
const customerTransport = customerDsn
? Sentry.makeFetchTransport({ dsn: customerDsn })
: null;
return (options: Sentry.TransportOptions) => ({
send: async (envelope: Sentry.Envelope) => {
const results = await Promise.allSettled([
vendorTransport(options).send(envelope),
customerTransport ? customerTransport(options).send(envelope) : Promise.resolve(),
]);
return results[0].status === "fulfilled" ? results[0].value : results[1].value;
},
flush: (timeout?: number) => vendorTransport(options).flush(timeout),
});
}Sentry Tags (applied to every event)
| Tag | Value | Purpose |
|---|---|---|
| `deployment_id` | Unique per deployment | Filter vendor Sentry by deployment |
| `org_id` | Org identifier | Group errors by customer org |
| `component` | `worker`, `spa`, `rtk-sdk`, `webhook-handler` | Error source |
| `meeting_id` | RTK meeting ID (when in meeting context) | Correlate errors to specific meetings |
| `route` | e.g., `POST /api/meetings/:id/join` | Which API route errored |
Source Maps
# Deploy script uploads source maps to Sentry
SENTRY_RELEASE=$(sentry-cli releases propose-version)
wrangler deploy --var SENTRY_RELEASE:$SENTRY_RELEASE
sentry-cli sourcemaps upload --release=$SENTRY_RELEASE dist/- Requires
sentry-cli >= 2.17.0andwrangler >= 3.x - Worker source maps:
upload_source_maps = truein wrangler.toml - SPA source maps: Vite plugin or manual upload
What Gets Reported to Sentry
| Source | What | Automatic? |
|---|---|---|
| Worker unhandled exceptions | All crashes, binding errors, OOM | Yes (`withSentry` catches them) |
| Worker `fetch()` failures | RTK API 4xx/5xx, timeout, network errors | Yes (Workers Observability traces + Sentry captures thrown errors) |
| Webhook handler failures | Processing errors, invalid payloads | Yes (thrown errors within `withSentry`) |
| D1/KV/R2 failures | Query errors, constraint violations, permission errors | Yes (thrown errors) |
| SPA React error boundaries | Component crashes | Yes (`Sentry.ErrorBoundary` wraps RTK UI Kit components) |
| SPA RTK SDK errors | See section 3.5 below | Manual (event listeners → `Sentry.captureException`) |
| SPA network errors | Failed API calls to our Worker | Yes (`browserTracingIntegration` traces fetch) |
3.3 Cloudflare Workers Observability
Workers Observability provides auto-instrumentation of all Worker I/O with zero code changes. It captures what we would otherwise need custom wrapper code to log.
Configuration
# wrangler.toml
[observability]
enabled = true
[observability.logs]
head_sampling_rate = 1 # 1.0 = 100% of requests logged (adjust for cost)
[observability.traces]
enabled = true
head_sampling_rate = 0.01 # 1% of requests traced (adjust for cost vs visibility)That's it. No code changes. Note: logs and traces are separate opt-ins. This enables:
- Workers Logs — all
console.log/warn/errorcalls are collected, queryable in Cloudflare dashboard, searchable - Workers Traces — automatic OpenTelemetry-compliant traces for every
fetch(), KVget/put, R2get/put, D1prepare/run, and Durable Object calls. Shows latency, status, and call hierarchy per request. - Metrics Dashboard (Beta) — single view of all Worker metrics + logs
What this replaces
The old spec had a diagnostic_rtk_api_calls D1 table and a custom rtkApiCall() wrapper that manually logged every RTK API call's method, endpoint, duration, and status. Workers Observability does this automatically for every fetch() the Worker makes — including RTK API calls, webhook deliveries, and any external HTTP calls. With full OpenTelemetry trace context.
OTel Export to Sentry
Workers Observability can export traces to Sentry's OTLP endpoint, connecting Cloudflare's auto-instrumented traces with Sentry's error tracking:
# wrangler.toml (if using direct OTel export — alternative to SDK-based tracing)
[observability]
enabled = true
[observability.logs]
enabled = true
invocation_logs = true # log start/end of each invocation
# Export config is set via Cloudflare dashboard or API:
# Destination: Sentry OTLP endpoint
# Protocol: OTLP/HTTPNote: When using @sentry/cloudflare with withSentry(), Sentry already captures traces via the SDK. OTel export provides a complementary view in Sentry (infrastructure-level traces vs application-level). Both can coexist.
Pricing (customer pays — runs on their Cloudflare account)
| Feature | Free | Workers Paid |
|---|---|---|
| Logs | 200K events/day, 3-day retention | 20M/month, 7-day retention, $0.60/M overage |
| Traces | Free during beta (no published pricing yet) | Free during beta |
For most deployments, the included quotas are more than sufficient. The head_sampling_rate can be lowered to reduce volume.
Logpush (optional, for long-term retention)
Customers who want logs retained beyond 7 days can enable Logpush to push Worker logs to external storage:
Supported destinations: R2, S3, Google Cloud Storage, Azure Blob, Datadog, Elastic, Splunk, Sumo Logic, New Relic, any HTTP endpoint.
Config: logpush = true in wrangler.toml. Workers Paid plan only.
3.4 D1 Diagnostics Tables (2 tables)
We only store data in D1 that no external tool captures: webhook processing status and health check results.
`diagnostic_webhook_deliveries`
Sentry captures errors, but it doesn't know about webhook processing flows (received → processing → processed/failed). Only we track this.
CREATE TABLE diagnostic_webhook_deliveries (
id TEXT PRIMARY KEY, -- ulid (time-sortable)
org_id TEXT NOT NULL,
webhook_event_type TEXT NOT NULL, -- e.g., recording.statusUpdate, meeting.summary
rtk_event_id TEXT, -- event ID from RTK payload
meeting_id TEXT,
session_id TEXT,
status TEXT NOT NULL CHECK(status IN ('received','processing','processed','failed','retry_exhausted')),
processing_duration_ms INTEGER,
error_message TEXT, -- null if success
payload_summary TEXT, -- truncated/redacted payload for debugging
received_at TEXT NOT NULL DEFAULT (datetime('now')),
processed_at TEXT
);
CREATE INDEX idx_webhook_org_time ON diagnostic_webhook_deliveries(org_id, received_at DESC);
CREATE INDEX idx_webhook_status ON diagnostic_webhook_deliveries(org_id, status);
CREATE INDEX idx_webhook_event ON diagnostic_webhook_deliveries(org_id, webhook_event_type, received_at DESC);
CREATE INDEX idx_webhook_meeting ON diagnostic_webhook_deliveries(meeting_id, received_at DESC);`diagnostic_health_snapshots`
Periodic health probe results. No external tool runs these probes — we do.
CREATE TABLE diagnostic_health_snapshots (
id TEXT PRIMARY KEY, -- ulid
org_id TEXT NOT NULL,
check_type TEXT NOT NULL CHECK(check_type IN ('scheduled','manual','phone_home')),
status TEXT NOT NULL CHECK(status IN ('healthy','degraded','unhealthy')),
d1_latency_ms INTEGER,
kv_latency_ms INTEGER,
r2_latency_ms INTEGER,
rtk_api_latency_ms INTEGER,
rtk_api_reachable INTEGER NOT NULL DEFAULT 1,
active_meetings_count INTEGER NOT NULL DEFAULT 0,
active_participants_count INTEGER NOT NULL DEFAULT 0,
details TEXT, -- JSON: per-check details
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_health_org_time ON diagnostic_health_snapshots(org_id, created_at DESC);
CREATE INDEX idx_health_status ON diagnostic_health_snapshots(org_id, status);3.5 Client-Side RTK SDK Error Capture
The RTK SDK emits events that @sentry/react doesn't automatically capture (they're not unhandled exceptions — they're SDK-specific events). We listen for them and forward to Sentry.
RTK SDK Events to Capture
| Event | Source | What it means |
|---|---|---|
| `mediaPermissionError` | `meeting.self` | Camera/mic denied: `DENIED`, `SYS_DENIED`, `UNAVAILABLE`, `CANCELED`. **UNVERIFIED** — exact event name not confirmed in RTK docs, verify at build time |
| `mediaConnectionUpdate` | `meeting.meta` | WebRTC media connection state changed (connected/disconnected/failed) |
| `socketConnectionUpdate` | `meeting.meta` | Signaling WebSocket state changed. **UNVERIFIED** — verify exact event name at build time |
| `networkQualityScore` | `meeting.self` + remote participants | Per-participant network quality score. **UNVERIFIED** — verify exact event name at build time |
| Recording `ERRORED` state | `meeting.recording` | Recording failed (irrecoverable) |
| Screenshare errors | UI Kit language pack | Max screenshare limit reached, unknown error |
| Livestream errors | UI Kit language pack | Not supported, not found, sync error, start/stop failure |
Implementation Pattern
// In the React meeting component, after RTK meeting is initialized:
meeting.self.on("mediaPermissionError", (error) => {
Sentry.captureException(new Error(`Media permission: ${error.kind} - ${error.message}`), {
tags: { component: "rtk-sdk", error_type: "media_permission", meeting_id: meeting.meta.meetingId },
extra: { permission: error.mediaPermissions },
});
});
meeting.meta.on("mediaConnectionUpdate", ({ state }) => {
if (state === "failed") {
Sentry.captureException(new Error("WebRTC media connection failed"), {
tags: { component: "rtk-sdk", error_type: "media_connection", meeting_id: meeting.meta.meetingId },
});
}
// Non-error states logged as breadcrumbs for context
Sentry.addBreadcrumb({ category: "rtk", message: `Media connection: ${state}`, level: "info" });
});
meeting.self.on("networkQualityScore", (score) => {
// Not an error — logged as breadcrumb so it's available if an error occurs later
Sentry.addBreadcrumb({ category: "rtk", message: `Network quality: ${score}`, level: "info" });
});React Error Boundaries
Wrap RTK UI Kit components in Sentry error boundaries to catch component crashes:
<Sentry.ErrorBoundary fallback={<MeetingErrorFallback />} showDialog>
<RtkMeeting meeting={meeting} />
</Sentry.ErrorBoundary>RTK Debugger Components (built-in quality UI)
RTK provides 6 debugger components that display real-time quality metrics. We include these in the meeting UI behind a toggle — no custom code needed:
| Component | Shows |
|---|---|
| `rtk-debugger` | Parent container for all debug panels |
| `rtk-debugger-audio` | Audio bitrate, packet loss, jitter, CPU limitations |
| `rtk-debugger-video` | Video bitrate, packet loss, jitter, bandwidth limitations |
| `rtk-debugger-screenshare` | Screenshare network & media stats |
| `rtk-debugger-system` | Battery level, battery charging status |
| `rtk-debugger-toggle` | Button to show/hide the debugger panel |
These components display quality ratings (Good / Average / Poor) with detailed stats. We render them as-is — RTK handles the data collection and display.
3.6 Diagnostic Data Flows
Flow 1: Worker Error (automatic)
Request arrives → Sentry withSentry() wrapper
→ Handler executes
→ On unhandled error:
├── Sentry captures automatically (stack trace, request context, breadcrumbs)
├── Workers Observability logs the error + full fetch trace
└── Worker returns error response
→ No custom error logging code neededFlow 2: Webhook Processing (custom — we track the flow)
POST /webhooks/rtk arrives
→ Validate webhook signature
├── Invalid: Sentry.captureMessage("Invalid webhook signature"), return 401
└── Valid: continue
→ Parse event type from payload
→ INSERT INTO diagnostic_webhook_deliveries (status: 'processing')
→ Route to event handler
→ On success:
├── UPDATE webhook delivery (status: 'processed', processing_duration_ms)
└── Return 200
→ On failure:
├── Sentry.captureException(err, { tags: { webhook_event_type, meeting_id } })
├── UPDATE webhook delivery (status: 'failed', error_message)
└── Return 500 (RTK will retry)Flow 3: Health Check (custom — periodic probes)
Scheduled CRON trigger (every 5 minutes) OR GET /api/diagnostics/health
→ Run parallel health probes:
├── D1: SELECT 1 → measure latency
├── KV: get test key → measure latency
├── R2: head test object → measure latency
└── RTK API: GET /meetings?limit=1 → measure latency + confirm auth
→ Compute status:
├── healthy: all probes pass
├── degraded: any probe > 2s
└── unhealthy: any probe fails
→ INSERT INTO diagnostic_health_snapshots
→ If phone_home_enabled AND (degraded OR unhealthy):
├── POST to vendor phone-home endpoint
└── Log delivery result
→ Return health JSONFlow 4: Client-Side Errors (Sentry SDK handles it)
RTK SDK error event fires in browser
→ Our event listener calls Sentry.captureException() with tags
→ Sentry SDK sends to vendor DSN (and customer DSN if configured)
→ No Worker round-trip needed — Sentry SDK sends directly to Sentry
React component crash in meeting UI
→ Sentry.ErrorBoundary catches it
→ Sentry SDK sends crash report with component tree
→ Fallback UI shown to user3.7 Phone-Home Diagnostics
Optional vendor telemetry. Enabled by default (pre-configured by deployment app). Org can disable from settings.
When enabled (org_settings.phone_home_enabled = 1):
→ On every health check (scheduled or manual):
POST {PHONE_HOME_URL}/api/telemetry/health
Body: {
orgId (SHA-256 hashed),
status, // healthy | degraded | unhealthy
activeMeetings,
activeParticipants,
d1Latency, kvLatency, r2Latency, rtkApiLatency,
workerVersion,
sdkVersion,
timestamp
}
→ On FATAL Sentry event (via beforeSend hook):
POST {PHONE_HOME_URL}/api/telemetry/alert
Body: {
orgId (SHA-256 hashed),
severity: 'fatal',
errorMessage (truncated, no PII),
component,
timestamp
}Privacy guarantees:
- Org ID is SHA-256 hashed before transmission
- No user data, meeting content, chat messages, participant names, emails, or IPs ever transmitted
- Only aggregate counts and system health metrics
- All phone-home calls logged in
diagnostic_health_snapshotsfor transparency - Can be disabled at any time; disabling immediately stops all outbound telemetry
3.8 Diagnostics Routes
| Method | Route | Purpose | Auth |
|---|---|---|---|
| GET | `/api/diagnostics/health` | System health check (probe results) | Owner/Admin |
| GET | `/api/diagnostics/webhooks` | Webhook delivery status/failures (paginated) | Owner/Admin |
| GET | `/api/diagnostics/webhooks/:id` | Single webhook delivery detail | Owner/Admin |
| GET | `/api/diagnostics/active-meetings` | Currently active meetings | Owner/Admin |
| POST | `/api/diagnostics/phone-home` | Manual trigger for phone-home report | Owner only |
| PATCH | `/api/diagnostics/settings` | Update diagnostics settings (toggle vendor Sentry, phone-home, set customer DSN) | Owner only |
Removed routes (now handled by Sentry):
→ Use Sentry dashboardGET /api/diagnostics/errors→ Use Sentry dashboardGET /api/diagnostics/errors/:errorId→ Use Sentry dashboardGET /api/diagnostics/trends
3.9 Diagnostics Dashboard (Admin UI)
The SPA includes a /admin/diagnostics page (Owner/Admin role only):
| Panel | Data Source | Content |
|---|---|---|
| **System Health** | `diagnostic_health_snapshots` | Status badge (healthy/degraded/unhealthy), D1/KV/R2/RTK API latency gauges, last check time |
| **Webhook Deliveries** | `diagnostic_webhook_deliveries` | Recent deliveries with status badges, failure rate %, event types |
| **Active Meetings** | Worker API (KV + RTK) | Currently active meetings with participant count and duration |
| **Sentry Link** | External | "View Errors in Sentry" button linking to the Sentry project (filtered by deployment_id) |
| **Settings** | `org_settings` | Toggle vendor Sentry on/off, add/edit customer Sentry DSN, toggle phone-home |
3.10 Data Retention & Cleanup
Scheduled cleanup via Worker CRON:
| Table | Retention | Schedule |
|---|---|---|
| `diagnostic_webhook_deliveries` | 30 days | Daily at 03:00 UTC |
| `diagnostic_health_snapshots` | 90 days | Weekly |
Sentry retention is managed by the Sentry plan (90 days on Team plan). Workers Observability logs/traces retain for 7 days (Cloudflare managed). No cleanup code needed for either.
CRON trigger in wrangler config:
[triggers]
crons = ["0 3 * * *"] # Daily at 03:00 UTC3.11 Wrangler Configuration for Observability
# Observability — logs + traces, zero code
[observability]
enabled = true
[observability.logs]
head_sampling_rate = 1
invocation_logs = true
[observability.traces]
enabled = true
head_sampling_rate = 0.01Required secrets (set via wrangler secret put):
SENTRY_DSN_VENDOR # Vendor's Sentry DSN (set by deployment app)
SENTRY_DSN_CUSTOMER # Customer's Sentry DSN (optional, set via org settings)
SENTRY_RELEASE # Git SHA (set at deploy time)
PHONE_HOME_URL # Vendor telemetry endpoint (set by deployment app)3.12 FUTURE: Alerting
Alerting is deferred to a future iteration. For now:
- Sentry built-in alerts cover error spikes, new error types, and fatal errors (configurable in Sentry dashboard, zero code)
- Phone-home notifies the vendor on degraded/unhealthy health checks
- FUTURE: Org-facing alerting (email, webhook to Slack/Teams/PagerDuty) for health status changes, webhook failure spikes, and meeting quality degradation. Will be built as a separate notification system when the core platform is stable.
Sections 4+ (Frontend Architecture, Feature Implementation, Deployment, Testing) to be written by other team members.
Sections 4-6: User System, Role-to-Preset Mapping, Meeting Lifecycle
Section 4: User System & Authentication
4.1 User Model
The platform defines five roles in a strict hierarchy:
| Role | Level | Description |
|---|---|---|
| **Owner** | 5 | Organization creator. One per org. Cannot be demoted. Full control over billing, settings, and all users. |
| **Admin** | 4 | Organization-level administrator. Manages users, settings, presets. Cannot delete org or transfer ownership. |
| **Host** | 3 | Can create and manage meetings, admit waiting room participants, control stage, kick users. Default role for new members with meeting creation privileges. |
| **Member** | 2 | Standard participant. Can join meetings they're invited to or that are open. Cannot create meetings unless promoted. |
| **Guest** | 1 | Unauthenticated or externally-authenticated user. Joins via meeting link. Controlled by guest access policies. No org-level visibility. |
4.2 D1 Schema: Users & Organizations
-- Organizations table
CREATE TABLE organizations (
id TEXT PRIMARY KEY, -- nanoid
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE, -- URL-safe org identifier (e.g., "acme-corp")
rtk_app_id TEXT NOT NULL, -- RealtimeKit App ID for this org
owner_id TEXT NOT NULL, -- references users.id
settings_json TEXT DEFAULT '{}', -- org-wide settings (JSON blob)
guest_access_default TEXT NOT NULL DEFAULT 'link_only', -- 'disabled' | 'link_only' | 'open'
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Users table
CREATE TABLE users (
id TEXT PRIMARY KEY, -- nanoid
org_id TEXT NOT NULL REFERENCES organizations(id),
email TEXT NOT NULL,
name TEXT NOT NULL,
password_hash TEXT, -- null for SSO/guest users
role TEXT NOT NULL DEFAULT 'member', -- 'owner' | 'admin' | 'host' | 'member'
avatar_url TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
last_login_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(org_id, email)
);
-- Guest access tokens (for unauthenticated meeting access)
CREATE TABLE guest_tokens (
id TEXT PRIMARY KEY, -- nanoid
meeting_id TEXT NOT NULL REFERENCES meetings(id),
display_name TEXT NOT NULL,
email TEXT, -- optional, for follow-up
token_hash TEXT NOT NULL UNIQUE, -- hashed access token
rtk_participant_id TEXT, -- RTK participant ID once joined
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_users_org ON users(org_id);
CREATE INDEX idx_users_email ON users(org_id, email);
CREATE INDEX idx_guest_tokens_meeting ON guest_tokens(meeting_id);
CREATE INDEX idx_guest_tokens_hash ON guest_tokens(token_hash);4.3 Authentication Flow
This is a self-hosted Worker app — no third-party auth provider required. JWT-based auth with refresh tokens.
Registration / Org Creation Flow
1. POST /api/auth/register
Body: { email, password, name, orgName, orgSlug }
→ Create organization row (generate rtk_app_id via RTK REST API: POST /realtime/kit/apps)
→ Create user row with role='owner'
→ Create default presets in RTK for this app (video_host, video_participant, etc.)
→ Return { accessToken, refreshToken, user, org }Login Flow
1. POST /api/auth/login
Body: { email, password, orgSlug }
→ Lookup user by (org.slug → org.id, email)
→ Verify password_hash (argon2id)
→ Return { accessToken, refreshToken, user }
2. Access token: JWT, 15-minute expiry
Payload: { sub: user.id, org: org.id, role: user.role, iat, exp }
Signed with: HS256 using Worker secret (RTK_JWT_SECRET env var)
3. Refresh token: opaque token, 30-day expiry, stored hashed in D1
POST /api/auth/refresh → new access token + rotated refresh tokenGuest Join Flow
1. User visits meeting link: /m/{meetingSlug} or /{orgSlug}/m/{meetingSlug}
2. If not authenticated → show guest join form (display name, optional email)
3. POST /api/meetings/{meetingId}/guest-join
Body: { displayName, email? }
→ Check meeting exists and is ACTIVE
→ Check org guest_access_default and meeting-level guest_access override
→ If guest access disabled → 403
→ Create guest_tokens row
→ Create RTK participant via REST API (preset = guest preset for this meeting type)
→ Return { guestToken, rtk_authToken, meetingConfig }
4. Client SDK initializes with rtk_authToken
5. If waiting room enabled → guest enters waiting room; host admits/rejectsJWT Middleware (Worker)
// Every authenticated route runs through this middleware
async function authMiddleware(request: Request, env: Env): Promise<AuthContext> {
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) throw new HTTPError(401, 'Missing token');
const payload = await verifyJWT(token, env.RTK_JWT_SECRET);
const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?')
.bind(payload.sub).first();
if (!user || !user.is_active) throw new HTTPError(401, 'Invalid user');
return { userId: user.id, orgId: user.org_id, role: user.role };
}4.4 Refresh Token Storage
CREATE TABLE refresh_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
token_hash TEXT NOT NULL UNIQUE,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_refresh_tokens_user ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_hash ON refresh_tokens(token_hash);Token rotation: each refresh issues a new token and invalidates the old one. If a previously-rotated token is reused (replay detection), all tokens for that user are revoked.
4.5 Role-Based Access Control (RBAC)
Authorization checks happen at the Worker API layer, not in RTK:
| Action | Owner | Admin | Host | Member | Guest |
|---|---|---|---|---|---|
| Manage org settings | Yes | Yes | - | - | - |
| Manage users (invite, promote, deactivate) | Yes | Yes | - | - | - |
| Create meetings | Yes | Yes | Yes | - | - |
| Edit/delete any meeting | Yes | Yes | Own only | - | - |
| Start/stop recording | Yes | Yes | Yes (own) | - | - |
| Join meetings | Yes | Yes | Yes | Invited/Open | Link |
| Configure presets | Yes | Yes | - | - | - |
| View analytics | Yes | Yes | Yes (own) | - | - |
| Delete organization | Yes | - | - | - | - |
Section 5: Role-to-Preset Mapping
5.1 Core Principle
RTK presets are the enforcement layer for in-meeting permissions. Our platform roles (Owner, Admin, Host, Member, Guest) map to RTK presets at meeting join time. The Worker selects the correct preset when calling the RTK "Add Participant" API.
Platform Role + Meeting Type → RTK Preset Name → Participant Created with That Preset5.2 Preset Naming Convention
Format: {meeting_type}_{participant_role}
| Meeting Type | Host Preset | Participant Preset | Guest Preset | Viewer Preset |
|---|---|---|---|---|
| **Video** (group call) | `video_host` | `video_participant` | `video_guest` | — |
| **Audio-Only** | `audio_host` | `audio_participant` | `audio_guest` | — |
| **Webinar** | `webinar_host` | `webinar_participant` | `webinar_guest` | `webinar_viewer` |
| **Livestream** | `livestream_host` | `livestream_participant` | — | `livestream_viewer` |
- Owner and Admin always get the
*_hostpreset (maximum in-meeting control) - Host gets the
*_hostpreset for meetings they created;*_participantfor others (configurable) - Member gets the
*_participantpreset - Guest gets the
*_guestpreset (most restrictive with media access) or*_viewer(view-only)
5.3 Preset Resolution Logic (Worker)
function resolvePreset(
userRole: 'owner' | 'admin' | 'host' | 'member' | 'guest',
meetingType: 'video' | 'audio' | 'webinar' | 'livestream',
isCreator: boolean
): string {
// Owner and Admin always get host preset
if (userRole === 'owner' || userRole === 'admin') {
return `${meetingType}_host`;
}
// Host gets host preset for own meetings, participant for others
if (userRole === 'host') {
return isCreator ? `${meetingType}_host` : `${meetingType}_participant`;
}
// Guest gets guest or viewer preset
if (userRole === 'guest') {
if (meetingType === 'webinar' || meetingType === 'livestream') {
return `${meetingType}_viewer`;
}
return `${meetingType}_guest`;
}
// Member gets participant preset
return `${meetingType}_participant`;
}5.4 Preset Configurations by Meeting Type
Each preset is created via the RTK REST API (POST /accounts/{ACCOUNT_ID}/realtime/kit/{APP_ID}/presets) when the org is provisioned. Below are the exact configurations using the verified RTK OpenAPI schema.
Schema note: The preset API uses snake_case throughout. Top-level required fields:
name,config,ui. Thepermissionsobject is optional but all its sub-fields are required when provided. Media permissions use"ALLOWED"/"NOT_ALLOWED"/"CAN_REQUEST"enums (not booleans). Theconfig.view_typehas three valid values:GROUP_CALL,WEBINAR,AUDIO_ROOM. Livestream presets useWEBINARview_type withcan_livestream: true.
5.4.1 Video (Group Call) Presets
video_host — Full meeting control
{
"name": "video_host",
"config": {
"view_type": "GROUP_CALL",
"max_video_streams": { "desktop": 25, "mobile": 6 },
"max_screenshare_count": 1,
"media": {
"video": { "quality": "hd", "frame_rate": 30 },
"screenshare": { "quality": "hd", "frame_rate": 15 }
}
},
"permissions": {
"media": {
"audio": { "can_produce": "ALLOWED" },
"video": { "can_produce": "ALLOWED" },
"screenshare": { "can_produce": "ALLOWED" }
},
"kick_participant": true,
"pin_participant": true,
"can_spotlight": true,
"disable_participant_audio": true,
"disable_participant_video": true,
"disable_participant_screensharing": true,
"accept_waiting_requests": true,
"can_accept_production_requests": true,
"can_change_participant_permissions": true,
"can_record": true,
"can_livestream": false,
"can_edit_display_name": true,
"show_participant_list": true,
"hidden_participant": false,
"waiting_room_type": "ON_PRIVILEGED_USER_ENTRY",
"recorder_type": "NONE",
"is_recorder": false,
"chat": {
"public": { "can_send": true, "text": true, "files": true },
"private": { "can_send": true, "can_receive": true, "text": true, "files": true }
},
"polls": { "can_create": true, "can_vote": true, "can_view": true },
"plugins": { "can_start": true, "can_close": true, "can_edit_config": true, "config": { "access_control": "FULL_ACCESS", "handles_view_only": false } },
"connected_meetings": { "can_alter_connected_meetings": true, "can_switch_connected_meetings": true, "can_switch_to_parent_meeting": true }
},
"ui": {
"design_tokens": {
"theme": "dark",
"border_radius": "rounded",
"border_width": "thin",
"spacing_base": 4,
"logo": "",
"colors": {
"brand": { "300": "#844d1c", "400": "#9d5b22", "500": "#b56927", "600": "#d37c30", "700": "#d9904f" },
"background": { "600": "#222222", "700": "#1f1f1f", "800": "#1b1b1b", "900": "#181818", "1000": "#141414" },
"text": "#EEEEEE", "text_on_brand": "#EEEEEE",
"danger": "#FF2D2D", "success": "#62A504", "warning": "#FFCD07", "video_bg": "#191919"
}
}
}
}video_participant — Standard participant (abbreviated: only differences from host shown)
{
"name": "video_participant",
"config": { "view_type": "GROUP_CALL", "...": "same as video_host" },
"permissions": {
"media": {
"audio": { "can_produce": "ALLOWED" },
"video": { "can_produce": "ALLOWED" },
"screenshare": { "can_produce": "ALLOWED" }
},
"kick_participant": false,
"pin_participant": true,
"can_spotlight": false,
"disable_participant_audio": false,
"disable_participant_video": false,
"disable_participant_screensharing": false,
"accept_waiting_requests": false,
"can_accept_production_requests": false,
"can_change_participant_permissions": false,
"can_record": false,
"can_livestream": false,
"can_edit_display_name": false,
"show_participant_list": true,
"hidden_participant": false,
"waiting_room_type": "ON_PRIVILEGED_USER_ENTRY",
"recorder_type": "NONE",
"is_recorder": false,
"chat": {
"public": { "can_send": true, "text": true, "files": true },
"private": { "can_send": true, "can_receive": true, "text": true, "files": true }
},
"polls": { "can_create": false, "can_vote": true, "can_view": true },
"plugins": { "can_start": false, "can_close": false, "can_edit_config": false, "config": { "access_control": "VIEW_ONLY", "handles_view_only": true } },
"connected_meetings": { "can_alter_connected_meetings": false, "can_switch_connected_meetings": true, "can_switch_to_parent_meeting": true }
},
"ui": { "design_tokens": "...same as video_host..." }
}video_guest — Restricted participant (no screenshare, no private chat)
{
"name": "video_guest",
"config": { "view_type": "GROUP_CALL", "...": "same as video_host" },
"permissions": {
"media": {
"audio": { "can_produce": "ALLOWED" },
"video": { "can_produce": "ALLOWED" },
"screenshare": { "can_produce": "NOT_ALLOWED" }
},
"kick_participant": false,
"pin_participant": false,
"can_spotlight": false,
"disable_participant_audio": false,
"disable_participant_video": false,
"disable_participant_screensharing": false,
"accept_waiting_requests": false,
"can_accept_production_requests": false,
"can_change_participant_permissions": false,
"can_record": false,
"can_livestream": false,
"can_edit_display_name": false,
"show_participant_list": true,
"hidden_participant": false,
"waiting_room_type": "ON_PRIVILEGED_USER_ENTRY",
"recorder_type": "NONE",
"is_recorder": false,
"chat": {
"public": { "can_send": true, "text": true, "files": false },
"private": { "can_send": false, "can_receive": true, "text": false, "files": false }
},
"polls": { "can_create": false, "can_vote": true, "can_view": true },
"plugins": { "can_start": false, "can_close": false, "can_edit_config": false, "config": { "access_control": "VIEW_ONLY", "handles_view_only": true } },
"connected_meetings": { "can_alter_connected_meetings": false, "can_switch_connected_meetings": false, "can_switch_to_parent_meeting": false }
},
"ui": { "design_tokens": "...same as video_host..." }
}5.4.2 Audio-Only Presets
Same structure as Video presets but with config.view_type: "AUDIO_ROOM" and video can_produce set to "NOT_ALLOWED":
| Preset | `view_type` | Audio | Video | Screenshare | Host Controls |
|---|---|---|---|---|---|
| `audio_host` | `AUDIO_ROOM` | `ALLOWED` | `NOT_ALLOWED` | `ALLOWED` | All (`kick_participant`, `disable_*`, etc.) |
| `audio_participant` | `AUDIO_ROOM` | `ALLOWED` | `NOT_ALLOWED` | `NOT_ALLOWED` | None |
| `audio_guest` | `AUDIO_ROOM` | `ALLOWED` | `NOT_ALLOWED` | `NOT_ALLOWED` | None |
Chat, polls, plugins, and other permissions mirror the video equivalents at the same role level.
5.4.3 Webinar Presets
Webinars use view_type: "WEBINAR" with stage management. Only hosts and invited speakers are on stage; everyone else is a viewer. Stage management is handled by can_accept_production_requests (host accepts stage requests) and media CAN_REQUEST (participant requests to go on stage).
webinar_host — On stage, full control
{
"name": "webinar_host",
"config": {
"view_type": "WEBINAR",
"max_video_streams": { "desktop": 25, "mobile": 6 },
"max_screenshare_count": 1,
"media": {
"video": { "quality": "hd", "frame_rate": 30 },
"screenshare": { "quality": "hd", "frame_rate": 15 }
}
},
"permissions": {
"media": {
"audio": { "can_produce": "ALLOWED" },
"video": { "can_produce": "ALLOWED" },
"screenshare": { "can_produce": "ALLOWED" }
},
"kick_participant": true,
"pin_participant": true,
"can_spotlight": true,
"disable_participant_audio": true,
"disable_participant_video": true,
"disable_participant_screensharing": true,
"accept_waiting_requests": true,
"can_accept_production_requests": true,
"can_change_participant_permissions": true,
"can_record": true,
"can_livestream": true,
"can_edit_display_name": true,
"show_participant_list": true,
"hidden_participant": false,
"waiting_room_type": "ON_PRIVILEGED_USER_ENTRY",
"recorder_type": "NONE",
"is_recorder": false,
"chat": {
"public": { "can_send": true, "text": true, "files": true },
"private": { "can_send": true, "can_receive": true, "text": true, "files": true }
},
"polls": { "can_create": true, "can_vote": true, "can_view": true },
"plugins": { "can_start": true, "can_close": true, "can_edit_config": true, "config": { "access_control": "FULL_ACCESS", "handles_view_only": false } },
"connected_meetings": { "can_alter_connected_meetings": true, "can_switch_connected_meetings": true, "can_switch_to_parent_meeting": true }
},
"ui": { "design_tokens": "...same as video_host..." }
}webinar_participant — Can request stage access
{
"name": "webinar_participant",
"config": { "view_type": "WEBINAR", "...": "same as webinar_host" },
"permissions": {
"media": {
"audio": { "can_produce": "CAN_REQUEST" },
"video": { "can_produce": "CAN_REQUEST" },
"screenshare": { "can_produce": "NOT_ALLOWED" }
},
"kick_participant": false,
"accept_waiting_requests": false,
"can_accept_production_requests": false,
"can_record": false,
"can_livestream": false,
"show_participant_list": true,
"waiting_room_type": "ON_PRIVILEGED_USER_ENTRY",
"chat": {
"public": { "can_send": true, "text": true, "files": false },
"private": { "can_send": false, "can_receive": true, "text": false, "files": false }
},
"polls": { "can_create": false, "can_vote": true, "can_view": true },
"...": "remaining fields same pattern as video_participant"
},
"ui": { "design_tokens": "...same as video_host..." }
}webinar_viewer — View-only (guests and livestream viewers)
{
"name": "webinar_viewer",
"config": { "view_type": "WEBINAR", "...": "same as webinar_host" },
"permissions": {
"media": {
"audio": { "can_produce": "NOT_ALLOWED" },
"video": { "can_produce": "NOT_ALLOWED" },
"screenshare": { "can_produce": "NOT_ALLOWED" }
},
"kick_participant": false,
"accept_waiting_requests": false,
"can_accept_production_requests": false,
"can_record": false,
"can_livestream": false,
"show_participant_list": false,
"hidden_participant": false,
"waiting_room_type": "SKIP",
"chat": {
"public": { "can_send": false, "text": false, "files": false },
"private": { "can_send": false, "can_receive": false, "text": false, "files": false }
},
"polls": { "can_create": false, "can_vote": true, "can_view": true },
"...": "remaining fields all false/NONE"
},
"ui": { "design_tokens": "...same as video_host..." }
}5.4.4 Livestream Presets
Livestreaming uses view_type: "WEBINAR" (there is no LIVESTREAM view_type in the API) with can_livestream: true for hosts. RTMP/HLS export is controlled via the separate livestreaming API, not the preset itself.
| Preset | `view_type` | `can_livestream` | Audio | Video | Screenshare | Host Controls |
|---|---|---|---|---|---|---|
| `livestream_host` | `WEBINAR` | `true` | `ALLOWED` | `ALLOWED` | `ALLOWED` | All |
| `livestream_participant` | `WEBINAR` | `false` | `CAN_REQUEST` | `CAN_REQUEST` | `NOT_ALLOWED` | None |
| `livestream_viewer` | `WEBINAR` | `false` | `NOT_ALLOWED` | `NOT_ALLOWED` | `NOT_ALLOWED` | None |
5.5 Dynamic Preset Override
Hosts can temporarily promote/demote participants during a meeting. This is handled via the RTK REST API:
PATCH /realtime/kit/{APP_ID}/meetings/{MEETING_ID}/participants/{PARTICIPANT_ID}
Body: { "preset_name": "video_host" }Our Worker wraps this with RBAC checks — only users with host-level permissions (Owner, Admin, Host of meeting) can call this endpoint. The promotion is tracked in an audit log:
CREATE TABLE participant_events (
id TEXT PRIMARY KEY,
meeting_id TEXT NOT NULL REFERENCES meetings(id),
user_id TEXT, -- null for guests
guest_token_id TEXT, -- null for authenticated users
event_type TEXT NOT NULL, -- 'joined' | 'left' | 'promoted' | 'demoted' | 'kicked' | 'admitted'
preset_from TEXT,
preset_to TEXT,
performed_by TEXT, -- user_id of the person who did it
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_participant_events_meeting ON participant_events(meeting_id);Section 6: Meeting Lifecycle
6.1 Meeting Types
Four meeting types, each mapping to an RTK meetingType in the preset:
| Type | RTK meetingType | Key Characteristics |
|---|---|---|
| **Video** | `video` | Full audio/video, all participants equal, screenshare allowed |
| **Audio-Only** | `voice` | Microphone only, no camera, lower bandwidth |
| **Webinar** | `webinar` | Stage-based, hosts present, audience watches, Q&A via chat |
| **Livestream** | `webinar` + RTMP | Stage-based + RTMP export to external platforms + HLS playback |
6.2 D1 Schema: Meetings & Scheduling
-- Meetings table
CREATE TABLE meetings (
id TEXT PRIMARY KEY, -- nanoid
org_id TEXT NOT NULL REFERENCES organizations(id),
rtk_meeting_id TEXT, -- RTK meeting ID (set after RTK API call)
title TEXT NOT NULL,
slug TEXT NOT NULL, -- URL-friendly meeting identifier
description TEXT,
meeting_type TEXT NOT NULL DEFAULT 'video', -- 'video' | 'audio' | 'webinar' | 'livestream'
scheduling_type TEXT NOT NULL DEFAULT 'instant', -- 'instant' | 'scheduled' | 'recurring' | 'permanent'
status TEXT NOT NULL DEFAULT 'draft', -- 'draft' | 'scheduled' | 'active' | 'ended' | 'cancelled'
created_by TEXT NOT NULL REFERENCES users(id),
-- Scheduling fields (null for instant meetings)
scheduled_start TEXT, -- ISO 8601 datetime
scheduled_end TEXT, -- ISO 8601 datetime
actual_start TEXT, -- set when first participant joins
actual_end TEXT, -- set when last participant leaves
timezone TEXT DEFAULT 'UTC',
-- Permanent room fields
is_permanent INTEGER NOT NULL DEFAULT 0,
vanity_slug TEXT UNIQUE, -- e.g., "standup" → /acme-corp/m/standup
-- Guest access (overrides org default)
guest_access TEXT, -- null = inherit org default | 'disabled' | 'link_only' | 'open'
-- Meeting configuration
waiting_room_enabled INTEGER NOT NULL DEFAULT 1,
recording_auto_start INTEGER NOT NULL DEFAULT 0,
transcription_enabled INTEGER NOT NULL DEFAULT 1,
max_participants INTEGER DEFAULT 100,
-- Cloud storage override (null = use org default R2)
cloud_storage_config TEXT, -- JSON: { provider, bucket, region, credentials_ref }
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(org_id, slug)
);
-- Recurring meeting patterns
CREATE TABLE recurring_patterns (
id TEXT PRIMARY KEY,
meeting_id TEXT NOT NULL REFERENCES meetings(id) ON DELETE CASCADE,
rrule TEXT NOT NULL, -- iCal RRULE string (e.g., "FREQ=WEEKLY;BYDAY=MO,WE,FR")
dtstart TEXT NOT NULL, -- pattern start date (ISO 8601)
until_date TEXT, -- pattern end date (null = no end)
occurrence_count INTEGER, -- alternative to until_date
exceptions TEXT DEFAULT '[]', -- JSON array of excluded dates
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Individual occurrences of recurring meetings (materialized)
CREATE TABLE meeting_occurrences (
id TEXT PRIMARY KEY,
meeting_id TEXT NOT NULL REFERENCES meetings(id) ON DELETE CASCADE,
pattern_id TEXT NOT NULL REFERENCES recurring_patterns(id) ON DELETE CASCADE,
occurrence_date TEXT NOT NULL, -- the specific date/time of this occurrence
status TEXT NOT NULL DEFAULT 'scheduled', -- 'scheduled' | 'active' | 'ended' | 'cancelled'
rtk_meeting_id TEXT, -- each occurrence gets its own RTK meeting
actual_start TEXT,
actual_end TEXT,
override_json TEXT, -- per-occurrence overrides (title, description, etc.)
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(meeting_id, occurrence_date)
);
-- Meeting invites
CREATE TABLE meeting_invites (
id TEXT PRIMARY KEY,
meeting_id TEXT NOT NULL REFERENCES meetings(id) ON DELETE CASCADE,
user_id TEXT REFERENCES users(id),
email TEXT, -- for inviting non-members
role_override TEXT, -- override the user's default preset mapping
rsvp_status TEXT DEFAULT 'pending', -- 'pending' | 'accepted' | 'declined' | 'tentative'
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(meeting_id, user_id)
);
CREATE INDEX idx_meetings_org ON meetings(org_id);
CREATE INDEX idx_meetings_status ON meetings(org_id, status);
CREATE INDEX idx_meetings_vanity ON meetings(vanity_slug);
CREATE INDEX idx_meetings_scheduled ON meetings(org_id, scheduling_type, scheduled_start);
CREATE INDEX idx_occurrences_meeting ON meeting_occurrences(meeting_id, occurrence_date);
CREATE INDEX idx_occurrences_date ON meeting_occurrences(occurrence_date, status);
CREATE INDEX idx_invites_user ON meeting_invites(user_id);
CREATE INDEX idx_invites_meeting ON meeting_invites(meeting_id);6.3 Meeting Lifecycle State Machine
create
│
▼
┌──────────────[DRAFT]──────────────┐
│ │ │
│ schedule │ start now │ cancel
│ │ │
▼ │ ▼
[SCHEDULED] │ [CANCELLED]
│ │
│ activate │
│ (auto or manual)│
▼ ▼
[ACTIVE] ◄────────────
│
│ last participant leaves
│ OR host ends meeting
▼
[ENDED]State transitions and RTK API calls:
| Transition | Trigger | RTK API Call |
|---|---|---|
| `draft → scheduled` | Host sets date/time | None (RTK meeting not created yet) |
| `draft → active` | Host clicks "Start Now" | `POST /meetings` (create RTK meeting) then `PATCH /meetings/{id}` (set status=ACTIVE) |
| `scheduled → active` | Cron trigger at scheduled time OR host starts early | `POST /meetings` (create RTK meeting) then `PATCH /meetings/{id}` (set ACTIVE) |
| `active → ended` | Last participant leaves OR host clicks "End" | `PATCH /meetings/{id}` (set INACTIVE), kick all participants |
| `any → cancelled` | Host/Admin cancels | If RTK meeting exists: `PATCH /meetings/{id}` (set INACTIVE) |
6.4 Meeting Creation Flow
Instant Meeting
1. POST /api/meetings
Body: { title, meetingType: "video", schedulingType: "instant" }
2. Worker validates auth (must be Owner/Admin/Host)
3. Generate meeting slug (nanoid, 10 chars, URL-safe)
4. Create RTK meeting:
POST /realtime/kit/{APP_ID}/meetings
Body: { title, status: "ACTIVE" }
→ Returns rtk_meeting_id
5. Insert meeting row in D1 (status = 'active', rtk_meeting_id set)
6. Return { meetingId, slug, joinUrl: "/{orgSlug}/m/{slug}" }Scheduled Meeting
1. POST /api/meetings
Body: { title, meetingType, schedulingType: "scheduled", scheduledStart, scheduledEnd, timezone }
2. Worker validates auth + time is in the future
3. Insert meeting row in D1 (status = 'scheduled', rtk_meeting_id = null)
4. RTK meeting is NOT created yet (saves resources, avoids stale meetings)
5. When scheduled time arrives:
- Cron trigger (Workers Cron) queries meetings WHERE status='scheduled' AND scheduled_start <= now
- Creates RTK meeting via API
- Updates D1 row: status='active', rtk_meeting_id set
- Optionally sends notification (webhook/email) to inviteesRecurring Meeting
1. POST /api/meetings
Body: { title, meetingType, schedulingType: "recurring", rrule: "FREQ=WEEKLY;BYDAY=MO,WE,FR", dtstart, timezone }
2. Create meeting row (template) in D1
3. Create recurring_patterns row with RRULE
4. Materialize next N occurrences (default: 4 weeks ahead) into meeting_occurrences table
5. Each occurrence is activated independently via the same cron mechanism
6. Cron also materializes additional future occurrences on a rolling basis
7. Each occurrence gets its own RTK meeting ID (created at activation time)Permanent Room
1. POST /api/meetings
Body: { title, meetingType, schedulingType: "permanent", vanitySlug: "standup" }
2. Validate vanity slug uniqueness within org
3. Create meeting row with is_permanent=1, vanity_slug set
4. Create RTK meeting immediately (persistent room):
POST /realtime/kit/{APP_ID}/meetings
Body: { title, status: "INACTIVE" }
5. Room URL: /{orgSlug}/m/{vanitySlug} (e.g., /acme-corp/m/standup)
6. Room is always available but INACTIVE until someone joins:
- On join request → PATCH RTK meeting to ACTIVE
- On last leave → PATCH RTK meeting to INACTIVE
- RTK meeting ID is reused across sessions (never deleted)
7. Permanent rooms never expire. Owner/Admin can deactivate (soft delete) by setting meeting status to 'cancelled'.6.5 Joining a Meeting
1. Client navigates to /{orgSlug}/m/{meetingSlug}
2. POST /api/meetings/{meetingId}/join
Headers: Authorization: Bearer <jwt> (or guest token)
3. Worker resolves:
a. User identity (authenticated user or guest)
b. User's platform role
c. Meeting type
d. Whether user is meeting creator
e. Preset name = resolvePreset(role, meetingType, isCreator)
4. Create RTK participant:
POST /realtime/kit/{APP_ID}/meetings/{RTK_MEETING_ID}/participants
Body: {
name: user.name,
preset_name: resolvedPreset,
custom_participant_id: user.id || crypto.randomUUID() // REQUIRED — links RTK participant to our user (UUID for guests)
}
→ Returns { id: rtk_participant_id, authToken: rtk_auth_token }
5. Log participant_events row (event_type = 'joined')
6. Return to client:
{
rtkAuthToken, // for SDK initialization
rtkMeetingId, // for SDK meeting join
preset_name, // for client-side UI hints
meetingConfig: { // for client-side rendering
title, type, waitingRoomEnabled, transcriptionEnabled, ...
}
}
7. Client initializes RTK SDK:
const meeting = await RTKClient.init({ authToken: rtkAuthToken });
await meeting.join();6.6 Ending a Meeting
Two triggers:
Host ends meeting explicitly:
POST /api/meetings/{meetingId}/end
1. Validate caller is Owner/Admin/Host-of-meeting
2. RTK: kick all participants
POST /realtime/kit/{APP_ID}/meetings/{RTK_MEETING_ID}/active-session/kick-all
3. RTK: set meeting inactive
PATCH /realtime/kit/{APP_ID}/meetings/{RTK_MEETING_ID}
Body: { status: "INACTIVE" }
4. D1: update meeting status = 'ended', actual_end = now
5. Trigger post-meeting processing (recording finalization, transcript, summary)Last participant leaves (webhook-driven):
1. RTK fires `meeting.ended` webhook when last participant leaves (after `session_keep_alive_time_in_secs` elapses — default 60s, configurable 60-600s per meeting)
2. Worker webhook handler:
- Look up meeting by rtk_meeting_id
- If scheduling_type != 'permanent':
Set meeting status = 'ended', actual_end = now
- If scheduling_type == 'permanent':
Set RTK meeting to INACTIVE (keep D1 status as 'active' for rejoining)
- Trigger post-meeting processing6.7 Guest Access Configuration Hierarchy
Guest access is controlled at two levels, with meeting-level overriding org-level:
Effective guest access = meeting.guest_access ?? org.guest_access_default| Setting | Behavior |
|---|---|
| `disabled` | No guest access. Only authenticated org members can join. |
| `link_only` | Guests can join via meeting link. Must provide display name. Waiting room recommended. |
| `open` | Guests can join via link with minimal friction. Waiting room still applies if enabled. |
Per-meeting override: When creating/editing a meeting, the Host can set guest_access to override the org default for that specific meeting. Null means "inherit org default."
6.8 Abuse Protection Mechanisms
Rate Limiting (Cloudflare Workers)
// Rate limit configuration per endpoint category
const RATE_LIMITS = {
'auth.login': { requests: 5, window: 60 }, // 5 login attempts per minute
'auth.register': { requests: 3, window: 3600 }, // 3 registrations per hour (per IP)
'meeting.create': { requests: 10, window: 60 }, // 10 meetings per minute (per org)
'meeting.join': { requests: 30, window: 60 }, // 30 joins per minute (per meeting)
'guest.join': { requests: 20, window: 60 }, // 20 guest joins per minute (per meeting)
'chat.send': { requests: 30, window: 60 }, // 30 messages per minute (per user)
'api.general': { requests: 100, window: 60 }, // 100 requests per minute (per user)
};Rate limiting is enforced using a KV-based sliding window counter (org_id or IP as key).
Capacity Caps
| Resource | Default Limit | Configurable |
|---|---|---|
| Max participants per meeting | 100 | Yes (per meeting) |
| Max concurrent meetings per org | 25 | Yes (org setting) |
| Max guests per meeting | 50% of max_participants | Yes (per meeting) |
| Max meeting duration | 24 hours | Yes (org setting) |
| Max recording storage (R2) | 50 GB per org | Yes (org setting) |
When a cap is reached, the Worker returns HTTP 429 with a clear error message. RTK's own limits (if any) are respected as the floor.
Guest Abuse Controls
- Waiting room enforcement: Guests always enter waiting room when
guest_access = 'link_only'(even if meeting-level waiting room is off for members) - Display name validation: Strip HTML/script tags, enforce 2-50 char length, block known abuse patterns
- Token expiry: Guest tokens expire after 4 hours (configurable)
- IP-based join throttle: Max 3 guest joins from the same IP within 5 minutes per meeting
- Host can lock meeting: Once started, host can toggle
acceptNewParticipants = falsevia RTK API to prevent new joins - Auto-kick idle guests: If a guest is in waiting room for >10 minutes without being admitted, their token is revoked
Webhook Verification
All incoming RTK webhooks are verified:
- Check signature header against shared secret — UNVERIFIED: exact header name (
X-RTK-Signature?) and verification mechanism not confirmed in RTK docs. Verify at build time; if no signature mechanism exists, validate by cross-referencing event data with REST API. - Check timestamp — reject if >5 minutes old (replay protection). UNVERIFIED: exact header name (
X-RTK-Timestamp?) not confirmed. - Idempotency: store webhook event IDs in KV with 24-hour TTL to deduplicate
6.9 Post-Meeting Processing Pipeline
After a meeting ends (either explicitly or via last-leave webhook):
Meeting Ends
│
├─→ Recording: Poll RTK recording status via REST API until 'completed'
│ └─→ Store recording metadata in D1 (duration, size, R2 path)
│
├─→ Transcript: Wait for meeting.transcript webhook OR poll REST API
│ └─→ Store transcript in D1 (full text + timestamped segments)
│
├─→ AI Summary: Wait for meeting.summary webhook
│ └─→ Store summary in D1
│
├─→ Chat Export: Wait for meeting.chatSynced webhook
│ └─→ Store chat log in D1
│
└─→ Analytics: Aggregate session data
└─→ Update analytics tables (session duration, participant count, peak concurrent, etc.)All post-meeting data is linked to the meeting row and accessible via:
GET /api/meetings/{meetingId}/recordingGET /api/meetings/{meetingId}/transcriptGET /api/meetings/{meetingId}/summaryGET /api/meetings/{meetingId}/chat-export
Design Document: Sections 7-9
Section 7: RTK Feature Mapping (Exhaustive)
Every RealtimeKit feature mapped to its app surface, API, and implementation status.
Legend:
- Config Level:
Preset= controlled via preset permissions,Meeting= meeting-level config,App= app-wide config,Client= SDK-side only,Server= REST API / Worker - Status:
Build= in scope now,Research= needs investigation,Future= planned later,Low Priority= groundwork only
7.1 Core Meeting Infrastructure
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Meeting creation | `POST /meetings` REST API | Create Meeting page, Schedule modal | Server | Build |
| Meeting ACTIVE/INACTIVE toggle | `PATCH /meetings/{id}` REST API | Meeting list admin actions | Server | Build |
| Meeting metadata | `meeting.meta` (client SDK) | Meeting header, lobby screen | Meeting | Build |
| Meeting title display | `rtk-meeting-title` component | In-meeting header bar | Meeting | Build |
| Meeting duration clock | `rtk-clock` component | In-meeting header bar | Client | Build |
| Session lifecycle (auto-create on join, end on last leave) | Managed by RTK platform | Backend webhook handler | Server | Build |
| Participant creation | `POST /meetings/{id}/participants` REST API | Join flow (Worker creates participant, returns authToken) | Server | Build |
| Participant token refresh | `POST /participants/{id}/token` REST API | Auto-refresh middleware in Worker | Server | Build |
| Preset creation & management | `POST /presets`, `PATCH /presets/{id}` REST API | Org Settings > Presets page | Server + App | Build |
| Meeting type: Video (Group Call) | Preset `view_type: "GROUP_CALL"` | Meeting creation form, preset config | Preset | Build |
| Meeting type: Voice (Audio Only) | Preset `view_type: "AUDIO_ROOM"` | Meeting creation form, preset config | Preset | Build |
| Meeting type: Webinar | Preset `view_type: "WEBINAR"` | Meeting creation form, preset config | Preset | Build |
| `record_on_start` config | Meeting creation parameter | Create Meeting form toggle | Meeting | Build |
| `persist_chat` config | Meeting creation parameter | Create Meeting form toggle | Meeting | Build |
| `ai_config` (transcription + summarization) | Meeting creation parameter | Create Meeting form — AI section | Meeting | Build |
| `summarize_on_end` config | Meeting creation parameter | Create Meeting form toggle | Meeting | Build |
7.2 Participant & Room Management
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Participant list | `rtk-participants` component | Sidebar > Participants tab | Preset | Build |
| Participant count badge | `rtk-participant-count` component | Control bar, header | Client | Build |
| Participant tile (video) | `rtk-participant-tile` component | Main grid area | Client | Build |
| Participant tile (audio-only) | `rtk-audio-tile` component | Audio grid layout | Client | Build |
| Participant name tag | `rtk-name-tag` component | Overlay on each tile | Client | Build |
| Participant avatar | `rtk-avatar` component | Audio tiles, participant list | Client | Build |
| Participant setup/preview | `rtk-participant-setup` component | Pre-join screen | Client | Build |
| Virtualized participant list | `rtk-virtualized-participant-list` | Large meeting sidebar | Client | Build |
| Viewer list (webinar) | `rtk-participants-viewer-list` component | Webinar sidebar tab | Client | Build |
| Viewer count (livestream) | `rtk-viewer-count` component | Livestream header | Client | Build |
| Kick participant | Host control via SDK | Participant context menu | Preset | Build |
| Mute participant audio/video | Host control via SDK | Participant context menu | Preset | Build |
| Mute all participants | `rtk-mute-all-button` + `rtk-mute-all-confirmation` | Control bar (host only) | Preset | Build |
| Edit participant name | `meeting.self` permission | Participant settings | Preset | Build |
| User ID (persistent across sessions) | `meeting.self.userId` / `participant.userId` | Analytics, user tracking | Client | Build |
| Session ID (per-connection) | `meeting.self.id` / `participant.id` | Session-level tracking | Client | Build |
7.3 Waiting Room
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Waiting room enable/disable | Preset configuration | Preset editor > Waiting Room section | Preset | Build |
| Waiting room screen | `rtk-waiting-screen` component | Shown to waitlisted participants | Client | Build |
| Waiting room participant list | `rtk-participants-waiting-list` component | Host sidebar > Waiting tab | Preset | Build |
| Admit participant | Host SDK method | Waiting list item action | Preset | Build |
| Reject participant | Host SDK method | Waiting list item action | Preset | Build |
| Admit all | Host SDK method | Waiting list bulk action | Preset | Build |
| Auto-admit when host joins | Preset `waiting_room` config | Preset editor toggle | Preset | Build |
| Bypass waiting room (by preset) | Preset configuration | Preset editor — bypass role list | Preset | Build |
7.4 Stage Management (Webinar)
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Stage view | `rtk-stage` component | Main content area (webinar mode) | Preset | Build |
| Join/leave stage toggle | `rtk-stage-toggle` component | Control bar (webinar) | Preset | Build |
| Join stage button | `rtk-join-stage` component | Viewer UI prompt | Preset | Build |
| On-stage participants list | `rtk-participants-stage-list` component | Host sidebar | Preset | Build |
| Stage request queue | `rtk-participants-stage-queue` component | Host sidebar > Requests tab | Preset | Build |
| Host: approve/deny stage requests | SDK host methods | Stage queue item actions | Preset | Build |
| Host: invite/remove from stage | SDK host methods | Participant context menu | Preset | Build |
7.5 Chat System
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Complete chat panel | `rtk-chat` component | Sidebar > Chat tab | Preset | Build |
| Chat toggle | `rtk-chat-toggle` component | Control bar | Client | Build |
| Chat header | `rtk-chat-header` component | Chat panel header (pinned msgs, DM selector) | Client | Build |
| Message composer | `rtk-chat-composer-view` component | Chat panel bottom | Client | Build |
| Chat messages list (paginated) | `rtk-chat-messages-ui-paginated` component | Chat panel body (infinite scroll) | Client | Build |
| Single message view | `rtk-chat-message` / `rtk-message-view` component | Within chat list | Client | Build |
| Markdown rendering | `rtk-markdown-view` component | Chat message content | Client | Build |
| Public chat | `chatPublic` preset permission | Default chat mode | Preset | Build |
| Private chat (1:1 DMs) | `chatPrivate` preset permission + `privateChatRecipient` prop | Chat selector > DM tab | Preset | Build |
| Chat selector (public/private) | `rtk-chat-selector` / `rtk-chat-selector-ui` | Chat panel header | Client | Build |
| Pinned messages | `rtk-pinned-message-selector` component | Chat header area (public chat only) | Client | Build |
| Chat search | `rtk-chat-search-results` component | Chat panel search bar | Client | Build |
| File messages | `rtk-file-message` / `rtk-file-message-view` | In chat message list | Client | Build |
| Image messages | `rtk-image-message` / `rtk-image-message-view` + `rtk-image-viewer` | In chat + full-screen viewer | Client | Build |
| File upload (drag & drop) | `rtk-file-dropzone` + `rtk-file-picker-button` | Chat composer area | Client | Build |
| Draft attachment preview | `rtk-draft-attachment-view` component | Chat composer area | Client | Build |
| Emoji picker | `rtk-emoji-picker` + `rtk-emoji-picker-button` | Chat composer | Client | Build |
| Chat export (post-meeting) | `meeting.chatSynced` webhook + Chat Replay REST API | Post-meeting page > Chat tab | Server | Build |
| Chat CSV dump | REST API download URL | Post-meeting download button | Server | Build |
| Fetch private messages | `meeting.chat.fetchPrivateMessages` SDK | DM chat history | Client | Build |
7.6 Polls
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Polls panel | `rtk-polls` component | Sidebar > Polls tab | Preset | Build |
| Polls toggle | `rtk-polls-toggle` component | Control bar | Client | Build |
| Single poll display | `rtk-poll` component | Within polls panel | Client | Build |
| Poll creation form | `rtk-poll-form` component | Polls panel > Create | Preset | Build |
| Poll permissions (create/view/interact) | Preset `polls` config | Preset editor > Polls section | Preset | Build |
7.7 Breakout Rooms (Connected Meetings)
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Breakout rooms manager | `rtk-breakout-rooms-manager` component | Sidebar > Breakout tab (host) | Preset | Build |
| Single room manager | `rtk-breakout-room-manager` component | Within breakout panel | Preset | Build |
| Room participant list | `rtk-breakout-room-participants` component | Within each room card | Client | Build |
| Breakout rooms toggle | `rtk-breakout-rooms-toggle` component | Control bar (host) | Client | Build |
| Broadcast message to rooms | `rtk-broadcast-message-modal` component | Breakout panel > Broadcast button | Preset | Build |
| Create/switch/close rooms | Connected Meetings SDK APIs | Breakout room management UI | Preset | Build |
| Cross-meeting broadcast | `meeting.participants.broadcastMessage` with `meetingIds` | Host broadcast action | Preset | Build |
| **Platform limitation** | Web only (beta) | Show "web only" badge; hide on mobile | — | Build |
7.8 Message Broadcasting & Collaborative Stores
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Broadcast message to meeting | `meeting.participants.broadcastMessage(type, payload)` | Custom event system (reactions, notifications) | Client | Build |
| Subscribe to broadcasts | `broadcastedMessage` event listener | Event handlers throughout app | Client | Build |
| Rate limiting config | `rateLimitConfig` / `updateRateLimits()` | Internal config (not user-facing) | Client | Build |
| Collaborative stores (KV) | `meeting.stores` API — create, subscribe, update | Shared state: hand raise, custom annotations, cursor position | Client | Build |
| Store subscription | `RTKStore` instance — `.subscribe()` on key changes | Real-time UI updates from store changes | Client | Build |
| Store session scoping | Data persists until session ends | Automatic cleanup — no action needed | — | Build |
7.9 Recording
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Start recording | Start Recording REST API + client SDK | Control bar > Record button (host) | Preset | Build |
| Stop recording | Stop Recording REST API + client SDK | Control bar > Stop Record button | Preset | Build |
| Recording toggle | `rtk-recording-toggle` component | Control bar | Preset | Build |
| Recording indicator | `rtk-recording-indicator` component | Meeting header (visible to all) | Client | Build |
| Fetch active recording | Fetch Active Recording REST API | Admin monitoring | Server | Build |
| Fetch recording details/download | Fetch Recording Details REST API | Post-meeting page > Recordings | Server | Build |
| `recording.statusUpdate` webhook | Webhook event (INVOKED/RECORDING/UPLOADING/UPLOADED/ERRORED) | Webhook handler > update D1 | Server | Build |
| Record on start | `record_on_start` meeting config | Meeting creation form | Meeting | Build |
| Composite recording | Default mode — multi-user single file (H.264/VP8) | Primary recording output | Meeting | Build |
| Custom cloud storage upload | AWS, Azure, DigitalOcean config via API | Org Settings > Storage config | App | Build |
| Watermarking | `video_config.watermark` in Start Recording API | Org Settings > Recording > Watermark | App | Build |
| Recording codec config | Video/audio codec parameters in Start Recording API | Org Settings > Recording > Quality | App | Build |
| Interactive recording (timed metadata) | Recording SDK + metadata API | Advanced: searchable playback markers | Meeting | Build |
| Custom recording app | `@cloudflare/realtimekit-recording-sdk` | Future: custom recording layouts | App | Future |
| Disable RTK bucket upload | Recording config option | Org Settings > Storage | App | Build |
| Recording config precedence | Config hierarchy management | Internal Worker logic | Server | Build |
| Per-track recording | `POST /recordings/track` — layers API for mapping audio/video tracks to output destinations ($0.0005/min) | Meeting settings toggle | Meeting | Build |
7.10 Livestreaming
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Livestream indicator | `rtk-livestream-indicator` component | Meeting header (during livestream) | Client | Build |
| Livestream toggle | `rtk-livestream-toggle` component | Control bar (host, webinar mode) | Preset | Build |
| Livestream player (HLS) | `rtk-livestream-player` component | Viewer page / external embed | Client | Build |
| RTMP export config | Part of recording/export system | Meeting settings > Livestream section | Meeting | Build |
| HLS playback (hls.js) | Bundled hls.js dependency | Viewer page, post-meeting playback | Client | Build |
| Livestream as recording export mode | Pricing: same as "Export (recording, RTMP or HLS streaming)" | Worker config when starting stream | Server | Build |
| Stage management for livestream | Stage + livestream integration | Webinar host controls during stream | Preset | Build |
7.11 Transcription & AI
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Real-time transcription | Whisper Large v3 Turbo (Workers AI) | In-meeting captions overlay | Meeting + Preset | Build |
| Caption toggle | `rtk-caption-toggle` component | Control bar | Client | Build |
| AI panel | `rtk-ai` component | Sidebar > AI tab | Client | Build |
| AI toggle | `rtk-ai-toggle` component | Control bar | Client | Build |
| Live transcription display | `rtk-ai-transcriptions` component | AI panel / caption overlay | Client | Build |
| Single transcript entry | `rtk-transcript` component | Within transcription list | Client | Build |
| Transcript history | `rtk-transcripts` component | AI panel scrollback | Client | Build |
| Transcription language config | `ai_config.transcription.language` on meeting creation | Meeting creation form > Language | Meeting | Build |
| `transcription_enabled` preset flag | Preset parameter | Preset editor toggle | Preset | Build |
| Post-meeting transcript | `meeting.transcript` webhook + REST API fetch | Post-meeting page > Transcript tab | Server | Build |
| Transcript download URL | Presigned R2 URL (7-day retention) | Post-meeting download button | Server | Build |
| AI meeting summary | `meeting.summary` webhook + REST API fetch/trigger | Post-meeting page > Summary tab | Server | Build |
| Summary download URL | Presigned R2 URL | Post-meeting download button | Server | Build |
| Summary type config | `ai_config.summarization.summary_type` (e.g. `"team_meeting"`) | Meeting creation form > Summary type | Meeting | Build |
| Summary text format | `text_format: "markdown"` | Internal config (always markdown) | Meeting | Build |
| Manual summary trigger | `POST /sessions/{id}/summary` REST API | Post-meeting page > Generate Summary button | Server | Build |
| Summary output (Key Points, Action Items, Decisions) | Structured markdown output | Post-meeting summary viewer | Server | Build |
7.12 Virtual Backgrounds & Video Effects
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Background blur | `@cloudflare/realtimekit-virtual-background` package | Settings > Video > Background | Client | Build |
| Virtual background (custom image) | Same package + Video Background addon | Settings > Video > Background | Client | Build |
| Video Background addon | `realtimekit-ui-addons` Video Background addon | Pre-join screen + in-meeting settings | Client | Build |
7.13 Plugins
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Plugins panel | `rtk-plugins` component | Sidebar > Plugins tab | Preset | Build |
| Plugins toggle | `rtk-plugins-toggle` component | Control bar | Client | Build |
| Plugin main view | `rtk-plugin-main` component | Main content area (when plugin active) | Client | Build |
| Whiteboard (built-in) | Built-in plugin, enabled via preset | Plugins panel > Whiteboard | Preset | Build |
| Document Sharing (built-in) | Built-in plugin, enabled via preset | Plugins panel > Document Sharing | Preset | Build |
| Custom plugins | Plugin SDK (`meeting.plugins` API, iframe-based) | Org Settings > Custom Plugins (future) | Preset | Build |
| Plugin permissions (view/open/close) | Preset `plugins` config | Preset editor > Plugins section | Preset | Build |
| Plugin data exchange | `plugin.sendData()` / `plugin.handleIframeMessage()` | Plugin <-> app communication | Client | Build |
7.14 Media Controls & Device Selection
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Camera toggle | `rtk-camera-toggle` component | Control bar | Client | Build |
| Camera selector | `rtk-camera-selector` component | Settings > Video > Camera picker | Client | Build |
| Microphone toggle | `rtk-mic-toggle` component | Control bar | Client | Build |
| Microphone selector | `rtk-microphone-selector` component | Settings > Audio > Mic picker | Client | Build |
| Speaker selector | `rtk-speaker-selector` component | Settings > Audio > Speaker picker | Client | Build |
| Audio visualizer | `rtk-audio-visualizer` component | Name tag, settings preview, pre-join | Client | Build |
| Settings panel | `rtk-settings` component | Sidebar > Settings tab | Client | Build |
| Audio settings | `rtk-settings-audio` component | Settings > Audio sub-panel | Client | Build |
| Video settings | `rtk-settings-video` component | Settings > Video sub-panel | Client | Build |
| Settings toggle | `rtk-settings-toggle` component | Control bar | Client | Build |
7.15 Screen Sharing & PiP
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Screen share toggle | `rtk-screen-share-toggle` component | Control bar | Client | Build |
| Screen share view | `rtk-screenshare-view` component | Main content area (when sharing) | Client | Build |
| Screen share frame rate config | SDK media config `screenShareConfig.frameRate` | Internal config (default 5 FPS) | Client | Build |
| Picture-in-Picture toggle | `rtk-pip-toggle` component | Control bar | Client | Build |
| PiP with reactions | SDK PiP support | Browser PiP window | Client | Build |
| Fullscreen toggle | `rtk-fullscreen-toggle` component | Control bar | Client | Build |
7.16 Simulcast & Active Speakers
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Simulcast (multi-quality streams) | SDK config (added Core 1.2.0) | Internal — transparent to user | Client | Build |
| Per-track simulcast config | `preferredRid`, `priorityOrdering`, `ridNotAvailable` | Bandwidth-adaptive quality (automatic) | Client | Build |
| Active speaker detection | `meeting.participants.activeSpeaker` (single) | Spotlight grid, speaker indicator | Client | Build |
| Active participants list | `meeting.participants.activeParticipants` (multiple) | Active speaker grid layout | Client | Build |
| Spotlight grid | `rtk-spotlight-grid` component | Layout option: spotlight mode | Client | Build |
| Spotlight indicator | `rtk-spotlight-indicator` component | On active speaker tile | Client | Build |
| Network quality indicator | `rtk-network-indicator` component | On participant tiles | Client | Build |
7.17 Grid Layouts
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Default responsive grid | `rtk-grid` component | Main content area (default layout) | Client | Build |
| Simple grid | `rtk-simple-grid` component | Compact layout option | Client | Build |
| Mixed grid (video + screenshare) | `rtk-mixed-grid` component | When screen sharing active | Client | Build |
| Audio-only grid | `rtk-audio-grid` component | Voice meeting layout | Client | Build |
| Grid pagination | `rtk-grid-pagination` component | Below grid (large meetings) | Client | Build |
7.18 UI Chrome & Meeting Shell
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Complete meeting experience | `rtk-meeting` component | Drop-in full meeting page | Client | Build |
| Pre-join setup screen | `rtk-setup-screen` component | Meeting lobby / pre-join | Client | Build |
| Idle/disconnected screen | `rtk-idle-screen` component | Before joining / connection lost | Client | Build |
| Meeting ended screen | `rtk-ended-screen` component | After meeting ends | Client | Build |
| Leave meeting | `rtk-leave-meeting` / `rtk-leave-button` | Control bar > Leave | Client | Build |
| Control bar | `rtk-controlbar` component | Bottom of meeting view | Client | Build |
| Control bar buttons | `rtk-controlbar-button` component | Individual action buttons | Client | Build |
| Sidebar container | `rtk-sidebar` / `rtk-sidebar-ui` component | Right side panel (chat, participants, etc.) | Client | Build |
| Dialog/modal system | `rtk-dialog` / `rtk-dialog-manager` component | Confirmation dialogs, settings modals | Client | Build |
| Menu system | `rtk-menu` / `rtk-menu-item` / `rtk-menu-list` | Context menus, dropdowns | Client | Build |
| Notification system | `rtk-notification` / `rtk-notifications` | Toast notifications | Client | Build |
| Tab navigation | `rtk-tab-bar` component | Sidebar tab switching | Client | Build |
| Overlay modal | `rtk-overlay-modal` component | Full-screen overlays | Client | Build |
| Confirmation modal | `rtk-confirmation-modal` component | Destructive action confirmations | Client | Build |
| Permissions prompt | `rtk-permissions-message` component | Browser permission requests | Client | Build |
| More options menu | `rtk-more-toggle` component | Control bar overflow menu | Client | Build |
| Header bar | `rtk-header` component | Meeting top bar | Client | Build |
| Logo display | `rtk-logo` component | Header / idle screen | Client | Build |
| Loading spinner | `rtk-spinner` component | Loading states | Client | Build |
7.19 Addons
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Camera host control | `realtimekit-ui-addons` Camera Host Control | Participant tile menu (host) | Preset | Build |
| Mic host control | `realtimekit-ui-addons` Mic Host Control | Participant tile menu (host) | Preset | Build |
| Chat host control | `realtimekit-ui-addons` Chat Host Control | Participant tile menu (host) | Preset | Build |
| Hand raise | `realtimekit-ui-addons` Hand Raise addon | Control bar + participant list | Client | Build |
| Reactions | `realtimekit-ui-addons` Reactions Manager | Control bar + floating reactions overlay | Client | Build |
| Participant tile menu | `realtimekit-ui-addons` Participant Tile Menu | Right-click / long-press on tile | Client | Build |
| Custom control bar button | `realtimekit-ui-addons` Custom Control Bar Button | Extend control bar with app-specific actions | Client | Build |
| Participant tab actions | `realtimekit-ui-addons` Participants Tab Action/Toggle | Participant panel extensions | Client | Build |
7.20 Branding & Design System
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| UI Provider context | `rtk-ui-provider` component | App root wrapper | App | Build |
| Design system tokens | `provideRtkDesignSystem()` utility | App initialization | App | Build |
| Theme (light/dark/darkest) | Design token `theme` | Org Settings > Branding | App | Build |
| Brand color | Design token `brand.500` | Org Settings > Branding | App | Build |
| Background color | Design token `background.1000` | Org Settings > Branding | App | Build |
| Typography | Design token `fontFamily` / `googleFont` | Org Settings > Branding | App | Build |
| Spacing scale | Design token `spacingBase` | Internal (default 4px) | App | Build |
| Border width/radius | Design tokens | Org Settings > Branding | App | Build |
| Custom icon pack | JSON SVG icon pack (40+ icons) | Org Settings > Branding > Icons | App | Build |
| Custom logo | `rtk-logo` component or `rtk-meeting` prop | Org Settings > Branding > Logo | App | Build |
| Internationalization (i18n) | `RtkI18n` type + `t` prop / `useLanguage()` hook | Org Settings > Language | App | Build |
| CSS variable prefix (`--rtk-*`) | All design system outputs | Automatic from token config | App | Build |
7.21 Debug & Diagnostics
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Debug panel | `rtk-debugger` component | Settings > Debug (admin/dev only) | Client | Build |
| Debug toggle | `rtk-debugger-toggle` component | Control bar (admin only) | Client | Build |
| Audio debug info | `rtk-debugger-audio` component | Debug panel > Audio tab | Client | Build |
| Video debug info | `rtk-debugger-video` component | Debug panel > Video tab | Client | Build |
| Screenshare debug info | `rtk-debugger-screenshare` component | Debug panel > Screen tab | Client | Build |
| System debug info | `rtk-debugger-system` component | Debug panel > System tab | Client | Build |
| Error codes | RTK error code reference | Error display UI + Sentry reporting | Client | Build |
7.22 Realtime Agents (Low Priority — Groundwork Only)
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| Agent as meeting participant | `@cloudflare/realtime-agents` SDK | Agent joins meeting with authToken | Server (DO) | Low Priority |
| STT pipeline (Deepgram) | `DeepgramSTT` component | Automatic transcription of meeting audio | Server (DO) | Low Priority |
| LLM processing | `TextComponent` + Workers AI binding | Agent text generation | Server (DO) | Low Priority |
| TTS pipeline (ElevenLabs) | `ElevenLabsTTS` component | Agent spoken responses | Server (DO) | Low Priority |
| RTK Transport | `RealtimeKitTransport` component | Agent media I/O to meeting | Server (DO) | Low Priority |
| DO binding in wrangler | Durable Object config | `wrangler.jsonc` setup | Server | Low Priority |
| Agent route stubs | `/agent/*` and `/agentsInternal` routes | Worker routing | Server | Low Priority |
7.23 TURN (ICE Fallback)
| Feature | RTK API/Component | App Location | Config Level | Status |
|---|---|---|---|---|
| TURN relay (automatic via RTK) | Managed by RealtimeKit platform internally | Transparent — no user-facing config | Platform | Build |
| TURN credential generation | Short-lived credentials with TTL (max 48h) | Automatic via SDK | Platform | Build |
| Protocol support (STUN/UDP, TURN/UDP/TCP/TLS) | Platform-level | Automatic fallback | Platform | Build |
Section 8: Post-Meeting Experience
The post-meeting page is a first-class product surface — not an afterthought. Users land here after a meeting ends, from email links (triggered by meeting.ended webhook), and from the meeting history list.
8.1 Page Structure
Route: /meetings/{meetingId}/sessions/{sessionId}
Layout: Full-width page with tabbed content area and session metadata header.
+------------------------------------------------------------------+
| Session Header |
| Meeting Title | Date & Time | Duration | Participant Count |
| Host: Name | Type: Video/Voice/Webinar | Status: Ended |
+------------------------------------------------------------------+
| [Summary] [Transcript] [Recording] [Chat] [Analytics] |
+------------------------------------------------------------------+
| |
| (Active tab content) |
| |
+------------------------------------------------------------------+8.2 Session Metadata Header
Sourced from D1 (populated by meeting.ended webhook + REST API enrichment):
| Field | Source |
|---|---|
| Meeting title | `meeting.meta.meetingTitle` (stored in D1 on session start) |
| Date & time | Webhook `meeting.ended` timestamp + session start time |
| Duration | Calculated: end - start |
| Participant count | From webhook payload or REST API session detail |
| Host name | From our user system (mapped via participant userId) |
| Meeting type | From preset config (Video/Voice/Webinar) |
| Recording available | Boolean — from `recording.statusUpdate` UPLOADED event |
| Transcript available | Boolean — from `meeting.transcript` webhook received |
| Summary available | Boolean — from `meeting.summary` webhook received |
8.3 Summary Tab
Data source: meeting.summary webhook summaryDownloadUrl or REST API GET /sessions/{id}/summary
Display:
- Rendered markdown (RTK returns structured markdown with sections: Key Discussion Points, Action Items, Decisions)
- Copy-to-clipboard button for full summary
- Download as
.mdbutton - If summary not yet generated and meeting has transcription data, show "Generate Summary" button (calls
POST /sessions/{id}/summary) - If no transcription was enabled, show disabled state with explanation
Retention notice: "Available for 7 days from meeting end" with countdown badge.
8.4 Transcript Tab
Data source: meeting.transcript webhook transcriptDownloadUrl or REST API GET /sessions/{id}/transcript
Display:
- Timeline-based transcript viewer with speaker labels and timestamps
- Each entry:
[HH:MM:SS] Speaker Name: "Transcript text" - Search within transcript (client-side filter)
- Jump to timestamp (if recording is available, clicking a timestamp seeks the recording player)
- Download as
.txtor.json - Partial transcript indicator: entries with
isPartialTranscript: trueare excluded from post-meeting view (only final transcripts shown)
Retention notice: Same 7-day window.
8.5 Recording Tab
Data source: recording.statusUpdate webhook (UPLOADED state) provides download URL via Fetch Recording Details REST API.
Display:
- Video player (HTML5
<video>) for composite recording playback - Player controls: play/pause, seek, volume, playback speed (0.5x-2x), fullscreen
- If transcript is available: synchronized caption overlay (map transcript timestamps to video timeline)
- Download original recording button
- Recording metadata: codec (H.264/VP8), duration, file size, storage location
- If watermark was applied: watermark indicator
- If recording is still processing (UPLOADING state): show progress indicator
- If recording errored: show error state with explanation
Custom cloud storage: If org has configured external storage (AWS/Azure/DigitalOcean), recording URL points to their bucket. Display storage location label.
8.6 Chat Tab
Data source: meeting.chatSynced webhook triggers chat replay fetch via REST API. Chat CSV dump available for download.
Display:
- Full chat replay in chronological order
- Message rendering: text, markdown, files, images (reusing chat components where possible)
- Speaker attribution with participant names
- File/image attachments with download links
- Search within chat
- Download as CSV button (using RTK's CSV dump format: id, participant info, message content, metadata)
- Pinned messages highlighted at top
8.7 Analytics Tab (Session-Level)
Data source: D1 database (custom-built, populated by webhooks — see Section 9).
Display:
- Participation timeline: Visual bar showing when each participant was in the session (join/leave events)
- Duration breakdown: Total session time, per-participant time, average engagement
- Feature usage: Recording duration, chat message count, poll count, screen share count
- Quality indicators: If available from debug data — connection quality summary
This tab is custom-built (RTK has no analytics API). Data is approximate, based on webhook events and periodic REST API polling during active sessions.
8.8 Access Control
- Org members: Can access any session from their org's meetings
- Guests: Cannot access post-meeting pages (redirected to sign-in)
- Hosts/Admins: See full analytics tab; members see summary/transcript/recording/chat only
- Expiry: After 7-day retention window, transcript/summary/recording links expire. D1 metadata and chat data persist indefinitely.
8.9 Email Notification Flow
meeting.endedwebhook fires- Worker processes event, stores session metadata in D1
- Worker enqueues email notification to all participants with our user accounts
- Email contains: meeting title, duration, summary snippet (first 200 chars), link to post-meeting page
- If transcript/summary/recording not yet ready (async processing), email says "Processing..." with link to check back
Section 9: Webhook & Analytics Pipeline
9.1 Webhook Architecture
Endpoint: POST /api/webhooks/rtk — a route on our Cloudflare Worker
Registration: On app setup, Worker calls POST /webhooks REST API to register our webhook URL for all supported events.
Security: Webhook payloads should be validated (signature verification if RTK provides it; if not, validate by cross-referencing event data with REST API).
9.2 Webhook Event Catalog
Every webhook event RTK sends, what we do with each:
| Webhook Event | Payload (key fields) | Our Action | D1 Table |
|---|---|---|---|
| `meeting.started` | `meetingId`, `sessionId`, timestamp | Create session record in D1 with exact start time. Update KV meeting-state. | `sessions` |
| `meeting.ended` | `meetingId`, `sessionId`, timestamp | 1. Store session end record in D1. 2. Trigger post-meeting data aggregation (fetch participant list, duration from REST API). 3. Queue email notifications to participants. 4. Mark session as ENDED in D1. | `sessions` |
| `meeting.participantJoined` | `meetingId`, `sessionId`, `participantId` | Update real-time participant count, update `peak_participant_count` if new high. | `sessions`, `session_participants` |
| `meeting.participantLeft` | `meetingId`, `sessionId`, `participantId` | Update participant record with leave time, compute individual duration. Decrement count. | `sessions`, `session_participants` |
| `recording.statusUpdate` | `meetingId`, `sessionId`, `recordingId`, `status` (INVOKED/RECORDING/UPLOADING/UPLOADED/ERRORED) | 1. Upsert recording status in D1. 2. On UPLOADED: fetch recording details (download URL, duration, size) from REST API, store in D1. 3. On ERRORED: log to Sentry, mark recording as failed. | `recordings` |
| `meeting.transcript` | `meetingId`, `sessionId`, `transcriptDownloadUrl`, `transcriptDownloadUrlExpiry` | 1. Store transcript URL and expiry in D1. 2. Download transcript JSON, store parsed version in D1 for search/display. 3. Update session record: `has_transcript = true`. | `transcripts`, `sessions` |
| `meeting.summary` | `meetingId`, `sessionId`, `summaryDownloadUrl`, `summaryDownloadUrlExpiry` | 1. Store summary URL and expiry in D1. 2. Download summary markdown, store in D1 for display. 3. Update session record: `has_summary = true`. | `summaries`, `sessions` |
| `meeting.chatSynced` | `meetingId`, `sessionId` | 1. Fetch chat replay via REST API. 2. Store chat messages in D1 (persists beyond 7-day window). 3. Update session record: `has_chat = true`. | `chat_messages`, `sessions` |
| `livestreaming.statusUpdate` | `meetingId`, `livestreamId`, `status` | Update livestream state in D1. On completion, store playback URL. | `livestreams` |
9.3 Inferred Events (REST API Polling)
With meeting.participantJoined/Left webhooks, real-time participant tracking is handled. Polling supplements for reliability and catches edge cases:
| Data Point | Method | Frequency | D1 Table |
|---|---|---|---|
| Active sessions list | `GET /meetings` (filter ACTIVE) | Every 60s via Cron Trigger | `sessions` |
| Participant count per active session | REST API session detail | Every 60s during active sessions | `session_snapshots` |
| Recording status (backup) | `GET /recordings/active-recording/{meeting_id}` | Every 30s during active recording (per-meeting) | `recordings` |
| Meeting metadata changes | `GET /meetings/{id}` | On-demand when needed | `meetings` |
Cron Trigger: A scheduled Worker runs every 60 seconds to poll active sessions and update D1. This supplements webhook data for analytics accuracy.
9.4 D1 Analytics Schema
-- Core entities (our system, not RTK)
CREATE TABLE organizations (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
settings_json TEXT -- org-level config (branding, storage, etc.)
);
CREATE TABLE users (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL REFERENCES organizations(id),
email TEXT NOT NULL,
name TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'host', 'member')),
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- RTK entity mirrors (synced from RTK via REST API)
CREATE TABLE rtk_apps (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL REFERENCES organizations(id),
name TEXT NOT NULL,
environment TEXT NOT NULL CHECK (environment IN ('production', 'staging')),
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE meetings (
id TEXT PRIMARY KEY,
rtk_app_id TEXT NOT NULL REFERENCES rtk_apps(id),
org_id TEXT NOT NULL REFERENCES organizations(id),
title TEXT NOT NULL,
meeting_type TEXT NOT NULL CHECK (meeting_type IN ('video', 'audio', 'webinar', 'livestream')),
status TEXT NOT NULL CHECK (status IN ('active', 'inactive')) DEFAULT 'inactive',
record_on_start INTEGER NOT NULL DEFAULT 0,
persist_chat INTEGER NOT NULL DEFAULT 1,
ai_config_json TEXT, -- transcription + summarization config
created_by TEXT REFERENCES users(id),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Session tracking (populated by webhooks + REST API)
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
meeting_id TEXT NOT NULL REFERENCES meetings(id),
org_id TEXT NOT NULL REFERENCES organizations(id),
started_at TEXT,
ended_at TEXT,
duration_seconds INTEGER, -- calculated on end
participant_count INTEGER DEFAULT 0,
peak_participant_count INTEGER DEFAULT 0,
has_recording INTEGER NOT NULL DEFAULT 0,
has_transcript INTEGER NOT NULL DEFAULT 0,
has_summary INTEGER NOT NULL DEFAULT 0,
has_chat INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL CHECK (status IN ('active', 'ended')) DEFAULT 'active',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_sessions_meeting ON sessions(meeting_id);
CREATE INDEX idx_sessions_org ON sessions(org_id);
CREATE INDEX idx_sessions_status ON sessions(status);
CREATE INDEX idx_sessions_started ON sessions(started_at);
-- Session participant snapshots (from polling during active sessions)
CREATE TABLE session_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL REFERENCES sessions(id),
participant_count INTEGER NOT NULL,
recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_snapshots_session ON session_snapshots(session_id);
-- Recording metadata (from recording.statusUpdate webhook)
CREATE TABLE recordings (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL REFERENCES sessions(id),
meeting_id TEXT NOT NULL REFERENCES meetings(id),
org_id TEXT NOT NULL REFERENCES organizations(id),
status TEXT NOT NULL CHECK (status IN ('invoked', 'recording', 'uploading', 'uploaded', 'errored')),
download_url TEXT,
download_url_expiry TEXT,
duration_seconds INTEGER,
file_size_bytes INTEGER,
codec TEXT, -- 'h264' or 'vp8'
storage_location TEXT, -- 'r2' | 'aws' | 'azure' | 'digitalocean' | 'gcs' | 'sftp'
has_watermark INTEGER NOT NULL DEFAULT 0,
started_at TEXT,
ended_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_recordings_session ON recordings(session_id);
CREATE INDEX idx_recordings_org ON recordings(org_id);
CREATE INDEX idx_recordings_status ON recordings(status);
-- Transcript metadata (from meeting.transcript webhook)
CREATE TABLE transcripts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL REFERENCES sessions(id),
meeting_id TEXT NOT NULL REFERENCES meetings(id),
org_id TEXT NOT NULL REFERENCES organizations(id),
download_url TEXT NOT NULL,
download_url_expiry TEXT NOT NULL,
transcript_json TEXT, -- full parsed transcript for in-app display
language TEXT DEFAULT 'en-US',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_transcripts_session ON transcripts(session_id);
-- Summary metadata (from meeting.summary webhook)
CREATE TABLE summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL REFERENCES sessions(id),
meeting_id TEXT NOT NULL REFERENCES meetings(id),
org_id TEXT NOT NULL REFERENCES organizations(id),
download_url TEXT NOT NULL,
download_url_expiry TEXT NOT NULL,
summary_markdown TEXT, -- full summary content for in-app display
summary_type TEXT, -- e.g. 'team_meeting'
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_summaries_session ON summaries(session_id);
-- Chat messages (from meeting.chatSynced webhook + Chat Replay API)
CREATE TABLE chat_messages (
id TEXT PRIMARY KEY, -- RTK chat message ID
session_id TEXT NOT NULL REFERENCES sessions(id),
meeting_id TEXT NOT NULL REFERENCES meetings(id),
org_id TEXT NOT NULL REFERENCES organizations(id),
sender_user_id TEXT, -- RTK participant userId
sender_name TEXT,
message_type TEXT NOT NULL CHECK (message_type IN ('text', 'file', 'image')),
content TEXT, -- message body
is_pinned INTEGER NOT NULL DEFAULT 0,
is_private INTEGER NOT NULL DEFAULT 0,
recipient_user_id TEXT, -- for private messages
sent_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_chat_session ON chat_messages(session_id);
CREATE INDEX idx_chat_pinned ON chat_messages(session_id, is_pinned) WHERE is_pinned = 1;
-- Webhook event log (all raw webhook payloads for debugging/replay)
CREATE TABLE webhook_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL,
meeting_id TEXT,
session_id TEXT,
payload_json TEXT NOT NULL,
processed INTEGER NOT NULL DEFAULT 0,
error_message TEXT,
received_at TEXT NOT NULL DEFAULT (datetime('now')),
processed_at TEXT
);
CREATE INDEX idx_webhook_type ON webhook_events(event_type);
CREATE INDEX idx_webhook_unprocessed ON webhook_events(processed) WHERE processed = 0;
-- Aggregate analytics (computed daily by Cron Trigger)
CREATE TABLE daily_analytics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
org_id TEXT NOT NULL REFERENCES organizations(id),
date TEXT NOT NULL, -- YYYY-MM-DD
total_sessions INTEGER NOT NULL DEFAULT 0,
total_participants INTEGER NOT NULL DEFAULT 0,
total_duration_minutes INTEGER NOT NULL DEFAULT 0,
total_recordings INTEGER NOT NULL DEFAULT 0,
total_recording_minutes INTEGER NOT NULL DEFAULT 0,
total_chat_messages INTEGER NOT NULL DEFAULT 0,
total_transcripts INTEGER NOT NULL DEFAULT 0,
total_summaries INTEGER NOT NULL DEFAULT 0,
meeting_type_video INTEGER NOT NULL DEFAULT 0,
meeting_type_audio INTEGER NOT NULL DEFAULT 0,
meeting_type_webinar INTEGER NOT NULL DEFAULT 0,
meeting_type_livestream INTEGER NOT NULL DEFAULT 0,
computed_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(org_id, date)
);
CREATE INDEX idx_daily_org_date ON daily_analytics(org_id, date);9.5 Webhook Processing Pipeline
Incoming POST /api/webhooks/rtk
|
v
1. Validate payload (signature check or cross-reference)
2. Store raw event in webhook_events table (payload_json)
3. Route by event_type:
|
+-- meeting.ended -----------> processSessionEnd()
+-- recording.statusUpdate --> processRecordingUpdate()
+-- meeting.transcript ------> processTranscript()
+-- meeting.summary ---------> processSummary()
+-- meeting.chatSynced ------> processChatSync()
|
4. Mark webhook_events.processed = 1
5. On error: set error_message, keep processed = 0 for retry9.6 Retry & Reliability
- Idempotency: All webhook handlers use upsert patterns keyed by RTK IDs. Duplicate delivery is safe.
- Dead letter: Unprocessed events (processed = 0 with error_message) are retried by a Cron Trigger every 5 minutes, up to 3 attempts.
- Monitoring: Sentry alert on webhook processing failures. Dashboard shows unprocessed event count.
- Raw event storage: All webhook payloads stored permanently in
webhook_eventsfor debugging and replay.
9.7 Analytics Computation
Real-time (per-event):
- Session count, participant count, recording status — updated immediately on webhook receipt
Periodic (Cron Trigger, daily at 00:05 UTC):
- Aggregate
daily_analyticsrows from sessions, recordings, chat_messages, transcripts, summaries - Roll up per-org daily totals for dashboard display
Dashboard queries (examples):
- Sessions this week:
SELECT COUNT(*) FROM sessions WHERE org_id = ? AND started_at >= date('now', '-7 days') - Average session duration:
SELECT AVG(duration_seconds) FROM sessions WHERE org_id = ? AND ended_at IS NOT NULL - Recording usage:
SELECT SUM(duration_seconds) FROM recordings WHERE org_id = ? AND status = 'uploaded' - Feature adoption:
SELECT has_transcript, has_summary, has_recording, COUNT(*) FROM sessions WHERE org_id = ? GROUP BY 1, 2, 3
Design Document: Sections 10-13
Section 10: App Pages & Navigation
10.1 Page Inventory
The app has a flat navigation structure with role-gated access. Every page listed below is a discrete route.
Public Pages (no auth required)
| Page | Route | Purpose | Key Elements |
|---|---|---|---|
| **Login** | `/login` | Email/password + OAuth login | Login form, OAuth buttons, "forgot password" link, org logo |
| **Register** | `/register` | Account creation (invite-only or open, per org setting) | Registration form, invite code field (if required), terms checkbox |
| **Forgot Password** | `/forgot-password` | Password reset request | Email input, submit, confirmation message |
| **Reset Password** | `/reset-password/:token` | Set new password | New password + confirm fields |
| **Join as Guest** | `/join/:meetingId` | Guest entry to a specific meeting | Name input, optional email, device preview (camera/mic), "Join" button |
Authenticated Pages (any role)
| Page | Route | Purpose | Key Elements |
|---|---|---|---|
| **Dashboard** | `/` | Home page — upcoming meetings, quick actions | Meeting list (upcoming/recent), "New Meeting" button, join-by-code input |
| **Meeting Room** | `/meeting/:meetingId` | Live meeting experience (RTK UI Kit) | Full `rtk-meeting` component or custom layout with RTK components |
| **Pre-Join** | `/meeting/:meetingId/setup` | Device check before joining | `rtk-setup-screen` — camera preview, mic selector, speaker test, display name |
| **Post-Meeting** | `/meeting/:meetingId/summary` | After-meeting experience | Recording player, transcript viewer, AI summary, chat export, attendee list |
| **Recordings** | `/recordings` | Browse all recordings user has access to | Recording list with search/filter, thumbnail, duration, date, download link |
| **Recording Player** | `/recordings/:recordingId` | Playback a specific recording | Video player, transcript sidebar (synced), AI summary tab, download button |
| **Profile** | `/profile` | User's own profile settings | Display name, avatar upload, email, password change, notification prefs |
Host/Admin Pages (role-gated)
| Page | Route | Purpose | Key Elements |
|---|---|---|---|
| **Schedule Meeting** | `/meetings/new` | Create instant or scheduled meeting | Meeting type selector (instant/scheduled/recurring/permanent), title, date/time picker, preset selector, recording toggle, waiting room toggle, invite list |
| **Meeting Settings** | `/meetings/:meetingId/settings` | Edit meeting config before/between sessions | Same fields as create, plus: delete meeting, regenerate link, view past sessions |
| **Meeting History** | `/meetings/:meetingId/history` | Past sessions for a specific meeting room | Session list with start/end times, participant count, recordings, transcripts |
Admin Pages (Admin/Owner only)
| Page | Route | Purpose | Key Elements |
|---|---|---|---|
| **Org Settings** | `/admin/settings` | Organization-wide configuration | Tabbed interface (see Section 12 for full schema) |
| **Members** | `/admin/members` | User management | Member list, role assignment (Owner/Admin/Host/Member), invite button, remove/deactivate |
| **Invite** | `/admin/invite` | Send invitations | Email input (single or bulk CSV), role selector, custom message, pending invites list |
| **Analytics** | `/admin/analytics` | Usage dashboard | Session count, participant count, total duration, recording stats, charts (daily/weekly/monthly) |
| **Presets** | `/admin/presets` | Manage RTK presets | List of presets with preview of permissions, create/edit/clone/delete, JSON editor for advanced users |
| **Branding** | `/admin/branding` | Visual customization | Theme picker, color inputs, logo upload, font selector, live preview panel |
| **Recording Storage** | `/admin/storage` | Configure recording destination | Storage provider selector (RTK default, AWS S3, Azure Blob, DigitalOcean Spaces), credentials form, test connection button |
10.2 Navigation Structure
Top Nav Bar (persistent, all authenticated pages):
+------------------------------------------------------------------+
| [Org Logo] Dashboard Recordings | [Profile Avatar] [Logout] |
+------------------------------------------------------------------+
| |
| (Admin/Owner only) |
+-- Admin dropdown: +-- Profile
Settings | Members | Analytics | Change password
Presets | Branding | Storage Notification prefsRules:
- Top nav is hidden during active meeting (meeting room is full-screen)
- Admin dropdown only visible to Admin/Owner roles
- "Schedule Meeting" is a button on Dashboard, not a nav item
- Mobile: top nav collapses to hamburger menu
- Post-meeting page shows a "Back to Dashboard" link prominently
10.3 Meeting Room Layout
The meeting room (/meeting/:meetingId) is the core experience. It uses RTK UI Kit components composed into a standard layout:
+------------------------------------------------------------------+
| rtk-header: [Logo] [Meeting Title] [Clock] [Recording Indicator] |
+------------------------------------------------------------------+
| | |
| | rtk-sidebar: |
| rtk-grid (or rtk-spotlight-grid | - rtk-chat |
| or rtk-mixed-grid) | - rtk-participants |
| | - rtk-polls |
| Contains: rtk-participant-tile | - rtk-ai |
| rtk-screenshare-view | - rtk-plugins |
| rtk-name-tag | - rtk-breakout- |
| rtk-audio-visualizer | rooms-manager |
| | |
+------------------------------------------------------------------+
| rtk-controlbar: |
| [Mic] [Camera] [Screenshare] [Recording] [More...] [Leave] |
| Sidebar toggles: [Chat] [Participants] [Polls] [AI] [Plugins] |
+------------------------------------------------------------------+Meeting lifecycle states mapped to RTK components:
- Not connected →
rtk-idle-screen(redirects to pre-join) - Pre-join →
rtk-setup-screen(device selection + preview) - Waiting room →
rtk-waiting-screen(if waiting room enabled) - In meeting → Full layout above
- Meeting ended →
rtk-ended-screen→ auto-redirect to post-meeting page
10.4 Page Transitions
| From | To | Trigger |
|---|---|---|
| Dashboard | Pre-Join | Click meeting link or "Join" button |
| Pre-Join | Meeting Room | Click "Join Meeting" after device setup |
| Pre-Join | Waiting Room | Auto, if meeting has waiting room enabled |
| Waiting Room | Meeting Room | Host admits participant |
| Meeting Room | Post-Meeting | Meeting ends or user leaves |
| Post-Meeting | Dashboard | Click "Back to Dashboard" |
| Post-Meeting | Recording Player | Click "View Recording" (when available) |
| Dashboard | Schedule Meeting | Click "New Meeting" |
Section 11: Branding & Theming
11.1 RTK Design Token System
All visual customization flows through provideRtkDesignSystem(), which generates CSS custom properties (prefix: --rtk-*). The org admin configures these values; they are passed to rtk-ui-provider or rtk-meeting at runtime.
Available Tokens
| Category | Token | Type | Default | Notes |
|---|---|---|---|---|
| **Theme** | `theme` | `'light' | 'dark' | 'darkest'` | `'dark'` | Sets background shade palette globally |
| **Brand Color** | `colors.brand.500` | hex color | RTK default blue | Primary accent. RTK auto-generates 300-700 shades |
| **Background** | `colors.background.1000` | hex color | Theme-dependent | Base background. RTK auto-generates 600-900 shades |
| **Text** | `colors.text` | hex color | `#FFFFFF` (dark) | Primary text. Lighter shades derived automatically |
| **Text on Primary** | `colors["text-on-primary"]` | hex color | `#FFFFFF` | Text on brand-colored surfaces |
| **Video BG** | `colors["video-bg"]` | hex color | `#1A1A1A` | Background of video tiles when camera off |
| **Font** | `fontFamily` | CSS font-family string | System default | Or use `googleFont` for Google Fonts auto-loading |
| **Spacing** | `spacingBase` | number (px) | `4` | Base unit; scale 0-96 auto-generated |
| **Borders** | `borderWidth` | `'none' | 'thin' | 'fat'` | `'thin'` | Component border thickness |
| **Corners** | `borderRadius` | `'sharp' | 'rounded' | 'extra-rounded' | 'circular'` | `'rounded'` | Corner rounding style |
Application Code
// At app initialization, before rendering any RTK components
import { provideRtkDesignSystem } from '@cloudflare/realtimekit-react-ui';
provideRtkDesignSystem({
theme: orgSettings.theme, // from org_settings table
colors: {
brand: { 500: orgSettings.brandColor },
background: { 1000: orgSettings.backgroundColor },
text: orgSettings.textColor,
"text-on-primary": orgSettings.textOnPrimaryColor,
"video-bg": orgSettings.videoBgColor,
},
fontFamily: orgSettings.fontFamily,
googleFont: orgSettings.googleFont, // mutually exclusive with fontFamily
borderWidth: orgSettings.borderWidth,
borderRadius: orgSettings.borderRadius,
});11.2 Logo Configuration
| Setting | Storage | Usage |
|---|---|---|
| `logoUrl` | R2 bucket (uploaded via admin UI) | Passed to `rtk-logo` component; shown in meeting header and idle/ended screens |
| `faviconUrl` | R2 bucket | Set as page favicon via `` |
| `logoHeight` | org_settings (number, px) | CSS constraint on logo display height |
Upload flow: Admin uploads image → Worker stores in R2 → returns public URL → saved to org_settings → served via R2 public access or signed URL.
11.3 Icon Pack Customization
RTK supports full SVG icon replacement via a JSON icon pack object (40+ icons). The icon editor at icons.realtime.cloudflare.com generates the JSON.
| Setting | Type | Storage |
|---|---|---|
| `customIconPack` | JSON blob | org_settings (TEXT column, JSON-serialized) |
Passed to rtk-meeting or rtk-ui-provider via the iconPack prop.
11.4 Recording Watermark
RTK supports watermarking on composite recordings natively. Configuration is per-preset (passed at recording start time).
| Setting | Type | Scope | Notes |
|---|---|---|---|
| `watermark.enabled` | boolean | org-wide default, overridable per meeting | Whether to apply watermark |
| `watermark.url` | string (URL) | org-wide | Watermark image (typically org logo), stored in R2 |
| `watermark.position` | enum: `left top`, `right top`, `left bottom`, `right bottom` | org-wide | Position on the recording frame |
| `watermark.size` | object: `{ height: number, width: number }` (px) | org-wide | Size of the watermark overlay |
Applied when starting a recording via the RTK Recording SDK / REST API parameters.
11.5 Internationalization
| Setting | Type | Scope |
|---|---|---|
| `defaultLocale` | string (BCP 47) | org-wide |
| `customStrings` | JSON blob | org-wide |
RTK components accept an RtkI18n object via the t prop. The app merges org custom strings over RTK defaults and passes them through rtk-ui-provider.
Section 12: Organization Configuration
12.1 Configuration Hierarchy
Settings apply at three levels with clear override rules:
Org-Wide Defaults (org_settings table)
└── Per-Meeting Overrides (meetings table columns)
└── Runtime Overrides (RTK preset / SDK calls during session)Rules:
- Org-wide defaults apply to all new meetings unless explicitly overridden
- Per-meeting overrides are set at meeting creation/edit time by the host
- Runtime overrides happen during a live session (e.g., host mutes all, starts recording)
- Lower levels can only override settings that the org allows overriding (some are locked at org level)
12.2 What Lives Where
| Setting | Org-Wide | Per-Meeting | Runtime | Notes |
|---|---|---|---|---|
| Branding (theme, colors, logo, fonts) | Yes (primary) | No | No | Org-wide only |
| Icon pack | Yes (primary) | No | No | Org-wide only |
| Default meeting type | Yes | Yes (override) | No | video, audio, webinar, livestream |
| Waiting room enabled | Yes (default) | Yes (override) | No | Host can toggle per meeting |
| Recording auto-start | Yes (default) | Yes (override) | Yes (start/stop) | |
| Recording storage provider | Yes (primary) | No | No | Org-wide only |
| Watermark config | Yes (primary) | No | No | Org-wide only |
| Max participants | Yes (default) | Yes (override) | No | Per-meeting cap |
| Guest access enabled | Yes (default) | Yes (override) | No | |
| Chat enabled | Yes (default) | Yes (override) | Yes (mute) | Via preset permissions |
| Polls enabled | Yes (default) | Yes (override) | No | Via preset |
| Breakout rooms enabled | Yes (default) | Yes (override) | Yes (create/close) | |
| Transcription enabled | Yes (default) | Yes (override) | Yes (start/stop) | |
| AI summaries enabled | Yes (default) | Yes (override) | No | |
| Virtual backgrounds allowed | Yes (default) | No | No | Org-wide policy |
| Plugins (whiteboard, doc sharing) | Yes (default) | Yes (override) | No | Via preset |
| RTMP livestream enabled | Yes (default) | Yes (override) | Yes (start/stop) | |
| Default locale | Yes (primary) | No | No | Org-wide only |
| Simulcast settings | Yes (default) | No | No | Org-wide, technical setting |
| Rate limits (joins/min) | Yes (primary) | No | No | Abuse protection, org-wide |
| Invite-only registration | Yes (primary) | No | No | Org-wide policy |
12.3 D1 Schema: `org_settings`
CREATE TABLE org_settings (
id TEXT PRIMARY KEY DEFAULT 'default', -- single-row table, always 'default'
org_name TEXT NOT NULL,
org_slug TEXT NOT NULL UNIQUE, -- URL-safe org identifier
-- Branding: Theme
theme TEXT NOT NULL DEFAULT 'dark', -- 'light' | 'dark' | 'darkest'
brand_color TEXT NOT NULL DEFAULT '#2563EB', -- hex, maps to brand.500
background_color TEXT, -- hex, maps to background.1000 (null = theme default)
text_color TEXT, -- hex (null = theme default)
text_on_primary_color TEXT, -- hex (null = auto)
video_bg_color TEXT, -- hex (null = theme default)
font_family TEXT, -- CSS font-family (null = system default)
google_font TEXT, -- Google Font name (null = use font_family)
border_width TEXT NOT NULL DEFAULT 'thin', -- 'none' | 'thin' | 'fat'
border_radius TEXT NOT NULL DEFAULT 'rounded', -- 'sharp' | 'rounded' | 'extra-rounded' | 'circular'
-- Branding: Assets
logo_url TEXT, -- R2 URL for org logo
logo_height INTEGER DEFAULT 32, -- px
favicon_url TEXT, -- R2 URL for favicon
custom_icon_pack TEXT, -- JSON blob for RTK icon pack (nullable)
-- Watermark
watermark_enabled INTEGER NOT NULL DEFAULT 0, -- boolean
watermark_image_url TEXT, -- R2 URL
watermark_position TEXT DEFAULT 'right bottom', -- 'left top' | 'right top' | 'left bottom' | 'right bottom'
watermark_width INTEGER DEFAULT 120, -- px
watermark_height INTEGER DEFAULT 40, -- px
-- Meeting Defaults
default_meeting_type TEXT NOT NULL DEFAULT 'video', -- 'video' | 'audio' | 'webinar' | 'livestream' (maps to RTK view_type: GROUP_CALL, AUDIO_ROOM, WEBINAR, WEBINAR+can_livestream)
default_waiting_room INTEGER NOT NULL DEFAULT 0, -- boolean
default_recording_autostart INTEGER NOT NULL DEFAULT 0, -- boolean
default_max_participants INTEGER NOT NULL DEFAULT 100,
default_guest_access INTEGER NOT NULL DEFAULT 1, -- boolean
default_chat_enabled INTEGER NOT NULL DEFAULT 1, -- boolean
default_polls_enabled INTEGER NOT NULL DEFAULT 1, -- boolean
default_breakout_rooms_enabled INTEGER NOT NULL DEFAULT 0, -- boolean (beta)
default_transcription_enabled INTEGER NOT NULL DEFAULT 0, -- boolean
default_ai_summaries_enabled INTEGER NOT NULL DEFAULT 0, -- boolean
default_plugins_enabled INTEGER NOT NULL DEFAULT 1, -- boolean
default_livestream_enabled INTEGER NOT NULL DEFAULT 0, -- boolean
-- Org Policies (not overridable per-meeting)
virtual_backgrounds_allowed INTEGER NOT NULL DEFAULT 1, -- boolean
guest_access_global INTEGER NOT NULL DEFAULT 1, -- boolean, master switch
invite_only_registration INTEGER NOT NULL DEFAULT 0, -- boolean
simulcast_enabled INTEGER NOT NULL DEFAULT 1, -- boolean
-- Recording Storage
recording_storage_provider TEXT NOT NULL DEFAULT 'r2', -- 'r2' | 'aws' | 'azure' | 'digitalocean' | 'gcs' | 'sftp'
recording_storage_config TEXT, -- JSON: bucket, region, credentials ref
-- i18n
default_locale TEXT NOT NULL DEFAULT 'en-US',
custom_strings TEXT, -- JSON blob for i18n overrides
-- Abuse Protection
rate_limit_joins_per_minute INTEGER NOT NULL DEFAULT 60,
max_concurrent_meetings INTEGER NOT NULL DEFAULT 50,
-- RTK App Config
rtk_app_id TEXT NOT NULL, -- RealtimeKit App ID
rtk_api_key_encrypted TEXT NOT NULL, -- Encrypted RTK API key
-- Metadata
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Trigger to auto-update updated_at
CREATE TRIGGER org_settings_updated_at
AFTER UPDATE ON org_settings
BEGIN
UPDATE org_settings SET updated_at = datetime('now') WHERE id = NEW.id;
END;12.4 Admin Settings UI Tabs
The /admin/settings page uses a tabbed layout:
| Tab | Settings Exposed | Maps to org_settings columns |
|---|---|---|
| **General** | Org name, slug | `org_name`, `org_slug` |
| **Branding** | Theme, colors, logo, favicon, fonts, borders, icon pack | `theme` through `custom_icon_pack` |
| **Meetings** | Default meeting type, waiting room, max participants, guest access | `default_meeting_type` through `default_livestream_enabled` |
| **Recording** | Auto-start default, storage provider, storage config, watermark | `default_recording_autostart`, `recording_storage_*`, `watermark_*` |
| **Features** | Chat, polls, breakout rooms, transcription, AI summaries, plugins, livestream, virtual backgrounds | Various `default_*` and policy columns |
| **Security** | Guest access master switch, invite-only registration, rate limits, max concurrent meetings | `guest_access_global`, `invite_only_registration`, `rate_limit_*`, `max_concurrent_meetings` |
| **Localization** | Default locale, custom strings | `default_locale`, `custom_strings` |
Each tab has a "Save" button that PATCHes only the changed fields. Changes to branding are previewed live in a side panel before saving.
12.5 Preset Management
RTK presets are managed via the RTK REST API, not stored in D1. The admin UI at /admin/presets is a CRUD interface over the RTK preset API:
- List presets:
GET /realtime/kit/apps/{appId}/presets - Create preset:
POST /realtime/kit/apps/{appId}/presets - Update preset:
PATCH /realtime/kit/{app_id}/presets/{preset_id} - Delete preset:
DELETE /realtime/kit/apps/{appId}/presets/{presetId}
Presets control per-participant permissions: media (camera, mic, screenshare), chat (public, private, file sharing), stage access, recording, plugins, host controls, and waiting room bypass.
The admin UI presents presets as role templates:
- Host: Full permissions (default for meeting creators)
- Participant: Standard permissions (camera, mic, chat, no host controls)
- Viewer: View-only (webinar audience, no media publishing)
- Guest: Restricted (no file sharing, no private chat, waiting room required)
Admins can clone and customize these or create new presets from scratch via a permission checklist UI or a raw JSON editor for advanced users.
Section 13: Future Features
Features listed here are NOT built in the initial release. Each has a clear trigger condition for when to build it.
13.1 Future Feature List
| Feature | Description | Build When |
|---|---|---|
| **Noise Cancellation** | Client-side audio processing (Krisp SDK or RNNoise WASM) | When user feedback shows audio quality is a top complaint, AND a viable client-side library is identified that works across web + mobile |
| **SIP/PSTN Interconnect** | Dial-in/dial-out via phone numbers | When RTK adds SIP support OR when a third-party SIP gateway (e.g., Twilio SIP trunking) can bridge to RTK meetings via a participant bot |
| **Per-Track Recording** | Individual audio/video track export via `POST /recordings/track` layers API | MOVED TO BUILD — API is documented. See Section 7.9. |
| **Mobile Apps (iOS/Android)** | Native mobile clients using RTK mobile UI Kits | After web app is feature-complete and stable. Start with iOS (Swift, UI Kit v0.5.7) as it's most mature mobile SDK |
| **Flutter / React Native Apps** | Cross-platform mobile clients | After native iOS/Android apps ship, IF the Flutter/RN SDKs reach v1.0 GA |
| **Realtime AI Agents** | Voice AI in meetings (STT -> LLM -> TTS via Durable Objects) | When Cloudflare consolidates `@cloudflare/realtime-agents` into the stable Agents SDK. Current SDK is experimental and will be replaced |
| **Advanced Analytics Dashboard** | Trends, cohort analysis, export, custom date ranges | After basic analytics (session count, participant count, duration) proves useful and users request deeper insights |
| **Calendar Integration** | Google Calendar / Outlook sync for scheduled meetings | When meeting scheduling is stable and users report friction from manual scheduling |
| **SSO / SAML** | Enterprise single sign-on | When enterprise customers require it. Implement via Cloudflare Access or direct SAML/OIDC integration |
| **Custom Recording Layouts** | Branded recording with custom grid layout, overlays | When users need recordings that look different from the default composite layout. Uses RTK Recording SDK (custom recording app) |
| **E2E Encryption** | End-to-end encrypted meetings | When RTK adds E2EE support (not currently available) |
| **Meeting Templates** | Pre-configured meeting setups (standup, all-hands, interview) | When hosts report repetitive meeting configuration as a pain point |
| **Webhook Integrations** | Slack/Teams/email notifications on meeting events | After webhook processing is stable and users request specific integrations |
| **API for Customers** | Public REST API for programmatic meeting management | When the product is used by teams that want to integrate meetings into their own apps |
| **White-Label Deployment App** | Self-service deployment tool for non-technical users | SEPARATE product. Full design in Section 14. Build after the main app is production-ready |
13.2 Prioritization Criteria
For any future feature, evaluate against these criteria before starting:
- Platform support: Does RTK provide the underlying capability? (If not, is a viable third-party integration available?)
- User demand: Have multiple users/orgs requested this?
- Effort vs. impact: Is the implementation effort proportional to the number of users it helps?
- SDK stability: For features depending on pre-GA SDKs, has the SDK reached v1.0?
- Dependencies: Are prerequisite features (e.g., basic analytics before advanced analytics) already built and stable?
13.3 Research Items (to resolve before deciding build/no-build)
| Item | Question to Answer | How to Research |
|---|---|---|
| ~~Per-track recording~~ | ~~RESOLVED~~ — `POST /recordings/track` API exists with layers system. Moved to BUILD. | N/A |
| RTMP/HLS setup | What configuration is needed to start an RTMP livestream? | Experiment with RTK SDK `RTKLivestream` API; check for RTMP URL/key configuration in REST API |
| Mobile UI Kit parity | Do mobile UI Kits have equivalent components to the web 136-component set? | Install iOS/Android UI Kit packages and enumerate available views/components |
| Recording SDK | How does the custom recording app work? Can we customize layout/watermark position? | Review RTK Recording SDK docs when available; test with sample implementation |
Section 14: Deployment App (Separate Product)
The Deployment App is a separate product from the collaboration platform. It runs on the vendor's Cloudflare account (Workers + D1 + R2) and enables non-technical customers to deploy the full RTK collaboration platform into their own Cloudflare account — with zero CLI usage, zero code, and zero Cloudflare expertise required.
14.1 Product Overview
| Aspect | Detail |
|---|---|
| **Target user** | Non-technical business user ("I want video meetings for my company") |
| **What customer provides** | A Cloudflare account + a scoped API token (we tell them exactly how to create it) |
| **What the app does** | 100% of deployment: creates Workers, D1, R2, KV, presets, webhooks, DNS — everything |
| **Post-deploy relationship** | Customer manages their platform directly. Comes back for updates, health checks, token rotation |
| **Tech stack** | CF Workers + D1 + R2 (same as the platform itself) |
| **Billing** | Deferred — not included in this design phase |
| **Deployments per account** | One (multi-deployment support designed for later) |
| **Update mechanism** | TBD — requires separate dedicated design session. UI surface placeholder included |
14.2 Authentication System
D1-backed sessions (not JWT — need server-side revocation for security).
Database schema (vendor D1):
CREATE TABLE deploy_users (
id TEXT PRIMARY KEY, -- ulid
email TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
password_hash TEXT NOT NULL, -- argon2id via @noble/hashes
email_verified INTEGER NOT NULL DEFAULT 0,
verification_token TEXT,
verification_expires_at TEXT,
reset_token TEXT,
reset_expires_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE deploy_sessions (
id TEXT PRIMARY KEY, -- random 32-byte hex
user_id TEXT NOT NULL REFERENCES deploy_users(id),
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_sessions_user ON deploy_sessions(user_id);
CREATE INDEX idx_sessions_expires ON deploy_sessions(expires_at);
CREATE TABLE deployments (
id TEXT PRIMARY KEY, -- ulid
user_id TEXT NOT NULL REFERENCES deploy_users(id),
cf_account_id TEXT NOT NULL,
org_name TEXT NOT NULL,
org_slug TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
CHECK(status IN ('pending','deploying','active','failed','suspended','deleted')),
worker_name TEXT NOT NULL,
custom_domain TEXT,
d1_database_id TEXT,
r2_branding_bucket TEXT,
r2_recordings_bucket TEXT,
r2_exports_bucket TEXT,
kv_sessions_id TEXT,
kv_rate_limits_id TEXT,
kv_meeting_state_id TEXT,
kv_cache_id TEXT,
rtk_app_id TEXT,
last_health_check TEXT,
health_status TEXT DEFAULT 'unknown',
deployed_version TEXT,
-- Pre-deploy configuration (applied to platform at deploy time)
config_brand_color TEXT DEFAULT '#0055FF',
config_guest_access INTEGER DEFAULT 1,
config_recording_policy TEXT DEFAULT 'host_decides',
config_default_meeting_type TEXT DEFAULT 'video',
config_waiting_room INTEGER DEFAULT 1,
config_transcription INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_deployments_user ON deployments(user_id);Auth flows:
- Registration: email + password + display name → argon2id hash → email verification (UNVERIFIED: Mailchannels dropped free Workers integration — evaluate Resend, AWS SES, or Cloudflare Email Routing as alternatives)
- Login: verify password → create D1 session → set
HttpOnly; Secure; SameSite=Strictcookie (7-day expiry) - Password reset: token-based, 1-hour expiry, invalidates all sessions on reset
- OAuth: Deferred. UI has reserved slot ("or" divider) for Google OAuth later
- Rate limiting: login attempts via KV (
rl:login:{email}:{minute})
14.3 Token Validation Pipeline
The most critical onboarding step. Customer pastes their CF API token; we validate it has exact required permissions.
Required token permissions:
| Permission Group | Permission | Level | Why |
|---|---|---|---|
| Account — Workers Scripts | Workers Scripts | Edit | Upload Worker code |
| Account — Workers KV Storage | Workers KV Storage | Edit | Create KV namespaces |
| Account — Workers R2 Storage | Workers R2 Storage | Edit | Create R2 buckets |
| Account — D1 | D1 | Edit | Create database, run migrations |
| Account — Account Settings | Account Settings | Read | Verify account access |
| Account — Cloudflare RealtimeKit | RealtimeKit | Edit | Create apps, presets, webhooks |
| Zone — Zone *(optional)* | Zone | Read | List zones for custom domain setup (Workers custom domains handle DNS automatically — no DNS Edit needed) |
Validation sequence:
- Verify token:
GET /user/tokens/verify→ confirms token is active - Get account:
GET /accounts→ extract account_id - Permission probes (7 parallel GET requests):
GET /accounts/{id}/workers/scripts→ WorkersGET /accounts/{id}/d1/database→ D1GET /accounts/{id}/r2/buckets→ R2GET /accounts/{id}/storage/kv/namespaces→ KVGET /accounts/{id}→ Account SettingsGET /accounts/{id}/realtime/kit/apps→ RealtimeKitGET /zones?account.id={id}→ Zone (optional, for custom domain)
- Store token: AES-256-GCM encrypted in vendor D1. Never stored in plaintext. Never sent to frontend after validation.
14.4 Deployment Pipeline
Creates all infrastructure in the customer's CF account. ~15-30 seconds total.
Resource inventory per deployment:
| Resource | Count | Names |
|---|---|---|
| D1 Database | 1 | `rtk-platform` |
| R2 Buckets | 3 | `{slug}-branding`, `{slug}-recordings`, `{slug}-exports` |
| KV Namespaces | 4 | `rtk-sessions`, `rtk-rate-limits`, `rtk-meeting-state`, `rtk-cache` |
| Workers | 2 | `rtk-platform-{slug}` (main) + `rtk-updater-{slug}` (update/rollback, never self-updates) |
| Worker Secrets | 6 | `ACCOUNT_ID`, `RTK_API_TOKEN`, `JWT_SECRET`, `SENTRY_DSN_VENDOR`, `SENTRY_DSN_CUSTOMER`, `SENTRY_RELEASE` |
| RTK App | 1 | Created via RTK REST API |
| RTK Presets | 14 | Per Section 5.2 (4 meeting types × 3-4 roles each) |
| RTK Webhook | 1 | Points to Worker's `/webhooks/rtk` |
| Custom Domain | 0-1 | Optional |
Pipeline phases:
Phase 1: Create Storage (parallel, ~2-4s)
8 independent API calls run simultaneously:
| Call | Endpoint | Body |
|---|---|---|
| D1 database | `POST /accounts/{id}/d1/database` | `{ "name": "rtk-platform" }` |
| R2 branding | `POST /accounts/{id}/r2/buckets` | `{ "name": "{slug}-branding" }` |
| R2 recordings | `POST /accounts/{id}/r2/buckets` | `{ "name": "{slug}-recordings" }` |
| R2 exports | `POST /accounts/{id}/r2/buckets` | `{ "name": "{slug}-exports" }` |
| KV sessions | `POST /accounts/{id}/storage/kv/namespaces` | `{ "title": "rtk-sessions" }` |
| KV rate limits | `POST /accounts/{id}/storage/kv/namespaces` | `{ "title": "rtk-rate-limits" }` |
| KV meeting state | `POST /accounts/{id}/storage/kv/namespaces` | `{ "title": "rtk-meeting-state" }` |
| KV cache | `POST /accounts/{id}/storage/kv/namespaces` | `{ "title": "rtk-cache" }` |
Phase 2: Deploy Worker (sequential, ~5-10s, needs Phase 1 IDs)
Upload Worker with bindings:
PUT /accounts/{id}/workers/scripts/{worker_name} Content-Type: multipart/form-dataWorker code is pre-built and stored in vendor R2. Bindings injected dynamically from Phase 1 resource IDs.
Set secrets (6 sequential PUT calls):
PUT /accounts/{id}/workers/scripts/{worker_name}/secretsSets:
ACCOUNT_ID,RTK_API_TOKEN,JWT_SECRET,SENTRY_DSN_VENDOR,SENTRY_DSN_CUSTOMER,SENTRY_RELEASEEnable workers.dev subdomain:
POST /accounts/{id}/workers/scripts/{worker_name}/subdomainRun D1 schema migration:
POST /accounts/{id}/d1/database/{db_id}/queryExecutes complete schema from Section 2.3.
Phase 3: Configure RTK (mostly parallel, ~4-8s)
- Create RTK App:
POST /accounts/{id}/realtime/kit/apps - Create 14 presets (parallel):
POST /accounts/{id}/realtime/kit/{app_id}/presets— per Section 5.2 - Register webhook:
POST /accounts/{id}/realtime/kit/{app_id}/webhooks - Seed org_settings + Create Owner user: via D1 query, using customer's pre-deploy config choices
Phase 4: Custom Domain (optional)
If customer has a zone on Cloudflare:
PUT /accounts/{id}/workers/domains
Body: { "hostname": "meet.example.com", "service": "{worker_name}", "zone_id": "{zone_id}" }Progress reporting: Server-Sent Events (SSE) stream real-time step completion to the frontend. Falls back to polling.
Rollback on failure: Every created resource is tracked. On failure, delete in reverse order. Deployments are idempotent — retry creates only missing resources.
Estimated timing:
| Phase | Description | Time |
|---|---|---|
| Phase 1 | Create storage (8 parallel) | 2-4s |
| Phase 2 | Deploy Worker + secrets + schema | 5-10s |
| Phase 3 | RTK app + 14 presets + webhook + seed | 4-8s |
| Phase 4 | Custom domain (optional) | 1-5s |
| **Total** | **12-27s** |
14.5 Health Check System
On-demand: Vendor Worker calls deployed Worker's /api/diagnostics/health:
| Check | Method | Pass Criteria |
|---|---|---|
| D1 connectivity | `SELECT 1` | No error |
| R2 connectivity | `HEAD` on branding bucket | 200 or 404 |
| KV connectivity | `GET` test key | No error |
| RTK API | `GET /realtime/kit/{app_id}/meetings?limit=1` | 200 |
Periodic: Deployed Worker runs health checks every 30 minutes via cron trigger. Vendor Worker iterates all active deployments every 30 minutes as well. Alert email after 3 consecutive failures.
14.6 User Flows
Flow 1: Registration
Landing page → Sign up (email + password) → Email verification → First login → Redirect to onboarding wizard.
Flow 2: Onboarding Wizard (5 steps)
Step 1 — Welcome: Explains what the app does, what they need (CF account + 5 minutes), what gets created.
Step 2 — Connect Cloudflare: Step-by-step visual guide to create a CF API token with exact permissions. Token input field (masked). Real-time validation with per-permission checkmarks. This is the hardest step — needs clear, non-intimidating instructions with expandable "Learn more" sections.
Step 3 — Configure Platform: Pre-deployment configuration (all changeable later on their platform):
| Setting | Input Type | Default | Description |
|---|---|---|---|
| Organization name | Text | Required | Shown in platform UI and meeting links |
| Domain | Domain selector (see below) | Required | Default: subdomain on user's zone. Fallback: `{slug}.workers.dev` |
| Primary brand color | Color picker | `#0055FF` | Applied to platform UI |
| Logo | File upload | Skip | Optional, changeable later |
| Default meeting type | Dropdown | Video | Pre-selected option when host creates a new meeting. Host can always change per-meeting. |
| Guest access | Toggle | On | Allow link-based access without accounts |
| Waiting room | Toggle | On | Require host approval for guests |
| Recording policy | Dropdown | Host Decides | Always / Host Decides / Never |
| Transcription | Toggle | Off | Auto-transcribe recordings |
Domain selector UX (Step 3):
- If user's token has Zone Read permission, fetch their zones via
GET /zones. Show a dropdown of available domains. - Default to subdomain mode: user picks a zone, enters a subdomain (e.g.,
meet.example.com). This is the recommended and most common option. - Option to use root domain: if selected, show a warning — "Using your root domain (example.com) will route all traffic to Castio. If you have an existing website or application at this domain, it will stop working. Only use a root domain if it's dedicated to Castio."
- If no zones available (token lacks Zone Read): fall back to
{slug}.workers.devwith a note that custom domains can be configured later. - Workers custom domains handle DNS automatically — no additional DNS configuration needed from the user.
Step 4 — Review & Deploy: Summary of all config. Edit links back to each step. "Deploy Platform" button.
Step 5 — Deploying: Real-time progress tracker (see Section 14.4). Continues server-side if tab is closed. On success: celebration animation, platform URL, "Go to Dashboard" button.
Flow 3: Post-Deploy Dashboard
Status card (healthy/degraded/unhealthy), quick stats (meetings, users, recordings), quick actions (open platform, view deployment), activity feed.
Flow 4: Error Recovery
- Deploy failed → retry from failed stage (idempotent)
- Token invalid/revoked → banner warning, platform keeps running, management blocked
- Token expiring → 30/7/1-day advance warnings
- Platform unhealthy → per-service diagnostics with suggested actions
14.7 Page & Route Map
Public Pages (no auth)
| Route | Page | Purpose |
|---|---|---|
| `/` | Landing | Product explanation, CTA to register |
| `/login` | Login | Email + password (OAuth slot reserved) |
| `/register` | Register | Email + password + display name |
| `/verify-email` | Email Verification | Pending verification + token confirmation |
| `/forgot-password` | Forgot Password | Email input to request reset |
| `/reset-password/:token` | Reset Password | New password form |
Onboarding Wizard (auth required, full-screen)
| Route | Step | Title |
|---|---|---|
| `/onboarding/welcome` | 1 | Welcome — what you need |
| `/onboarding/connect` | 2 | Connect Cloudflare — token setup |
| `/onboarding/configure` | 3 | Configure Platform — pre-deploy settings |
| `/onboarding/review` | 4 | Review & Deploy — summary |
| `/onboarding/deploying` | 5 | Deploying — live progress |
Authenticated Pages
| Route | Page | Purpose |
|---|---|---|
| `/dashboard` | Dashboard | Post-deploy home: health, stats, quick actions |
| `/deployment` | Deployment Detail | Health checks per service, infrastructure inventory, deploy history, danger zone |
| `/help` | Help & Docs | FAQ, troubleshooting, support contact |
Settings
| Route | Page | Purpose |
|---|---|---|
| `/settings/account` | Account | Profile, password, sessions, delete account |
| `/settings/cloudflare` | CF Connection | Token status, re-validation, token update |
| `/settings/platform` | Platform Config | Org name, branding, domain, meeting defaults, diagnostics |
| `/settings/notifications` | Notifications | Email alerts for health changes, token expiry, updates |
| `/settings/updates` | Updates | Current version, changelog, update button (mechanism TBD) |
Utility
| Route | Page |
|---|---|
| `*` | 404 Not Found |
| `/error` | Generic Error |
Total: 22 routes
14.8 Component Architecture
Layout Components
| Component | Description |
|---|---|
| `AppShell` | Authenticated layout: sidebar + topbar + content slot |
| `PublicLayout` | Unauthenticated layout: topbar + centered content |
| `Sidebar` | Nav links with icons, collapsible on mobile, health status dot |
| `Topbar` | Logo, breadcrumbs, notification bell, avatar dropdown |
| `WizardShell` | Full-screen onboarding layout with step indicator |
Feedback & Status
| Component | Description |
|---|---|
| `Toast` | Non-blocking notification (success/error/info/warning), auto-dismiss |
| `AlertBanner` | Page-level persistent alert, dismissible or sticky |
| `StatusBadge` | Colored pill: healthy/degraded/down/pending |
| `EmptyState` | Icon + message + CTA for empty data |
| `LoadingSkeleton` | Animated placeholder for cards/tables |
| `DeploymentProgress` | Vertical step tracker with live status per step |
Data Display
| Component | Description |
|---|---|
| `StatCard` | Metric: label + value + optional trend |
| `DataTable` | Sortable, paginated table with loading/empty/error states |
| `ActivityItem` | Icon + text + relative timestamp |
| `HealthCheckRow` | Service name + latency + status dot |
| `KeyValueList` | Label-value pairs for deployment info |
Forms & Inputs
| Component | Description |
|---|---|
| `FormField` | Label + input + help text + error wrapper |
| `TextInput` | Standard input with validation states |
| `PasswordInput` | Input with show/hide toggle + strength indicator |
| `TokenInputForm` | Masked input + validate button + permission checklist |
| `SelectDropdown` | Styled select |
| `Toggle` | On/off switch |
| `ColorPicker` | Color swatch for branding |
| `FileUpload` | Drag-and-drop with preview (logo) |
| `CopyableField` | Read-only with copy-to-clipboard |
Actions & Containers
| Component | Description |
|---|---|
| `PrimaryButton` | Main action with loading state |
| `DangerButton` | Red-styled for destructive actions |
| `Card` | Bordered container with optional header/body/footer |
| `CollapsibleCard` | Expandable card (progressive disclosure) |
| `DangerZone` | Red-bordered section for destructive actions |
Modals
| Modal | Trigger | Purpose |
|---|---|---|
| `ConfirmDeployModal` | Deploy/Redeploy button | Confirm before creating infrastructure |
| `ConfirmDestroyModal` | "Destroy Deployment" | Typed confirmation to delete all resources |
| `ConfirmDeleteAccountModal` | "Delete Account" | Typed confirmation |
| `TokenUpdateModal` | "Update Token" | Token input with permission diff |
| `RedeployRequiredModal` | Config save needing redeploy | Choose "Save & Redeploy" or "Save Only" |
| `SessionExpiredModal` | 401 response | "Sign In Again" prompt |
14.9 Responsive Design
| Breakpoint | Behavior |
|---|---|
| Desktop (>1024px) | Full sidebar + content area |
| Tablet (768-1024px) | Collapsible sidebar, full functionality |
| Mobile (<768px) | Hamburger menu, read-only dashboard + status check. No deployment or settings changes (too complex for small screens) |
14.10 Cross-Cutting Concerns
Loading states: Skeleton screens for dashboard cards. Inline spinners for async ops (token validation, health checks). Full-page loader only on initial app load.
Empty states: No deployment → "Complete setup to deploy" CTA. No activity → "No recent activity." No stats → "Hold your first meeting to see data."
Error handling pattern: Plain-language error messages for non-technical users. Technical details hidden behind expandable "Show details" sections. Every error has a clear action: retry, update token, contact support.
Security: Tokens encrypted at rest (AES-256-GCM). Session cookies are HttpOnly + Secure + SameSite=Strict. Customer's CF API token used server-side only, never sent to browser after validation. Customer can revoke their token from CF dashboard at any time — running platform is unaffected (Workers run independently of the API token).
14.11 Update & Rollback Mechanism
Updates are manual, initiated by the Owner role from their deployed platform (not from the vendor's deployment app). The deployed platform self-updates using the customer's own CF API token.
Architecture: Two Workers
| Worker | Purpose | Self-updates? |
|---|---|---|
| `rtk-platform-{slug}` | Main platform Worker | Yes (code is replaced during updates) |
| `rtk-updater-{slug}` | Tiny updater Worker | **Never** — always stable, can recover a bricked main Worker |
The updater Worker is deployed alongside the main Worker during initial deployment (Phase 2). It has the same CF API token and D1/KV bindings. Its only job: update or rollback the main Worker.
Version Manifest (Vendor R2, public read)
The vendor publishes version manifests to a public R2 bucket:
{
"latest": "1.2.3",
"minimum_supported": "1.0.0",
"released_at": "2026-03-06T00:00:00Z",
"changelog_url": "https://updates.vendor.com/changelog/1.2.3.md",
"bundle_url": "https://updates.vendor.com/bundles/1.2.3.bundle.js",
"migrations": ["0005_add_column.sql", "0006_add_index.sql"],
"sha256": "abc123...",
"breaking": false,
"notes": "Bug fixes and performance improvements"
}Update Check (Automatic)
Main Worker cron trigger checks for updates hourly:
[triggers]
crons = ["0 * * * *"] # every hourFetches manifest, compares latest against platform:current_version in KV. If newer version exists, stores manifest in platform:available_update KV key. Owner sees "Update Available" badge in their platform dashboard.
Update Flow (Manual, Owner-initiated)
Owner clicks "Update Now" in their platform settings. Request goes to the updater Worker:
- Download new bundle from vendor R2. Verify SHA-256 checksum.
- Save rollback info to KV:
rollback:{version}:previous_version= current Worker version ID (fromGET /deployments)rollback:{version}:bookmark= current D1 bookmark (fromGET /d1/{db_id}/time_travel/bookmark)
- Upload new Worker version (does NOT deploy yet):
POST /accounts/{id}/workers/scripts/{worker_name}/versions - Run D1 migrations (additive-only — see migration strategy below):
POST /accounts/{id}/d1/database/{db_id}/query - Deploy new version:
POST /accounts/{id}/workers/scripts/{worker_name}/deployments Body: { "strategy": "percentage", "versions": [{ "version_id": "{new}", "percentage": 100 }] } - Update KV:
platform:current_version= new version - Health check: Hit main Worker's
/api/diagnostics/health. If unhealthy, auto-rollback.
Rollback Flow (Owner-initiated or automatic)
Owner clicks "Roll Back" in settings, or auto-triggered if post-update health check fails. Request goes to the updater Worker:
- Read rollback info from KV: previous version ID + pre-migration D1 bookmark
- Redeploy previous Worker version:
POST /accounts/{id}/workers/scripts/{worker_name}/deployments Body: { "strategy": "percentage", "versions": [{ "version_id": "{previous}", "percentage": 100 }] } - Schema: No D1 rollback needed (additive-only migrations — old code ignores new columns)
- Emergency D1 restore (manual, only if data is corrupted):Warning: This loses all data written after the bookmark. Owner must confirm.
POST /accounts/{id}/d1/database/{db_id}/time_travel/restore?bookmark={saved_bookmark}
D1 Migration Strategy: Additive-Only
| Allowed | Not Allowed |
|---|---|
| ADD COLUMN (nullable or with default) | DROP COLUMN |
| ADD TABLE | DROP TABLE |
| ADD INDEX | RENAME COLUMN |
| INSERT seed data | ALTER COLUMN type destructively |
Why: Old Worker versions must work against newer schemas. When rolling back code, the database keeps its current schema. Old code simply ignores columns/tables it doesn't know about.
Migration tracking: _migrations table in each deployed D1:
CREATE TABLE _migrations (
version INTEGER PRIMARY KEY,
name TEXT NOT NULL,
applied_at TEXT NOT NULL DEFAULT (datetime('now')),
platform_version TEXT NOT NULL
);Before running migrations, check SELECT MAX(version) FROM _migrations. Run only unapplied migrations in order.
D1 bookmark: Saved before every migration batch as insurance. 30-day retention. Used only for emergency recovery, not routine rollback.
Cloudflare Rollback Capabilities (Reference)
| Feature | Detail |
|---|---|
| Worker version retention | **100 versions** available for rollback |
| Rollback speed | Instant (seconds for global propagation) |
| Rollback scope | Code + config only. **Secrets, bindings, KV, D1, R2 are NOT rolled back.** |
| Gradual rollouts | Can split traffic between two versions (canary deployment) |
| D1 Time Travel | **30-day retention**, always on, no extra cost |
| D1 restore | Atomic. Returns `previous_bookmark` for undo. All post-restore writes are lost. |
| R2 versioning | None. Use versioned key prefixes (`assets/v1.2.3/`) |
| KV versioning | None. Use versioned keys |
| Secrets | Script-level, not version-level. Persist across rollbacks. |
API Endpoints for Update System
| Purpose | Method | Path |
|---|---|---|
| List Worker versions | GET | `/accounts/{id}/workers/scripts/{name}/versions` |
| Upload new version | POST | `/accounts/{id}/workers/scripts/{name}/versions` |
| List deployments | GET | `/accounts/{id}/workers/scripts/{name}/deployments` |
| Create deployment (rollback) | POST | `/accounts/{id}/workers/scripts/{name}/deployments` |
| Get D1 bookmark | GET | `/accounts/{id}/d1/database/{db_id}/time_travel/bookmark` |
| Restore D1 | POST | `/accounts/{id}/d1/database/{db_id}/time_travel/restore` |
| Run D1 query (migrations) | POST | `/accounts/{id}/d1/database/{db_id}/query` |
UI Surface (Owner's Platform Settings)
- Current version:
v1.2.3with release date - Update available banner: version number + changelog summary + "Update Now" button
- Changelog viewer: Rendered markdown from vendor manifest
- Update progress: Step tracker (download → save rollback point → upload → migrate → deploy → health check)
- Rollback button: "Roll Back to v1.2.2" with confirmation dialog
- Emergency D1 restore: Hidden under "Advanced" — requires typed confirmation, warns about data loss
- Update history: Table of past updates (version, date, status: success/failed/rolled-back)
14.12 Deployment App API Routes
| Method | Route | Auth | Purpose |
|---|---|---|---|
| POST | `/api/auth/register` | No | Create account |
| POST | `/api/auth/login` | No | Login |
| POST | `/api/auth/logout` | Yes | Logout |
| GET | `/api/auth/verify` | No | Verify email token |
| POST | `/api/auth/forgot-password` | No | Request password reset |
| POST | `/api/auth/reset-password` | No | Complete password reset |
| GET | `/api/auth/me` | Yes | Get profile |
| PATCH | `/api/auth/profile` | Yes | Update profile |
| POST | `/api/auth/change-password` | Yes | Change password |
| GET | `/api/auth/sessions` | Yes | List active sessions |
| DELETE | `/api/auth/sessions` | Yes | Sign out all other sessions |
| POST | `/api/deploy/validate-token` | Yes | Validate CF API token + permissions |
| GET | `/api/deploy/check-subdomain/:name` | Yes | Check subdomain availability |
| POST | `/api/deploy/start` | Yes | Trigger deployment pipeline |
| GET | `/api/deploy/status` | Yes | Get deployment health + status |
| GET | `/api/deploy/status/stream` | Yes | SSE stream for deployment progress |
| GET | `/api/deploy/infrastructure` | Yes | List deployed CF resources |
| GET | `/api/deploy/history` | Yes | Deployment history |
| GET | `/api/deploy/stats` | Yes | Quick stats (proxied from platform) |
| GET | `/api/deploy/token-status` | Yes | Current token validity |
| PUT | `/api/deploy/token` | Yes | Update CF API token |
| GET | `/api/deploy/config` | Yes | Get platform configuration |
| PATCH | `/api/deploy/config` | Yes | Update platform configuration |
| POST | `/api/deploy/redeploy` | Yes | Trigger redeployment |
| POST | `/api/deploy/destroy` | Yes | Destroy all deployed resources |
| POST | `/api/deploy/health-check` | Yes | Trigger manual health check |
| GET | `/api/deploy/version` | Yes | Current deployed version |
| GET | `/api/deploy/updates` | Yes | Available updates |
| GET | `/api/activity` | Yes | Recent activity log |
| GET | `/api/settings/notifications` | Yes | Notification preferences |
| PATCH | `/api/settings/notifications` | Yes | Update notification preferences |
14.13 Cloudflare API Endpoints Used
Core deployment (16 endpoints):
| Purpose | Method | Path |
|---|---|---|
| Verify token | GET | `/user/tokens/verify` |
| List accounts | GET | `/accounts` |
| Create D1 database | POST | `/accounts/{id}/d1/database` |
| Query D1 | POST | `/accounts/{id}/d1/database/{db_id}/query` |
| Delete D1 | DELETE | `/accounts/{id}/d1/database/{db_id}` |
| Create R2 bucket | POST | `/accounts/{id}/r2/buckets` |
| Delete R2 bucket | DELETE | `/accounts/{id}/r2/buckets/{name}` |
| Create KV namespace | POST | `/accounts/{id}/storage/kv/namespaces` |
| Delete KV namespace | DELETE | `/accounts/{id}/storage/kv/namespaces/{ns_id}` |
| Upload Worker | PUT | `/accounts/{id}/workers/scripts/{name}` |
| Delete Worker | DELETE | `/accounts/{id}/workers/scripts/{name}` |
| Set Worker secret | PUT | `/accounts/{id}/workers/scripts/{name}/secrets` |
| Enable subdomain | POST | `/accounts/{id}/workers/scripts/{name}/subdomain` |
| Attach custom domain | PUT | `/accounts/{id}/workers/domains` |
| Detach custom domain | DELETE | `/accounts/{id}/workers/domains/{domain_id}` |
| Patch Worker settings | PATCH | `/accounts/{id}/workers/scripts/{name}/settings` |
RealtimeKit (8 endpoints):
| Purpose | Method | Path |
|---|---|---|
| Create app | POST | `/accounts/{id}/realtime/kit/apps` |
| Create preset | POST | `/accounts/{id}/realtime/kit/{app_id}/presets` |
| Update preset | PATCH | `/accounts/{id}/realtime/kit/{app_id}/presets/{preset_id}` |
| Delete preset | DELETE | `/accounts/{id}/realtime/kit/{app_id}/presets/{preset_id}` |
| Add webhook | POST | `/accounts/{id}/realtime/kit/{app_id}/webhooks` |
| Update webhook | PATCH | `/accounts/{id}/realtime/kit/{app_id}/webhooks/{wh_id}` |
| Delete webhook | DELETE | `/accounts/{id}/realtime/kit/{app_id}/webhooks/{wh_id}` |
| List meetings | GET | `/accounts/{id}/realtime/kit/{app_id}/meetings` |
Zone (optional, 1 endpoint — Workers custom domains handle DNS automatically):
| Purpose | Method | Path |
|---|---|---|
| List zones | GET | `/zones?account.id={id}` |