Castio — Platform Design Document

Date: 2026-03-06 Status: Draft — pending user approval


Table of Contents

  1. Product Overview
  2. System Architecture
  3. Diagnostics & Observability
  4. User System & Authentication
  5. Role-to-Preset Mapping
  6. Meeting Lifecycle
  7. RTK Feature Mapping
  8. Post-Meeting Experience
  9. Webhook & Analytics Pipeline
  10. App Pages & Navigation
  11. Branding & Theming
  12. Organization Configuration
  13. Future Features
  14. 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

1.3 What RealtimeKit Handles (We Do NOT Rebuild)

CapabilityRTK ComponentOur Role
Media routing (video/audio/screen share)SFU + Core SDKWire up SDK, expose in UI
Participant state & permissionsPresets + REST APICreate presets matching our roles, call REST API
Meeting lifecycle (join/leave/active/inactive)REST API + SDK eventsProxy REST calls, listen to SDK events
Chat (public + private DMs + pin)Core SDK + UI KitRender `rtk-chat` components
PollsCore SDK + UI KitRender `rtk-polls` components
Stage management (webinars)Presets + UI KitRender stage components, configure presets
Waiting roomsPresets + UI KitConfigure preset, render waiting screen
Breakout roomsConnected Meetings APIRender `rtk-breakout-rooms-manager` (web only, beta)
Recording (composite)REST API + Recording SDKStart/stop via API, configure cloud storage
Transcription (Whisper Large v3 Turbo)AI feature + webhooksEnable via preset, display `rtk-ai-transcriptions`
AI meeting summariesAI feature + webhooksConsume `meeting.summary` webhook
Virtual backgrounds`@cloudflare/realtimekit-virtual-background`Include addon
Plugins (whiteboard, doc sharing)Plugin system + UI KitEnable via preset
SimulcastCore SDK configSet simulcast options
TURN relay (ICE fallback)Managed by RTK internallyTransparent — no developer action needed
Message broadcastingServer-side APICall from Worker when needed
Collaborative storesServer-side APIUse for custom real-time state sync

1.4 What We Build (Not in RTK)

FeatureWhy CustomStorage
**User system** (accounts, auth, roles)RTK has participants, not usersD1
**Meeting scheduling** (instant, scheduled, recurring, permanent rooms)RTK meetings have no time conceptD1
**Post-meeting experience** (recording playback, transcript viewer, AI summary display)RTK stores data 7 days, we persist referencesD1 + 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 analyticsD1 (webhook-fed + RTK analytics API)
**Diagnostics & observability** (error tracking, health checks, Sentry)Enterprise requirement, not in RTKSentry + Workers Observability + D1 (2 tables)
**Org configuration** (settings, branding, policies)Platform-level concernD1 + R2
**Abuse protection** (rate limiting, capacity caps, guest controls)Platform-level concernKV + 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 RoleRTK Preset TypeKey Permissions
**Owner**Video + all host controlsFull admin: kick, mute others, manage stage, create polls, manage breakouts
**Admin**Video + all host controlsSame as Owner (platform-level distinction only — RTK treats identically)
**Host**Video + host controlsKick, mute others, manage stage, admit from waiting room
**Member**Video + standardAudio, video, screen share, chat, polls, waiting room bypass
**Guest**Video + restrictedAudio only, public chat only, no screen share, must go through waiting room
**Webinar Viewer**Webinar + viewerView stage, public chat, polls — no media publishing
**Webinar Panelist**Webinar + stageCan be added to stage by host, audio/video when on stage
**Livestream Viewer**Livestream + viewerHLS playback only, chat

1.6 Scope Boundaries

In scope (v1, web-first):

Low priority (groundwork only):

Needs research:

Future / 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

MethodRoutePurpose
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

MethodRoutePurpose
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

MethodRoutePurpose
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

MethodRoutePurpose
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

MethodRoutePurpose
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

MethodRoutePurpose
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

MethodRoutePurpose
POST`/webhooks/rtk`Receive all RTK webhook events

Agent Routes (Groundwork Only)

MethodRoutePurpose
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`

sql
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`

sql
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`

sql
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`

sql
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`

sql
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`

sql
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`

sql
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`

sql
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.

sql
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.

2.4 R2 Storage

BucketContentsAccess Pattern
`{org}-branding`Logos, custom backgrounds, faviconRead 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 downloadsWrite on demand, read once

2.5 KV Namespaces

NamespaceKey PatternValueTTL
`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 responses30-300s depending on resource

2.6 Durable Objects (Groundwork Only)

Reserved for Realtime Agents. Not built in v1 but wrangler config includes the binding:

jsonc
// 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

jsonc
// 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

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 components

2.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.

The ultimate test: when a customer says "my meeting was laggy" or "I couldn't join" — we must be able to look up that specific meeting, that specific user, and see exactly what happened without ever asking the customer to reproduce the problem on a screenshare.

We achieve this by layering four systems that each do what they're best at:

LayerToolWhat it handlesWhy not something else
**Error tracking & investigation**Sentry (`@sentry/cloudflare` + `@sentry/react`)Error capture, deduplication, grouping, stack traces, release tracking, alertingPurpose-built for this; replacing it with D1 tables would be rebuilding Sentry poorly
**Request tracing & I/O monitoring**Cloudflare Workers ObservabilityAuto-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.
**Post-meeting quality investigation**RTK Peer Report API (`GET /sessions/peer-report/{peer_id}`)Per-participant quality stats (MOS, jitter, packet loss, RTT), device info, ICE candidates, TURN usage, IP geolocation, timestamped eventsRTK already captures this server-side for every participant — we just query it on demand
**Operational data we own**D1 tables (2 tables)Webhook delivery tracking, health check snapshotsOnly we know about webhook processing flows and health probe results — no external tool captures this

What we do NOT build:

3.2 Sentry Integration

Sentry is the backbone of error tracking. Two SDKs, two contexts, one Sentry project.

Packages & Versions

DSN Ownership Model

Each deployment has up to two Sentry DSNs:

DSNSecret NamePurposeDefault
**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:

Sentry project structure (vendor side):

Worker-Side Init

typescript
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):

SPA-Side Init

typescript
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).

typescript
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)

TagValuePurpose
`deployment_id`Unique per deploymentFilter vendor Sentry by deployment
`org_id`Org identifierGroup errors by customer org
`user_id`Platform user ID"Show me all errors for user X" — set via `Sentry.setUser({ id })`
`participant_id`RTK participant/peer ID (set on meeting join)Cross-reference with peer-report API for quality investigation
`session_id`RTK session ID (set on meeting join)Correlate errors to specific meeting sessions
`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

bash
# 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/

What Gets Reported to Sentry

SourceWhatAutomatic?
Worker unhandled exceptionsAll crashes, binding errors, OOMYes (`withSentry` catches them)
Worker `fetch()` failuresRTK API 4xx/5xx, timeout, network errorsYes (Workers Observability traces + Sentry captures thrown errors)
Webhook handler failuresProcessing errors, invalid payloadsYes (thrown errors within `withSentry`)
D1/KV/R2 failuresQuery errors, constraint violations, permission errorsYes (thrown errors)
SPA React error boundariesComponent crashesYes (`Sentry.ErrorBoundary` wraps RTK UI Kit components)
SPA RTK SDK errorsSee section 3.5 belowManual (event listeners → `Sentry.captureException`)
SPA network errorsFailed API calls to our WorkerYes (`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

toml
# 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:

  1. Workers Logs — all console.log/warn/error calls are collected, queryable in Cloudflare dashboard, searchable
  2. Workers Traces — automatic OpenTelemetry-compliant traces for every fetch(), KV get/put, R2 get/put, D1 prepare/run, and Durable Object calls. Shows latency, status, and call hierarchy per request.
  3. 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.

One exception: Workers Observability captures URL, method, status code, and latency — but NOT response bodies. Our RTK API client wrapper adds error-response logging so we know why a call failed, not just that it did:

typescript
// In the RTK API client wrapper (auth/retries layer)
if (!res.ok) {
  const body = await res.text();
  console.error(JSON.stringify({
    rtk_api_error: { method, endpoint, status: res.status, body: body.slice(0, 500) }
  }));
  // Workers Observability captures this console.error, making it searchable
}

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:

toml
# 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/HTTP

Note: 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)

FeatureFreeWorkers Paid
Logs200K events/day, 3-day retention20M/month, 7-day retention, $0.60/M overage
TracesFree 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.

sql
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.

sql
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

EventSourceCallback shapeWhat it means
`mediaPermissionError``meeting.self``({ message, kind })` — `message`: `DENIED` / `SYSTEM_DENIED` / `COULD_NOT_START`; `kind`: `audio` / `video` / `screenshare`Camera/mic permission denied or device unavailable. **Verified** against live RTK docs.
`mediaConnectionUpdate``meeting.meta``({ transport, state })` — `transport`: `consuming` / `producing`; `state`: `new` / `connecting` / `connected` / `disconnected` / `reconnecting` / `failed`WebRTC media connection state changed. **Verified** (changelog + live docs).
`socketConnectionUpdate``meeting.meta``({ state, reconnectionAttempt, reconnected })` — `state`: `connected` / `disconnected` / `reconnecting` / `failed`Signaling WebSocket state changed. **Verified** against live RTK docs.
`mediaScoreUpdate``meeting.self` + `meeting.participants.joined``({ kind, isScreenshare, score, scoreStats })` — `kind`: `audio` / `video`; `score`: 0-10; `scoreStats`: `{ bitrate, jitter, packetLoss, resolution }`Per-participant media quality score. Also available for remote participants (adds `participantId`). **Verified** — note: often labeled "network quality score" in docs but event name is `mediaScoreUpdate`.
`roomJoined``meeting.self``()` (no args)User successfully joined the meeting room.
`roomLeft``meeting.self``({ state })` — `state`: `left` / `kicked` / `ended` / `rejected` / `disconnected` / `failed`User left the room — `state` tells you why. Critical for "unexpected disconnect" diagnostics.
`deviceUpdate``meeting.self`Device objectActive audio/video device changed mid-meeting. Useful for debugging sudden media issues.
`deviceListUpdate``meeting.self`Updated device listDevice plugged in or removed. Correlates with media failures.
Recording `ERRORED` state`meeting.recording`Recording failed (irrecoverable)
Screenshare errorsUI Kit language packMax screenshare limit reached, unknown error
Livestream errorsUI Kit language packNot supported, not found, sync error, start/stop failure

Implementation Pattern

typescript
// In the React meeting component, after RTK meeting is initialized:

// Media permission errors
meeting.self.on("mediaPermissionError", ({ message, kind }) => {
  // message: DENIED | SYSTEM_DENIED | COULD_NOT_START
  // kind: audio | video | screenshare
  Sentry.captureException(new Error(`Media permission: ${kind} - ${message}`), {
    tags: { component: "rtk-sdk", error_type: "media_permission", meeting_id: meeting.meta.meetingId },
  });
});

// WebRTC media connection state
meeting.meta.on("mediaConnectionUpdate", ({ transport, state }) => {
  if (state === "failed") {
    Sentry.captureException(new Error(`WebRTC ${transport} connection failed`), {
      tags: { component: "rtk-sdk", error_type: "media_connection", meeting_id: meeting.meta.meetingId },
    });
  }
  Sentry.addBreadcrumb({ category: "rtk", message: `Media ${transport}: ${state}`, level: "info" });
});

// Signaling WebSocket state
meeting.meta.on("socketConnectionUpdate", ({ state, reconnectionAttempt, reconnected }) => {
  if (state === "failed") {
    Sentry.captureException(new Error("Signaling WebSocket connection failed"), {
      tags: { component: "rtk-sdk", error_type: "socket_connection", meeting_id: meeting.meta.meetingId },
    });
  }
  Sentry.addBreadcrumb({ category: "rtk", message: `Socket: ${state}${reconnected ? " (reconnected)" : ""}`, level: "info" });
});

// Per-participant media quality (score 0-10)
meeting.self.on("mediaScoreUpdate", ({ kind, score, scoreStats }) => {
  Sentry.addBreadcrumb({ category: "rtk", message: `Quality ${kind}: ${score}/10`, level: "info", data: scoreStats });
});

Join Flow Tracing

"I couldn't join the meeting" is the #1 support complaint in any video product. The join flow crosses client→Worker→RTK→WebRTC, and failure at any step looks the same to the user: nothing happens. We trace every step with structured Sentry breadcrumbs so that if any step fails, the full trace is captured automatically.

typescript
// Join flow — each step adds a breadcrumb, failure at any step fires captureException with full trail
async function joinMeeting(meetingId: string) {
  const addStep = (step: string) =>
    Sentry.addBreadcrumb({ category: "join_flow", message: step, level: "info" });

  try {
    addStep("join_flow:requesting_token");
    const { authToken, participantId } = await api.post(`/meetings/${meetingId}/join`);

    Sentry.setTag("participant_id", participantId);
    addStep("join_flow:token_received");

    addStep("join_flow:sdk_init");
    const meeting = await RTKClient.init({ authToken });

    addStep("join_flow:connecting");
    await meeting.join();

    // Track room state transitions via roomJoined + roomLeft events
    meeting.self.on("roomJoined", () => {
      Sentry.addBreadcrumb({ category: "rtk", message: "Room joined", level: "info" });
    });
    meeting.self.on("roomLeft", ({ state }) => {
      // state: left | kicked | ended | rejected | disconnected | failed
      if (["disconnected", "kicked", "rejected", "failed"].includes(state)) {
        Sentry.captureMessage(`Meeting ended unexpectedly: ${state}`, {
          level: "warning",
          tags: { component: "rtk-sdk", meeting_id: meetingId },
        });
      }
      Sentry.addBreadcrumb({ category: "rtk", message: `Room left: ${state}`, level: "info" });
    });

    addStep("join_flow:joined");
  } catch (err) {
    // All breadcrumbs from prior steps are attached automatically
    Sentry.captureException(err, {
      tags: { component: "join_flow", meeting_id: meetingId },
    });
    throw err;
  }
}

Room lifecycle tracked via two events: roomJoined (successful entry) and roomLeft with state left | kicked | ended | rejected | disconnected | failed. The meeting.self.roomState property can also be polled: initjoined | waitlisted | rejected | kicked | left | ended. Abnormal exits (disconnected, kicked, rejected, failed) fire Sentry warning events.

React Error Boundaries

Wrap RTK UI Kit components in Sentry error boundaries to catch component crashes:

tsx
<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:

ComponentShows
`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 RTK Peer Report API (Post-Meeting Quality Investigation)

RTK captures detailed per-participant quality data server-side for every session. This is the primary tool for investigating "my meeting was laggy" complaints — no client-side instrumentation needed.

Endpoint: GET /accounts/{ACCOUNT_ID}/realtime/kit/{APP_ID}/sessions/peer-report/{PEER_ID}?filters=device_info,ip_information,precall_network_information,events,quality_stats

What it returns per participant:

CategoryDataInvestigation Use
`quality.audio_producer`Timestamped: jitter, packet loss, RTT, MOS score, bytes sent"Audio kept cutting out" → check packet loss spikes over time
`quality.video_producer`Same metrics for video"Video was frozen" → check if participant stopped producing or quality dropped
`quality.*_cumulative`Aggregates: avg packet loss, p50/p75/p90 MOS, RTT distributionQuick quality score for the session — was it good, mediocre, or bad?
`metadata.device_info`OS, browser, browser version, CPU count, mobile flag, SDK version"It only happens on Safari" → filter by browser across sessions
`metadata.ip_information`City, country, region, ASN, timezone"Our London office has problems" → check if issues correlate with location/ISP
`metadata.candidate_pairs`ICE candidates (local/remote type, protocol, RTT), producing + consuming transports"Behind corporate firewall" → see if TURN relay was used, check relay RTT
`metadata.pc_metadata``effective_network_type`, `reflexive_connectivity`, `relay_connectivity`, `turn_connectivity`Pre-call network assessment — was the user's network even viable?
`metadata.events`Timestamped peer events throughout sessionFull timeline of what happened from RTK's perspective

Usage model: On-demand, not automated. When a complaint arrives:

  1. Look up the meeting → get session ID from D1 or RTK sessions API
  2. Get participant list: GET /sessions/{SESSION_ID}/participants
  3. For the complaining user, call peer-report with all filters
  4. Compare their quality stats with other participants in the same session:
    • If only they had bad MOS/high packet loss → their network
    • If everyone had issues → RTK/regional problem
    • If specific participants had issues → check their shared characteristics (location, ISP, browser)

"Always laggy" user: Pull peer reports across multiple sessions for the same custom_participant_id. If quality is consistently bad regardless of meeting → it's their network/device. The data answers the question definitively.

3.7 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 needed

Flow 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 JSON

Flow 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 user

3.8 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:

3.9 Diagnostics Routes

MethodRoutePurposeAuth
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 detailOwner/Admin
GET`/api/diagnostics/active-meetings`Currently active meetingsOwner/Admin
GET`/api/diagnostics/meetings/:meetingId/investigate`Meeting investigation report (see below)Owner/Admin
POST`/api/diagnostics/phone-home`Manual trigger for phone-home reportOwner only
PATCH`/api/diagnostics/settings`Update diagnostics settings (toggle vendor Sentry, phone-home, set customer DSN)Owner only

Removed routes (now handled by Sentry):

Meeting Investigation Endpoint

GET /api/diagnostics/meetings/:meetingId/investigate — the "never need a screenshare" tool. Aggregates all diagnostic data for a specific meeting into one response.

What it calls (server-side, on demand):

  1. GET /sessions?meeting_id={meetingId} → session details (start/end time, status)
  2. GET /sessions/{sessionId}/participants → participant list with join/leave times
  3. GET /sessions/peer-report/{peerId}?filters=device_info,ip_information,quality_stats,events,precall_network_information → per-participant quality (called for each participant)
  4. SELECT * FROM diagnostic_webhook_deliveries WHERE meeting_id = ? → webhook processing history

Response structure:

json
{
  "meeting_id": "...",
  "session": { "id": "...", "started_at": "...", "ended_at": "...", "duration_seconds": 3600 },
  "participants": [
    {
      "id": "...", "display_name": "...", "custom_participant_id": "...",
      "joined_at": "...", "left_at": "...", "duration": 3540,
      "device": { "os": "macOS", "browser": "Chrome 120", "is_mobile": false },
      "network": { "city": "London", "country": "GB", "asn": "AS2856", "effective_type": "4g" },
      "quality_summary": {
        "audio_mos_avg": 4.2, "audio_packet_loss_avg": 0.3, "audio_rtt_avg": 45,
        "video_mos_avg": 3.8, "video_packet_loss_avg": 1.2,
        "ice_candidate_type": "relay", "turn_used": true
      }
    }
  ],
  "webhook_deliveries": [ { "event_type": "recording.statusUpdate", "status": "processed", "...": "..." } ],
  "sentry_link": "https://sentry.io/organizations/.../issues/?query=meeting_id:..."
}

This single endpoint answers: who was there, when they joined/left, what device/browser/network they were on, what their quality was like, whether TURN was needed, and what webhooks fired. If quality was bad for one participant but fine for others — it's their network. If bad for everyone — it's RTK or regional.

3.10 Diagnostics Dashboard (Admin UI)

The SPA includes a /admin/diagnostics page (Owner/Admin role only):

PanelData SourceContent
**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
**Meeting Investigation**`/api/diagnostics/meetings/:id/investigate`Enter a meeting ID → see full investigation report: participants, quality per user, devices, network, webhook history, Sentry link
**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.11 Data Retention & Cleanup

Scheduled cleanup via Worker CRON:

TableRetentionSchedule
`diagnostic_webhook_deliveries`30 daysDaily at 03:00 UTC
`diagnostic_health_snapshots`90 daysWeekly

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:

toml
[triggers]
crons = ["0 3 * * *"]  # Daily at 03:00 UTC

3.12 Wrangler Configuration for Observability

toml
# 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.01

Required 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.13 Alerting (Launch Minimum)

"Know about issues before customers call" requires minimum alerting at launch. We use Sentry's built-in alert rules (zero code, configured in Sentry dashboard) plus phone-home:

Launch alerts (Sentry dashboard config, no custom code):

AlertConditionAction
Error spike>10 errors in 5 minutesSentry notification (email/Slack/PagerDuty via Sentry integrations)
Fatal errorAny `level: fatal` eventImmediate Sentry notification + phone-home alert
Recording failureEvents tagged `component: rtk-sdk` + `error_type: recording`Sentry notification
Media connection failureEvents tagged `error_type: media_connection`Sentry notification
New issueFirst occurrence of a new error typeSentry notification

Launch alerts (phone-home, built into health check CRON):

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:

RoleLevelDescription
**Owner**5Organization creator. One per org. Cannot be demoted. Full control over billing, settings, and all users.
**Admin**4Organization-level administrator. Manages users, settings, presets. Cannot delete org or transfer ownership.
**Host**3Can create and manage meetings, admit waiting room participants, control stage, kick users. Default role for new members with meeting creation privileges.
**Member**2Standard participant. Can join meetings they're invited to or that are open. Cannot create meetings unless promoted.
**Guest**1Unauthenticated or externally-authenticated user. Joins via meeting link. Controlled by guest access policies. No org-level visibility.

4.2 D1 Schema: Users & Organizations

sql
-- 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 token

Guest 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/rejects

JWT Middleware (Worker)

typescript
// 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

sql
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:

ActionOwnerAdminHostMemberGuest
Manage org settingsYesYes---
Manage users (invite, promote, deactivate)YesYes---
Create meetingsYesYesYes--
Edit/delete any meetingYesYesOwn only--
Start/stop recordingYesYesYes (own)--
Join meetingsYesYesYesInvited/OpenLink
Configure presetsYesYes---
View analyticsYesYesYes (own)--
Delete organizationYes----

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 Preset

5.2 Preset Naming Convention

Format: {meeting_type}_{participant_role}

Meeting TypeHost PresetParticipant PresetGuest PresetViewer 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`

5.3 Preset Resolution Logic (Worker)

typescript
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. The permissions object is optional but all its sub-fields are required when provided. Media permissions use "ALLOWED" / "NOT_ALLOWED" / "CAN_REQUEST" enums (not booleans). The config.view_type has three valid values: GROUP_CALL, WEBINAR, AUDIO_ROOM. Livestream presets use WEBINAR view_type with can_livestream: true.

5.4.1 Video (Group Call) Presets

video_host — Full meeting control

json
{
  "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)

json
{
  "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)

json
{
  "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`AudioVideoScreenshareHost 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

json
{
  "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

json
{
  "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)

json
{
  "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`AudioVideoScreenshareHost 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:

sql
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:

TypeRTK meetingTypeKey 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` + RTMPStage-based + RTMP export to external platforms + HLS playback

6.2 D1 Schema: Meetings & Scheduling

sql
-- 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:

TransitionTriggerRTK API Call
`draft → scheduled`Host sets date/timeNone (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 cancelsIf 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 invitees

Recurring 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 processing

6.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
SettingBehavior
`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)

typescript
// 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

ResourceDefault LimitConfigurable
Max participants per meeting100Yes (per meeting)
Max concurrent meetings per org25Yes (org setting)
Max guests per meeting50% of max_participantsYes (per meeting)
Max meeting duration24 hoursYes (org setting)
Max recording storage (R2)50 GB per orgYes (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

  1. Waiting room enforcement: Guests always enter waiting room when guest_access = 'link_only' (even if meeting-level waiting room is off for members)
  2. Display name validation: Strip HTML/script tags, enforce 2-50 char length, block known abuse patterns
  3. Token expiry: Guest tokens expire after 4 hours (configurable)
  4. IP-based join throttle: Max 3 guest joins from the same IP within 5 minutes per meeting
  5. Host can lock meeting: Once started, host can toggle acceptNewParticipants = false via RTK API to prevent new joins
  6. 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:

  1. 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.
  2. Check timestamp — reject if >5 minutes old (replay protection). UNVERIFIED: exact header name (X-RTK-Timestamp?) not confirmed.
  3. 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:

Design Document: Sections 7-9

Section 7: RTK Feature Mapping (Exhaustive)

Every RealtimeKit feature mapped to its app surface, API, and implementation status.

Legend:

7.1 Core Meeting Infrastructure

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Meeting creation`POST /meetings` REST APICreate Meeting page, Schedule modalServerBuild
Meeting ACTIVE/INACTIVE toggle`PATCH /meetings/{id}` REST APIMeeting list admin actionsServerBuild
Meeting metadata`meeting.meta` (client SDK)Meeting header, lobby screenMeetingBuild
Meeting title display`rtk-meeting-title` componentIn-meeting header barMeetingBuild
Meeting duration clock`rtk-clock` componentIn-meeting header barClientBuild
Session lifecycle (auto-create on join, end on last leave)Managed by RTK platformBackend webhook handlerServerBuild
Participant creation`POST /meetings/{id}/participants` REST APIJoin flow (Worker creates participant, returns authToken)ServerBuild
Participant token refresh`POST /participants/{id}/token` REST APIAuto-refresh middleware in WorkerServerBuild
Preset creation & management`POST /presets`, `PATCH /presets/{id}` REST APIOrg Settings > Presets pageServer + AppBuild
Meeting type: Video (Group Call)Preset `view_type: "GROUP_CALL"`Meeting creation form, preset configPresetBuild
Meeting type: Voice (Audio Only)Preset `view_type: "AUDIO_ROOM"`Meeting creation form, preset configPresetBuild
Meeting type: WebinarPreset `view_type: "WEBINAR"`Meeting creation form, preset configPresetBuild
`record_on_start` configMeeting creation parameterCreate Meeting form toggleMeetingBuild
`persist_chat` configMeeting creation parameterCreate Meeting form toggleMeetingBuild
`ai_config` (transcription + summarization)Meeting creation parameterCreate Meeting form — AI sectionMeetingBuild
`summarize_on_end` configMeeting creation parameterCreate Meeting form toggleMeetingBuild

7.2 Participant & Room Management

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Participant list`rtk-participants` componentSidebar > Participants tabPresetBuild
Participants toggle`rtk-participants-toggle` componentControl barClientBuild
Single participant entry`rtk-participant` componentInside `rtk-participants` (name, media status, actions)ClientBuild
Audio participant list`rtk-participants-audio` componentSidebar in audio-only meetingsClientBuild
Participant count badge`rtk-participant-count` componentControl bar, headerClientBuild
Participant tile (video)`rtk-participant-tile` componentMain grid areaClientBuild
Participant tile (audio-only)`rtk-audio-tile` componentAudio grid layoutClientBuild
Participant name tag`rtk-name-tag` componentOverlay on each tileClientBuild
Participant avatar`rtk-avatar` componentAudio tiles, participant listClientBuild
Participant setup/preview`rtk-participant-setup` componentPre-join screenClientBuild
Virtualized participant list`rtk-virtualized-participant-list`Large meeting sidebarClientBuild
Viewer list (webinar)`rtk-participants-viewer-list` componentWebinar sidebar tabClientBuild
Viewer count (livestream)`rtk-viewer-count` componentLivestream headerClientBuild
Kick participant`participant.kick()` (RTKParticipant method)Participant context menuPresetBuild
Mute participant audio/video`participant.disableAudio()`, `participant.disableVideo()` (RTKParticipant methods)Participant context menuPresetBuild
Mute all participants`rtk-mute-all-button` + `rtk-mute-all-confirmation`Control bar (host only)PresetBuild
Edit participant name`meeting.self.setName(name)` (RTKSelf method)Participant settingsPresetBuild
User ID (persistent across sessions)`meeting.self.userId` / `participant.userId`Analytics, user trackingClientBuild
Session ID (per-connection)`meeting.self.id` / `participant.id`Session-level trackingClientBuild

7.3 Waiting Room

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Waiting room enable/disablePreset configurationPreset editor > Waiting Room sectionPresetBuild
Waiting room screen`rtk-waiting-screen` componentShown to waitlisted participantsClientBuild
Waiting room participant list`rtk-participants-waiting-list` componentHost sidebar > Waiting tabPresetBuild
Admit participant`meeting.participants.acceptWaitingRoomRequest(id)`Waiting list item actionPresetBuild
Reject participant`meeting.participants.rejectWaitingRoomRequest(id)`Waiting list item actionPresetBuild
Admit all`meeting.participants.acceptAllWaitingRoomRequest(userIds)`Waiting list bulk actionPresetBuild
Auto-admit when host joinsPreset `waiting_room` configPreset editor togglePresetBuild
Bypass waiting room (by preset)Preset configurationPreset editor — bypass role listPresetBuild

7.4 Stage Management (Webinar)

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Stage view`rtk-stage` componentMain content area (webinar mode)PresetBuild
Join/leave stage toggle`rtk-stage-toggle` componentControl bar (webinar)PresetBuild
Join stage button`rtk-join-stage` componentViewer UI promptPresetBuild
On-stage participants list`rtk-participants-stage-list` componentHost sidebarPresetBuild
Stage request queue`rtk-participants-stage-queue` componentHost sidebar > Requests tabPresetBuild
Host: approve stage requests`meeting.stage.grantAccess()`Stage queue item actionsPresetBuild
Host: deny stage requests`meeting.stage.denyAccess()`Stage queue item actionsPresetBuild
Host: remove from stage`meeting.stage.kick(userIds)`Participant context menuPresetBuild

7.5 Chat System

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Complete chat panel`rtk-chat` componentSidebar > Chat tabPresetBuild
Chat toggle`rtk-chat-toggle` componentControl barClientBuild
Chat header`rtk-chat-header` componentChat panel header (pinned msgs, DM selector)ClientBuild
Message composer`rtk-chat-composer-view` componentChat panel bottomClientBuild
Chat messages list (paginated)`rtk-chat-messages-ui-paginated` componentChat panel body (infinite scroll)ClientBuild
Single message view`rtk-message-view` componentWithin chat listClientBuild
Markdown rendering`rtk-markdown-view` componentChat message contentClientBuild
Public chat`chatPublic` preset permissionDefault chat modePresetBuild
Private chat (1:1 DMs)`chatPrivate` preset permission + `privateChatRecipient` propChat selector > DM tabPresetBuild
Chat selector (public/private)`rtk-chat-selector` / `rtk-chat-selector-ui`Chat panel headerClientBuild
Pinned messages`rtk-pinned-message-selector` componentChat header area (public chat only)ClientBuild
Chat search`rtk-chat-messages-ui-paginated` component (search via props)Chat panel search barClientBuild
File messages`rtk-file-message-view` componentIn chat message listClientBuild
Image messages`rtk-image-message-view` + `rtk-image-viewer`In chat + full-screen viewerClientBuild
File upload (drag & drop)`rtk-file-dropzone` + `rtk-file-picker-button`Chat composer areaClientBuild
Draft attachment preview`rtk-draft-attachment-view` componentChat composer areaClientBuild
Emoji picker`rtk-emoji-picker` + `rtk-emoji-picker-button`Chat composerClientBuild
Chat export (post-meeting)`meeting.chatSynced` webhook + Chat Replay REST APIPost-meeting page > Chat tabServerBuild
Chat CSV dumpREST API download URLPost-meeting download buttonServerBuild
Fetch private messages`meeting.chat.fetchPrivateMessages` SDKDM chat historyClientBuild

7.6 Polls

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Polls panel`rtk-polls` componentSidebar > Polls tabPresetBuild
Polls toggle`rtk-polls-toggle` componentControl barClientBuild
Single poll display`rtk-poll` componentWithin polls panelClientBuild
Poll creation form`rtk-poll-form` componentPolls panel > CreatePresetBuild
Poll permissions (create/view/interact)Preset `polls` configPreset editor > Polls sectionPresetBuild

7.7 Breakout Rooms (Connected Meetings)

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Breakout rooms manager`rtk-breakout-rooms-manager` componentSidebar > Breakout tab (host)PresetBuild
Single room manager`rtk-breakout-room-manager` componentWithin breakout panelPresetBuild
Room participant list`rtk-breakout-room-participants` componentWithin each room cardClientBuild
Breakout rooms toggle`rtk-breakout-rooms-toggle` componentControl bar (host)ClientBuild
Broadcast message to rooms`rtk-broadcast-message-modal` componentBreakout panel > Broadcast buttonPresetBuild
Create/switch/close roomsConnected Meetings SDK APIsBreakout room management UIPresetBuild
Cross-meeting broadcast`meeting.participants.broadcastMessage` with `meetingIds`Host broadcast actionPresetBuild
**Platform limitation**Web only (beta)Show "web only" badge; hide on mobileBuild

7.8 Message Broadcasting & Collaborative Stores

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Broadcast message to meeting`meeting.participants.broadcastMessage(type, payload)`Custom event system (reactions, notifications)ClientBuild
Subscribe to broadcasts`broadcastedMessage` event listenerEvent handlers throughout appClientBuild
Rate limiting config`rateLimitConfig` / `updateRateLimits()`Internal config (not user-facing)ClientBuild
Collaborative stores (KV)`meeting.stores` API — create, subscribe, updateShared state: hand raise, custom annotations, cursor positionClientBuild
Store subscription`RTKStore` instance — `.subscribe()` on key changesReal-time UI updates from store changesClientBuild
Store session scopingData persists until session endsAutomatic cleanup — no action neededBuild

7.9 Recording

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Start recordingStart Recording REST API + client SDKControl bar > Record button (host)PresetBuild
Stop recordingStop Recording REST API + client SDKControl bar > Stop Record buttonPresetBuild
Recording toggle`rtk-recording-toggle` componentControl barPresetBuild
Recording indicator`rtk-recording-indicator` componentMeeting header (visible to all)ClientBuild
Fetch active recordingFetch Active Recording REST APIAdmin monitoringServerBuild
Fetch recording details/downloadFetch Recording Details REST APIPost-meeting page > RecordingsServerBuild
`recording.statusUpdate` webhookWebhook event (INVOKED/RECORDING/UPLOADING/UPLOADED/ERRORED)Webhook handler > update D1ServerBuild
Record on start`record_on_start` meeting configMeeting creation formMeetingBuild
Composite recordingDefault mode — multi-user single file (H.264/VP8)Primary recording outputMeetingBuild
Custom cloud storage uploadAWS, Azure, DigitalOcean, GCS, SFTP config via API (R2 via aws-compatible config)Org Settings > Storage configAppBuild
Watermarking`video_config.watermark` in Start Recording APIOrg Settings > Recording > WatermarkAppBuild
Recording codec configVideo/audio codec parameters in Start Recording APIOrg Settings > Recording > QualityAppBuild
Interactive recording (timed metadata)Recording SDK + metadata APIAdvanced: searchable playback markersMeetingBuild
Custom recording app`@cloudflare/realtimekit-recording-sdk`Future: custom recording layoutsAppFuture
Disable RTK bucket uploadRecording config optionOrg Settings > StorageAppBuild
Recording config precedenceConfig hierarchy managementInternal Worker logicServerBuild
Per-track recording`POST /recordings/track` — layers API for mapping audio/video tracks to output destinations ($0.0005/min). **Zero docs available** — pricing confirms existence but no API reference.Meeting settings toggleMeetingResearch

7.10 Livestreaming

Sparse docs warning: RTK SDK confirms livestreaming components exist (rtk-livestream-*, RTKLivestream API), but setup/configuration docs are minimal. RTMP destination config and HLS URL retrieval patterns will need discovery-by-experimentation at build time. The Start Recording API's RTMP export option is the likely config path.

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Livestream indicator`rtk-livestream-indicator` componentMeeting header (during livestream)ClientBuild
Livestream toggle`rtk-livestream-toggle` componentControl bar (host, webinar mode)PresetBuild
Livestream player (HLS)`rtk-livestream-player` componentViewer page / external embedClientBuild
RTMP export configPart of recording/export system — likely via Start Recording API with RTMP destinationMeeting settings > Livestream sectionMeetingBuild
HLS playback (hls.js)Bundled hls.js dependencyViewer page, post-meeting playbackClientBuild
Livestream as recording export modePricing: same as "Export (recording, RTMP or HLS streaming)"Worker config when starting streamServerBuild
Stage management for livestreamStage + livestream integrationWebinar host controls during streamPresetBuild

7.11 Transcription & AI

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Real-time transcriptionWhisper Large v3 Turbo (Workers AI)In-meeting captions overlayMeeting + PresetBuild
Caption toggle`rtk-caption-toggle` componentControl barClientBuild
AI panel`rtk-ai` componentSidebar > AI tabClientBuild
AI toggle`rtk-ai-toggle` componentControl barClientBuild
Live transcription display`rtk-ai-transcriptions` componentAI panel / caption overlayClientBuild
Single transcript entry`rtk-transcript` componentWithin transcription listClientBuild
Transcript history`rtk-transcripts` componentAI panel scrollbackClientBuild
Transcription language config`ai_config.transcription.language` on meeting creationMeeting creation form > LanguageMeetingBuild
`transcription_enabled` preset flagPreset parameterPreset editor togglePresetBuild
Post-meeting transcript`meeting.transcript` webhook + REST API fetchPost-meeting page > Transcript tabServerBuild
Transcript download URLPresigned R2 URL (7-day retention)Post-meeting download buttonServerBuild
AI meeting summary`meeting.summary` webhook + REST API fetch/triggerPost-meeting page > Summary tabServerBuild
Summary download URLPresigned R2 URLPost-meeting download buttonServerBuild
Summary type config`ai_config.summarization.summary_type` (e.g. `"team_meeting"`)Meeting creation form > Summary typeMeetingBuild
Summary text format`text_format: "markdown"`Internal config (always markdown)MeetingBuild
Manual summary trigger`POST /sessions/{id}/summary` REST APIPost-meeting page > Generate Summary buttonServerBuild
Summary output (Key Points, Action Items, Decisions)Structured markdown outputPost-meeting summary viewerServerBuild

7.12 Virtual Backgrounds & Video Effects

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Background blur`@cloudflare/realtimekit-virtual-background` packageSettings > Video > BackgroundClientBuild
Virtual background (custom image)Same package + Video Background addonSettings > Video > BackgroundClientBuild
Video Background addon`realtimekit-ui-addons` Video Background addonPre-join screen + in-meeting settingsClientBuild

7.13 Plugins

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Plugins panel`rtk-plugins` componentSidebar > Plugins tabPresetBuild
Plugins toggle`rtk-plugins-toggle` componentControl barClientBuild
Plugin main view`rtk-plugin-main` componentMain content area (when plugin active)ClientBuild
Whiteboard (built-in)Built-in plugin, enabled via presetPlugins panel > WhiteboardPresetBuild
Document Sharing (built-in)Built-in plugin, enabled via presetPlugins panel > Document SharingPresetBuild
Custom pluginsPlugin SDK (`meeting.plugins` API, iframe-based)Org Settings > Custom Plugins (future)PresetBuild
Plugin permissions (view/open/close)Preset `plugins` configPreset editor > Plugins sectionPresetBuild
Plugin data exchange`plugin.sendData()` / `plugin.handleIframeMessage()`Plugin <-> app communicationClientBuild

7.14 Media Controls & Device Selection

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Camera toggle`rtk-camera-toggle` componentControl barClientBuild
Camera selector`rtk-camera-selector` componentSettings > Video > Camera pickerClientBuild
Microphone toggle`rtk-mic-toggle` componentControl barClientBuild
Microphone selector`rtk-microphone-selector` componentSettings > Audio > Mic pickerClientBuild
Speaker selector`rtk-speaker-selector` componentSettings > Audio > Speaker pickerClientBuild
Audio visualizer`rtk-audio-visualizer` componentName tag, settings preview, pre-joinClientBuild
Settings panel`rtk-settings` componentSidebar > Settings tabClientBuild
Audio settings`rtk-settings-audio` componentSettings > Audio sub-panelClientBuild
Video settings`rtk-settings-video` componentSettings > Video sub-panelClientBuild
Settings toggle`rtk-settings-toggle` componentControl barClientBuild

7.15 Screen Sharing & PiP

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Screen share toggle`rtk-screen-share-toggle` componentControl barClientBuild
Screen share view`rtk-screenshare-view` componentMain content area (when sharing)ClientBuild
Screen share frame rate configSDK media config `screenShareConfig.frameRate`Internal config (default 5 FPS)ClientBuild
Picture-in-Picture toggle`rtk-pip-toggle` componentControl barClientBuild
PiP with reactionsSDK PiP supportBrowser PiP windowClientBuild
Fullscreen toggle`rtk-fullscreen-toggle` componentControl barClientBuild

7.16 Simulcast & Active Speakers

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Simulcast (multi-quality streams)SDK config (added Core 1.2.0)Internal — transparent to userClientBuild
Per-track simulcast configSDK `simulcastConfig` (`disable`, `encodings`) via `initMeeting` overridesBandwidth-adaptive quality (automatic)ClientBuild
Active speaker detection`meeting.participants.lastActiveSpeaker` (single participantId)Spotlight grid, speaker indicatorClientBuild
Active participants map`meeting.participants.active` (map of active participants)Active speaker grid layoutClientBuild
Spotlight grid`rtk-spotlight-grid` componentLayout option: spotlight modeClientBuild
Spotlight indicator`rtk-spotlight-indicator` componentOn active speaker tileClientBuild
Network quality indicator`rtk-network-indicator` componentOn participant tilesClientBuild

7.17 Grid Layouts

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Default responsive grid`rtk-grid` componentMain content area (default layout)ClientBuild
Simple grid`rtk-simple-grid` componentCompact layout optionClientBuild
Mixed grid (video + screenshare)`rtk-mixed-grid` componentWhen screen sharing activeClientBuild
Audio-only grid`rtk-audio-grid` componentVoice meeting layoutClientBuild
Grid pagination`rtk-grid-pagination` componentBelow grid (large meetings)ClientBuild

7.18 UI Chrome & Meeting Shell

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Complete meeting experience`rtk-meeting` componentDrop-in full meeting pageClientBuild
Pre-join setup screen`rtk-setup-screen` componentMeeting lobby / pre-joinClientBuild
Idle/disconnected screen`rtk-idle-screen` componentBefore joining / connection lostClientBuild
Meeting ended screen`rtk-ended-screen` componentAfter meeting endsClientBuild
Leave meeting`rtk-leave-meeting` / `rtk-leave-button`Control bar > LeaveClientBuild
Control bar`rtk-controlbar` componentBottom of meeting viewClientBuild
Control bar buttons`rtk-controlbar-button` componentIndividual action buttonsClientBuild
Sidebar container`rtk-sidebar` / `rtk-sidebar-ui` componentRight side panel (chat, participants, etc.)ClientBuild
Dialog/modal system`rtk-dialog` / `rtk-dialog-manager` componentConfirmation dialogs, settings modalsClientBuild
Menu system`rtk-menu` / `rtk-menu-item` / `rtk-menu-list`Context menus, dropdownsClientBuild
Notification system`rtk-notification` / `rtk-notifications`Toast notificationsClientBuild
Tab navigation`rtk-tab-bar` componentSidebar tab switchingClientBuild
Overlay modal`rtk-overlay-modal` componentFull-screen overlaysClientBuild
Confirmation modal`rtk-confirmation-modal` componentDestructive action confirmationsClientBuild
Permissions prompt`rtk-permissions-message` componentBrowser permission requestsClientBuild
More options menu`rtk-more-toggle` componentControl bar overflow menuClientBuild
Header bar`rtk-header` componentMeeting top barClientBuild
Logo display`rtk-logo` componentHeader / idle screenClientBuild
Loading spinner`rtk-spinner` componentLoading statesClientBuild

7.19 Addons

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Camera host control`realtimekit-ui-addons` Camera Host ControlParticipant tile menu (host)PresetBuild
Mic host control`realtimekit-ui-addons` Mic Host ControlParticipant tile menu (host)PresetBuild
Chat host control`realtimekit-ui-addons` Chat Host ControlParticipant tile menu (host)PresetBuild
Hand raise`realtimekit-ui-addons` Hand Raise addonControl bar + participant listClientBuild
Reactions`realtimekit-ui-addons` Reactions ManagerControl bar + floating reactions overlayClientBuild
Participant tile menu`realtimekit-ui-addons` Participant Tile MenuRight-click / long-press on tileClientBuild
Custom control bar button`realtimekit-ui-addons` Custom Control Bar ButtonExtend control bar with app-specific actionsClientBuild
Participant menu item`@cloudflare/realtimekit-ui-addons` Participant Menu ItemCustom menu items in participant context menuClientBuild
Participant tab actions`@cloudflare/realtimekit-ui-addons` Participants Tab Action/ToggleParticipant panel extensionsClientBuild

7.20 Branding & Design System

FeatureRTK API/ComponentApp LocationConfig LevelStatus
UI Provider context`rtk-ui-provider` componentApp root wrapperAppBuild
Design system tokens`provideRtkDesignSystem()` utilityApp initializationAppBuild
Theme (light/dark/darkest)Design token `theme`Org Settings > BrandingAppBuild
Brand colorDesign token `brand.500`Org Settings > BrandingAppBuild
Background colorDesign token `background.1000`Org Settings > BrandingAppBuild
TypographyDesign token `fontFamily` / `googleFont`Org Settings > BrandingAppBuild
Spacing scaleDesign token `spacingBase`Internal (default 4px)AppBuild
Border width/radiusDesign tokensOrg Settings > BrandingAppBuild
Custom icon packJSON SVG icon pack (40+ icons)Org Settings > Branding > IconsAppBuild
Custom logo`rtk-logo` component or `rtk-meeting` propOrg Settings > Branding > LogoAppBuild
Internationalization (i18n)`RtkI18n` type + `t` prop / `useLanguage()` hookOrg Settings > LanguageAppBuild
CSS variable prefix (`--rtk-*`)All design system outputsAutomatic from token configAppBuild

7.21 Debug & Diagnostics

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Debug panel`rtk-debugger` componentSettings > Debug (admin/dev only)ClientBuild
Debug toggle`rtk-debugger-toggle` componentControl bar (admin only)ClientBuild
Audio debug info`rtk-debugger-audio` componentDebug panel > Audio tabClientBuild
Video debug info`rtk-debugger-video` componentDebug panel > Video tabClientBuild
Screenshare debug info`rtk-debugger-screenshare` componentDebug panel > Screen tabClientBuild
System debug info`rtk-debugger-system` componentDebug panel > System tabClientBuild
Error codesRTK error code referenceError display UI + Sentry reportingClientBuild

7.22 Realtime Agents (Low Priority — Groundwork Only)

FeatureRTK API/ComponentApp LocationConfig LevelStatus
Agent as meeting participant`@cloudflare/realtime-agents` SDKAgent joins meeting with authTokenServer (DO)Low Priority
STT pipeline (Deepgram)`DeepgramSTT` componentAutomatic transcription of meeting audioServer (DO)Low Priority
LLM processing`TextComponent` + Workers AI bindingAgent text generationServer (DO)Low Priority
TTS pipeline (ElevenLabs)`ElevenLabsTTS` componentAgent spoken responsesServer (DO)Low Priority
RTK Transport`RealtimeKitTransport` componentAgent media I/O to meetingServer (DO)Low Priority
DO binding in wranglerDurable Object config`wrangler.jsonc` setupServerLow Priority
Agent route stubs`/agent/*` and `/agentsInternal` routesWorker routingServerLow Priority

7.23 TURN & NAT Traversal

What TURN does

TURN (Traversal Using Relays around NAT) is a relay service that ensures video/voice works for users behind corporate firewalls, strict NATs, or restrictive networks. Without it, ~10-15% of users (typically corporate/enterprise) cannot establish direct connections and would be unable to join meetings. TURN relays their media traffic through a public server as a fallback when direct or STUN-assisted connections fail.

How RTK handles TURN — zero developer action required

RealtimeKit manages TURN/ICE/NAT traversal entirely within its platform. When a participant joins a meeting using their authToken, the RTK SDK automatically:

  1. Performs ICE candidate gathering (local, STUN reflexive, TURN relay)
  2. Negotiates the best connection path with the RTK SFU
  3. Falls back to TURN relay if direct connectivity fails
  4. Handles credential rotation for long-running sessions

There is no TURN configuration in the RTK SDK. The RealtimeKitClient.init() method accepts only authToken, baseURI, and defaults — no ICE server URLs, no TURN keys, no credentials. The RTK quickstart flow is: create meeting → add participant → get authToken → pass to SDK → done.

We write zero TURN code. No key provisioning, no credential generation, no ICE server distribution.

⚠️ AGENT BUILD WARNING

Do NOT implement any TURN key provisioning, credential generation, or ICE server configuration code. If you encounter Cloudflare documentation describing these steps, it is for a different product (the standalone TURN Service for raw WebRTC apps) — not RealtimeKit. RTK handles all TURN/ICE internally. We write zero TURN code.

TURN diagnostics (post-meeting investigation)

While we don't manage TURN, we can observe it via the Peer Report API. Each participant's report includes:

FieldLocation in peer reportWhat it tells us
`turn_connectivity``metadata.pc_metadata[]`Whether the participant's network could reach TURN servers
`relay_connectivity``metadata.pc_metadata[]`Whether relay (TURN) connections were available
`reflexive_connectivity``metadata.pc_metadata[]`Whether STUN reflexive connections were available
`effective_network_type``metadata.pc_metadata[]`Network type (4g, wifi, etc.) at connection time
`local_candidate_type``metadata.candidate_pairs.producing_transport[]``host` (direct), `srflx` (STUN), or `relay` (TURN) — which path was actually used
`remote_candidate_type``metadata.candidate_pairs.producing_transport[]`The SFU's candidate type

This data is available via GET /sessions/peer-report/{peer_id}?filters=precall_network_information and is surfaced in the Meeting Investigation page (Section 3.6).

FeatureRTK API/ComponentApp LocationConfig LevelStatus
TURN relayManaged by RTK internally via `authToken` — zero developer codeTransparent — no user-facing configPlatformBuild
NAT traversal (ICE)RTK SDK handles candidate gathering, STUN/TURN fallback automaticallyTransparent — no developer actionPlatformBuild
Protocol supportSTUN/UDP, TURN/UDP, TURN/TCP, TURN/TLS — all handled by RTKAutomatic fallback based on network conditionsPlatformBuild
TURN diagnosticsPeer Report API: `turn_connectivity`, `candidate_pairs`, `effective_network_type`Admin > Meeting Investigation pageServerBuild

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):

FieldSource
Meeting title`meeting.meta.meetingTitle` (stored in D1 on session start)
Date & timeWebhook `meeting.ended` timestamp + session start time
DurationCalculated: end - start
Participant countFrom webhook payload or REST API session detail
Host nameFrom our user system (mapped via participant userId)
Meeting typeFrom preset config (Video/Voice/Webinar)
Recording availableBoolean — from `recording.statusUpdate` UPLOADED event
Transcript availableBoolean — from `meeting.transcript` webhook received
Summary availableBoolean — from `meeting.summary` webhook received

8.3 Summary Tab

Data source: meeting.summary webhook summaryDownloadUrl or REST API GET /sessions/{id}/summary

Display:

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:

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:

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:

8.7 Analytics Tab (Session-Level)

Data source: D1 database (custom-built, populated by webhooks — see Section 9).

Display:

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

8.9 Email Notification Flow

  1. meeting.ended webhook fires
  2. Worker processes event, stores session metadata in D1
  3. Worker enqueues email notification to all participants with our user accounts
  4. Email contains: meeting title, duration, summary snippet (first 200 chars), link to post-meeting page
  5. 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 EventPayload (key fields)Our ActionD1 Table
`meeting.started``meetingId`, `sessionId`, timestampCreate session record in D1 with exact start time. Update KV meeting-state.`sessions`
`meeting.ended``meetingId`, `sessionId`, timestamp1. 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 PointMethodFrequencyD1 Table
Active sessions list`GET /meetings` (filter ACTIVE)Every 60s via Cron Trigger`sessions`
Participant count per active sessionREST API session detailEvery 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

sql
-- 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 retry

9.6 Retry & Reliability

9.7 Analytics Computation

Real-time (per-event):

Periodic (Cron Trigger, daily at 00:05 UTC):

Dashboard queries (examples):

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)

PageRoutePurposeKey Elements
**Login**`/login`Email/password + OAuth loginLogin 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 requestEmail input, submit, confirmation message
**Reset Password**`/reset-password/:token`Set new passwordNew password + confirm fields
**Join as Guest**`/join/:meetingId`Guest entry to a specific meetingName input, optional email, device preview (camera/mic), "Join" button

Authenticated Pages (any role)

PageRoutePurposeKey Elements
**Dashboard**`/`Home page — upcoming meetings, quick actionsMeeting 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 experienceRecording player, transcript viewer, AI summary, chat export, attendee list
**Recordings**`/recordings`Browse all recordings user has access toRecording list with search/filter, thumbnail, duration, date, download link
**Recording Player**`/recordings/:recordingId`Playback a specific recordingVideo player, transcript sidebar (synced), AI summary tab, download button
**Profile**`/profile`User's own profile settingsDisplay name, avatar upload, email, password change, notification prefs

Host/Admin Pages (role-gated)

PageRoutePurposeKey Elements
**Schedule Meeting**`/meetings/new`Create instant or scheduled meetingMeeting 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 sessionsSame fields as create, plus: delete meeting, regenerate link, view past sessions
**Meeting History**`/meetings/:meetingId/history`Past sessions for a specific meeting roomSession list with start/end times, participant count, recordings, transcripts

Admin Pages (Admin/Owner only)

PageRoutePurposeKey Elements
**Org Settings**`/admin/settings`Organization-wide configurationTabbed interface (see Section 12 for full schema)
**Members**`/admin/members`User managementMember list, role assignment (Owner/Admin/Host/Member), invite button, remove/deactivate
**Invite**`/admin/invite`Send invitationsEmail input (single or bulk CSV), role selector, custom message, pending invites list
**Analytics**`/admin/analytics`Usage dashboardSession count, participant count, total duration, recording stats, charts (daily/weekly/monthly)
**Presets**`/admin/presets`Manage RTK presetsList of presets with preview of permissions, create/edit/clone/delete, JSON editor for advanced users
**Branding**`/admin/branding`Visual customizationTheme picker, color inputs, logo upload, font selector, live preview panel
**Recording Storage**`/admin/storage`Configure recording destinationStorage 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 prefs

Rules:

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:

  1. Not connected → rtk-idle-screen (redirects to pre-join)
  2. Pre-join → rtk-setup-screen (device selection + preview)
  3. Waiting room → rtk-waiting-screen (if waiting room enabled)
  4. In meeting → Full layout above
  5. Meeting ended → rtk-ended-screen → auto-redirect to post-meeting page

10.4 Page Transitions

FromToTrigger
DashboardPre-JoinClick meeting link or "Join" button
Pre-JoinMeeting RoomClick "Join Meeting" after device setup
Pre-JoinWaiting RoomAuto, if meeting has waiting room enabled
Waiting RoomMeeting RoomHost admits participant
Meeting RoomPost-MeetingMeeting ends or user leaves
Post-MeetingDashboardClick "Back to Dashboard"
Post-MeetingRecording PlayerClick "View Recording" (when available)
DashboardSchedule MeetingClick "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

CategoryTokenTypeDefaultNotes
**Theme**`theme``'light' | 'dark' | 'darkest'``'dark'`Sets background shade palette globally
**Brand Color**`colors.brand.500`hex colorRTK default bluePrimary accent. RTK auto-generates 300-700 shades
**Background**`colors.background.1000`hex colorTheme-dependentBase 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 stringSystem defaultOr 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

typescript
// 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

SettingStorageUsage
`logoUrl`R2 bucket (uploaded via admin UI)Passed to `rtk-logo` component; shown in meeting header and idle/ended screens
`faviconUrl`R2 bucketSet 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.

SettingTypeStorage
`customIconPack`JSON bloborg_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).

SettingTypeScopeNotes
`watermark.enabled`booleanorg-wide default, overridable per meetingWhether to apply watermark
`watermark.url`string (URL)org-wideWatermark image (typically org logo), stored in R2
`watermark.position`enum: `left top`, `right top`, `left bottom`, `right bottom`org-widePosition on the recording frame
`watermark.size`object: `{ height: number, width: number }` (px)org-wideSize of the watermark overlay

Applied when starting a recording via the RTK Recording SDK / REST API parameters.

11.5 Internationalization

SettingTypeScope
`defaultLocale`string (BCP 47)org-wide
`customStrings`JSON bloborg-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:

12.2 What Lives Where

SettingOrg-WidePer-MeetingRuntimeNotes
Branding (theme, colors, logo, fonts)Yes (primary)NoNoOrg-wide only
Icon packYes (primary)NoNoOrg-wide only
Default meeting typeYesYes (override)Novideo, audio, webinar, livestream
Waiting room enabledYes (default)Yes (override)NoHost can toggle per meeting
Recording auto-startYes (default)Yes (override)Yes (start/stop)
Recording storage providerYes (primary)NoNoOrg-wide only
Watermark configYes (primary)NoNoOrg-wide only
Max participantsYes (default)Yes (override)NoPer-meeting cap
Guest access enabledYes (default)Yes (override)No
Chat enabledYes (default)Yes (override)Yes (mute)Via preset permissions
Polls enabledYes (default)Yes (override)NoVia preset
Breakout rooms enabledYes (default)Yes (override)Yes (create/close)
Transcription enabledYes (default)Yes (override)Yes (start/stop)
AI summaries enabledYes (default)Yes (override)No
Virtual backgrounds allowedYes (default)NoNoOrg-wide policy
Plugins (whiteboard, doc sharing)Yes (default)Yes (override)NoVia preset
RTMP livestream enabledYes (default)Yes (override)Yes (start/stop)
Default localeYes (primary)NoNoOrg-wide only
Simulcast settingsYes (default)NoNoOrg-wide, technical setting
Rate limits (joins/min)Yes (primary)NoNoAbuse protection, org-wide
Invite-only registrationYes (primary)NoNoOrg-wide policy

12.3 D1 Schema: `org_settings`

sql
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:

TabSettings ExposedMaps 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 backgroundsVarious `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:

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:

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

FeatureDescriptionBuild 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 numbersWhen 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 APIMOVED TO BUILD — API is documented. See Section 7.9.
**Mobile Apps (iOS/Android)**Native mobile clients using RTK mobile UI KitsAfter 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 clientsAfter 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 rangesAfter basic analytics (session count, participant count, duration) proves useful and users request deeper insights
**Calendar Integration**Google Calendar / Outlook sync for scheduled meetingsWhen meeting scheduling is stable and users report friction from manual scheduling
**SSO / SAML**Enterprise single sign-onWhen enterprise customers require it. Implement via Cloudflare Access or direct SAML/OIDC integration
**Custom Recording Layouts**Branded recording with custom grid layout, overlaysWhen users need recordings that look different from the default composite layout. Uses RTK Recording SDK (custom recording app)
**E2E Encryption**End-to-end encrypted meetingsWhen 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 eventsAfter webhook processing is stable and users request specific integrations
**API for Customers**Public REST API for programmatic meeting managementWhen 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 usersSEPARATE 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:

  1. Platform support: Does RTK provide the underlying capability? (If not, is a viable third-party integration available?)
  2. User demand: Have multiple users/orgs requested this?
  3. Effort vs. impact: Is the implementation effort proportional to the number of users it helps?
  4. SDK stability: For features depending on pre-GA SDKs, has the SDK reached v1.0?
  5. 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)

ItemQuestion to AnswerHow to Research
~~Per-track recording~~~~RESOLVED~~ — `POST /recordings/track` API exists with layers system. Moved to BUILD.N/A
RTMP/HLS setupWhat 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 parityDo mobile UI Kits have equivalent components to the web 136-component set?Install iOS/Android UI Kit packages and enumerate available views/components
Recording SDKHow 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

AspectDetail
**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):

sql
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:

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 GroupPermissionLevelWhy
Account — Workers ScriptsWorkers ScriptsEditUpload Worker code
Account — Workers KV StorageWorkers KV StorageEditCreate KV namespaces
Account — Workers R2 StorageWorkers R2 StorageEditCreate R2 buckets
Account — D1D1EditCreate database, run migrations
Account — Account SettingsAccount SettingsReadVerify account access
Account — Cloudflare RealtimeKitRealtimeKitEditCreate apps, presets, webhooks
Zone — Zone *(optional)*ZoneReadList zones for custom domain setup (Workers custom domains handle DNS automatically — no DNS Edit needed)

Validation sequence:

  1. Verify token: GET /user/tokens/verify → confirms token is active
  2. Get account: GET /accounts → extract account_id
  3. Permission probes (7 parallel GET requests):
    • GET /accounts/{id}/workers/scripts → Workers
    • GET /accounts/{id}/d1/database → D1
    • GET /accounts/{id}/r2/buckets → R2
    • GET /accounts/{id}/storage/kv/namespaces → KV
    • GET /accounts/{id} → Account Settings
    • GET /accounts/{id}/realtime/kit/apps → RealtimeKit
    • GET /zones?account.id={id} → Zone (optional, for custom domain)
  4. 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:

ResourceCountNames
D1 Database1`rtk-platform`
R2 Buckets3`{slug}-branding`, `{slug}-recordings`, `{slug}-exports`
KV Namespaces4`rtk-sessions`, `rtk-rate-limits`, `rtk-meeting-state`, `rtk-cache`
Workers2`rtk-platform-{slug}` (main) + `rtk-updater-{slug}` (update/rollback, never self-updates)
Worker Secrets6`ACCOUNT_ID`, `RTK_API_TOKEN`, `JWT_SECRET`, `SENTRY_DSN_VENDOR`, `SENTRY_DSN_CUSTOMER`, `SENTRY_RELEASE`
RTK App1Created via RTK REST API
RTK Presets14Per Section 5.2 (4 meeting types × 3-4 roles each)
RTK Webhook1Points to Worker's `/webhooks/rtk`
Custom Domain0-1Optional

Pipeline phases:

Phase 1: Create Storage (parallel, ~2-4s)

8 independent API calls run simultaneously:

CallEndpointBody
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)

  1. Upload Worker with bindings:

    PUT /accounts/{id}/workers/scripts/{worker_name}
    Content-Type: multipart/form-data

    Worker code is pre-built and stored in vendor R2. Bindings injected dynamically from Phase 1 resource IDs.

  2. Set secrets (6 sequential PUT calls):

    PUT /accounts/{id}/workers/scripts/{worker_name}/secrets

    Sets: ACCOUNT_ID, RTK_API_TOKEN, JWT_SECRET, SENTRY_DSN_VENDOR, SENTRY_DSN_CUSTOMER, SENTRY_RELEASE

  3. Enable workers.dev subdomain:

    POST /accounts/{id}/workers/scripts/{worker_name}/subdomain
  4. Run D1 schema migration:

    POST /accounts/{id}/d1/database/{db_id}/query

    Executes complete schema from Section 2.3.

Phase 3: Configure RTK (mostly parallel, ~4-8s)

  1. Create RTK App: POST /accounts/{id}/realtime/kit/apps
  2. Create 14 presets (parallel): POST /accounts/{id}/realtime/kit/{app_id}/presets — per Section 5.2
  3. Register webhook: POST /accounts/{id}/realtime/kit/{app_id}/webhooks
  4. 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:

PhaseDescriptionTime
Phase 1Create storage (8 parallel)2-4s
Phase 2Deploy Worker + secrets + schema5-10s
Phase 3RTK app + 14 presets + webhook + seed4-8s
Phase 4Custom domain (optional)1-5s
**Total****12-27s**

14.5 Health Check System

On-demand: Vendor Worker calls deployed Worker's /api/diagnostics/health:

CheckMethodPass Criteria
D1 connectivity`SELECT 1`No error
R2 connectivity`HEAD` on branding bucket200 or 404
KV connectivity`GET` test keyNo 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):

SettingInput TypeDefaultDescription
Organization nameTextRequiredShown in platform UI and meeting links
DomainDomain selector (see below)RequiredDefault: subdomain on user's zone. Fallback: `{slug}.workers.dev`
Primary brand colorColor picker`#0055FF`Applied to platform UI
LogoFile uploadSkipOptional, changeable later
Default meeting typeDropdownVideoPre-selected option when host creates a new meeting. Host can always change per-meeting.
Guest accessToggleOnAllow link-based access without accounts
Waiting roomToggleOnRequire host approval for guests
Recording policyDropdownHost DecidesAlways / Host Decides / Never
TranscriptionToggleOffAuto-transcribe recordings

Domain selector UX (Step 3):

  1. If user's token has Zone Read permission, fetch their zones via GET /zones. Show a dropdown of available domains.
  2. Default to subdomain mode: user picks a zone, enters a subdomain (e.g., meet.example.com). This is the recommended and most common option.
  3. 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."
  4. If no zones available (token lacks Zone Read): fall back to {slug}.workers.dev with a note that custom domains can be configured later.
  5. 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

14.7 Page & Route Map

Public Pages (no auth)

RoutePagePurpose
`/`LandingProduct explanation, CTA to register
`/login`LoginEmail + password (OAuth slot reserved)
`/register`RegisterEmail + password + display name
`/verify-email`Email VerificationPending verification + token confirmation
`/forgot-password`Forgot PasswordEmail input to request reset
`/reset-password/:token`Reset PasswordNew password form

Onboarding Wizard (auth required, full-screen)

RouteStepTitle
`/onboarding/welcome`1Welcome — what you need
`/onboarding/connect`2Connect Cloudflare — token setup
`/onboarding/configure`3Configure Platform — pre-deploy settings
`/onboarding/review`4Review & Deploy — summary
`/onboarding/deploying`5Deploying — live progress

Authenticated Pages

RoutePagePurpose
`/dashboard`DashboardPost-deploy home: health, stats, quick actions
`/deployment`Deployment DetailHealth checks per service, infrastructure inventory, deploy history, danger zone
`/help`Help & DocsFAQ, troubleshooting, support contact

Settings

RoutePagePurpose
`/settings/account`AccountProfile, password, sessions, delete account
`/settings/cloudflare`CF ConnectionToken status, re-validation, token update
`/settings/platform`Platform ConfigOrg name, branding, domain, meeting defaults, diagnostics
`/settings/notifications`NotificationsEmail alerts for health changes, token expiry, updates
`/settings/updates`UpdatesCurrent version, changelog, update button (mechanism TBD)

Utility

RoutePage
`*`404 Not Found
`/error`Generic Error

Total: 22 routes

14.8 Component Architecture

Layout Components

ComponentDescription
`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

ComponentDescription
`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

ComponentDescription
`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

ComponentDescription
`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

ComponentDescription
`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

ModalTriggerPurpose
`ConfirmDeployModal`Deploy/Redeploy buttonConfirm 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 redeployChoose "Save & Redeploy" or "Save Only"
`SessionExpiredModal`401 response"Sign In Again" prompt

14.9 Responsive Design

BreakpointBehavior
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

WorkerPurposeSelf-updates?
`rtk-platform-{slug}`Main platform WorkerYes (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:

json
{
  "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 hour

Fetches 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:

  1. Download new bundle from vendor R2. Verify SHA-256 checksum.
  2. Save rollback info to KV:
    • rollback:{version}:previous_version = current Worker version ID (from GET /deployments)
    • rollback:{version}:bookmark = current D1 bookmark (from GET /d1/{db_id}/time_travel/bookmark)
  3. Upload new Worker version (does NOT deploy yet):
    POST /accounts/{id}/workers/scripts/{worker_name}/versions
  4. Run D1 migrations (additive-only — see migration strategy below):
    POST /accounts/{id}/d1/database/{db_id}/query
  5. Deploy new version:
    POST /accounts/{id}/workers/scripts/{worker_name}/deployments
    Body: { "strategy": "percentage", "versions": [{ "version_id": "{new}", "percentage": 100 }] }
  6. Update KV: platform:current_version = new version
  7. 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:

  1. Read rollback info from KV: previous version ID + pre-migration D1 bookmark
  2. Redeploy previous Worker version:
    POST /accounts/{id}/workers/scripts/{worker_name}/deployments
    Body: { "strategy": "percentage", "versions": [{ "version_id": "{previous}", "percentage": 100 }] }
  3. Schema: No D1 rollback needed (additive-only migrations — old code ignores new columns)
  4. Emergency D1 restore (manual, only if data is corrupted):
    POST /accounts/{id}/d1/database/{db_id}/time_travel/restore?bookmark={saved_bookmark}
    Warning: This loses all data written after the bookmark. Owner must confirm.

D1 Migration Strategy: Additive-Only

AllowedNot Allowed
ADD COLUMN (nullable or with default)DROP COLUMN
ADD TABLEDROP TABLE
ADD INDEXRENAME COLUMN
INSERT seed dataALTER 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:

sql
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)

FeatureDetail
Worker version retention**100 versions** available for rollback
Rollback speedInstant (seconds for global propagation)
Rollback scopeCode + config only. **Secrets, bindings, KV, D1, R2 are NOT rolled back.**
Gradual rolloutsCan split traffic between two versions (canary deployment)
D1 Time Travel**30-day retention**, always on, no extra cost
D1 restoreAtomic. Returns `previous_bookmark` for undo. All post-restore writes are lost.
R2 versioningNone. Use versioned key prefixes (`assets/v1.2.3/`)
KV versioningNone. Use versioned keys
SecretsScript-level, not version-level. Persist across rollbacks.

API Endpoints for Update System

PurposeMethodPath
List Worker versionsGET`/accounts/{id}/workers/scripts/{name}/versions`
Upload new versionPOST`/accounts/{id}/workers/scripts/{name}/versions`
List deploymentsGET`/accounts/{id}/workers/scripts/{name}/deployments`
Create deployment (rollback)POST`/accounts/{id}/workers/scripts/{name}/deployments`
Get D1 bookmarkGET`/accounts/{id}/d1/database/{db_id}/time_travel/bookmark`
Restore D1POST`/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)

14.12 Deployment App API Routes

MethodRouteAuthPurpose
POST`/api/auth/register`NoCreate account
POST`/api/auth/login`NoLogin
POST`/api/auth/logout`YesLogout
GET`/api/auth/verify`NoVerify email token
POST`/api/auth/forgot-password`NoRequest password reset
POST`/api/auth/reset-password`NoComplete password reset
GET`/api/auth/me`YesGet profile
PATCH`/api/auth/profile`YesUpdate profile
POST`/api/auth/change-password`YesChange password
GET`/api/auth/sessions`YesList active sessions
DELETE`/api/auth/sessions`YesSign out all other sessions
POST`/api/deploy/validate-token`YesValidate CF API token + permissions
GET`/api/deploy/check-subdomain/:name`YesCheck subdomain availability
POST`/api/deploy/start`YesTrigger deployment pipeline
GET`/api/deploy/status`YesGet deployment health + status
GET`/api/deploy/status/stream`YesSSE stream for deployment progress
GET`/api/deploy/infrastructure`YesList deployed CF resources
GET`/api/deploy/history`YesDeployment history
GET`/api/deploy/stats`YesQuick stats (proxied from platform)
GET`/api/deploy/token-status`YesCurrent token validity
PUT`/api/deploy/token`YesUpdate CF API token
GET`/api/deploy/config`YesGet platform configuration
PATCH`/api/deploy/config`YesUpdate platform configuration
POST`/api/deploy/redeploy`YesTrigger redeployment
POST`/api/deploy/destroy`YesDestroy all deployed resources
POST`/api/deploy/health-check`YesTrigger manual health check
GET`/api/deploy/version`YesCurrent deployed version
GET`/api/deploy/updates`YesAvailable updates
GET`/api/activity`YesRecent activity log
GET`/api/settings/notifications`YesNotification preferences
PATCH`/api/settings/notifications`YesUpdate notification preferences

14.13 Cloudflare API Endpoints Used

Core deployment (16 endpoints):

PurposeMethodPath
Verify tokenGET`/user/tokens/verify`
List accountsGET`/accounts`
Create D1 databasePOST`/accounts/{id}/d1/database`
Query D1POST`/accounts/{id}/d1/database/{db_id}/query`
Delete D1DELETE`/accounts/{id}/d1/database/{db_id}`
Create R2 bucketPOST`/accounts/{id}/r2/buckets`
Delete R2 bucketDELETE`/accounts/{id}/r2/buckets/{name}`
Create KV namespacePOST`/accounts/{id}/storage/kv/namespaces`
Delete KV namespaceDELETE`/accounts/{id}/storage/kv/namespaces/{ns_id}`
Upload WorkerPUT`/accounts/{id}/workers/scripts/{name}`
Delete WorkerDELETE`/accounts/{id}/workers/scripts/{name}`
Set Worker secretPUT`/accounts/{id}/workers/scripts/{name}/secrets`
Enable subdomainPOST`/accounts/{id}/workers/scripts/{name}/subdomain`
Attach custom domainPUT`/accounts/{id}/workers/domains`
Detach custom domainDELETE`/accounts/{id}/workers/domains/{domain_id}`
Patch Worker settingsPATCH`/accounts/{id}/workers/scripts/{name}/settings`

RealtimeKit (8 endpoints):

PurposeMethodPath
Create appPOST`/accounts/{id}/realtime/kit/apps`
Create presetPOST`/accounts/{id}/realtime/kit/{app_id}/presets`
Update presetPATCH`/accounts/{id}/realtime/kit/{app_id}/presets/{preset_id}`
Delete presetDELETE`/accounts/{id}/realtime/kit/{app_id}/presets/{preset_id}`
Add webhookPOST`/accounts/{id}/realtime/kit/{app_id}/webhooks`
Update webhookPATCH`/accounts/{id}/realtime/kit/{app_id}/webhooks/{wh_id}`
Delete webhookDELETE`/accounts/{id}/realtime/kit/{app_id}/webhooks/{wh_id}`
List meetingsGET`/accounts/{id}/realtime/kit/{app_id}/meetings`

Zone (optional, 1 endpoint — Workers custom domains handle DNS automatically):

PurposeMethodPath
List zonesGET`/zones?account.id={id}`

Spec Review Tracker

This section is not part of the product specification. It tracks the review and verification status of each spec section before build begins.

Every section must be reviewed and marked DONE before build begins. Reviews follow dependency order — a section should not be reviewed until its dependencies are complete.

Review Status

OrderSectionStatusDepends OnKey Review FocusRisk
14 — User System & AuthTODONoneRole definitions (Owner/Admin/Host/Member/Guest), auth flow, token lifecycle, permission matrixHIGH — everything references roles
25 — Role-to-Preset MappingDONESection 4Preset schema verified against OpenAPI, `view_type` enum, permissions structure, 14 presets
36 — Meeting LifecycleTODOSections 4, 5Meeting CRUD, session lifecycle, participant creation, join flow, scheduling (instant/scheduled/recurring/permanent)HIGH — core object model
42 — System ArchitecturePARTIALSections 4, 5, 6D1 schemas match data model, KV key patterns, R2 bucket structure, FK consistency, column types, Worker routesHIGH — schema errors cascade
53 — Diagnostics & ObservabilityDONESection 2SDK events verified against live docs, code samples corrected, Peer Report API, join-flow tracing, alerting rules
67 — RTK Feature MappingDONESections 5, 6SDK methods verified, deprecated components replaced, per-track→Research, TURN rewritten, simulcast automatic
78 — Post-Meeting ExperienceTODOSection 7Recording playback, transcript viewer, AI summary display, download URLs, session analytics displayMEDIUM — depends on recording/transcription details in Section 7
89 — Webhook & Analytics PipelineTODOSections 6, 7, 8Event catalog completeness (known gap), payload shapes via MCP, D1 ingestion, analytics queries feasibleHIGH — incomplete webhook catalog
910 — App Pages & NavigationTODOSections 4, 6, 7, 8Page inventory matches features, route structure, component-to-page mapping, all user flows coveredHIGH — this is what users see
1011 — Branding & ThemingTODOSection 10Theme token names, CSS custom properties, design system consistency, RTK theming APILOW
1112 — Organization ConfigurationTODOAll feature sectionsSettings aggregation matches actual features, no orphaned settings, canonical schema cross-refMEDIUM
1213 — Future FeaturesTODOAll feature sectionsNothing marked "future" is actually buildable now; nothing marked "build" belongs hereLOW
1314 — Deployment AppTODOIndependentUpdate mechanism, Worker deploy, migration strategy, rollback, wrangler config, licensingMEDIUM
141 — Product OverviewTODOEverythingSummary accuracy vs all detailed sections, no stale claims, scope table matches realityLOW — final consistency pass

Completed Reviews

SectionDateKey Changes Made
3 — Diagnostics2026-03-06SDK events verified (mediaPermissionError, mediaConnectionUpdate, socketConnectionUpdate, mediaScoreUpdate, roomJoined/roomLeft). Added deviceUpdate, deviceListUpdate events. Code samples corrected. Peer Report API section added. Join-flow tracing added. Alerting upgraded to launch minimum with Sentry alert rules.
5 — Role-to-Preset Mapping2026-03-06Section 5.4 rewritten with verified OpenAPI preset schema. `view_type` enum corrected to GROUP_CALL/WEBINAR/AUDIO_ROOM. config/permissions/ui structure documented with full example.
7 — RTK Feature Mapping2026-03-06SDK methods added (participant.kick(), disableAudio/Video, waiting room accept/reject, stage grant/deny/kick). Deprecated components replaced. Per-track recording → Research. Livestream sparse docs noted. TURN section rewritten (RTK manages internally, not standalone TURN Service). Simulcast kept automatic.

Review Rules

Source: live from GitHub