Skip to content

docs(api): document the scheduled sessions API and its invitee tables - #63

Merged
ralyodio merged 4 commits into
masterfrom
docs/scheduled-sessions-api
Aug 11, 2026
Merged

docs(api): document the scheduled sessions API and its invitee tables#63
ralyodio merged 4 commits into
masterfrom
docs/scheduled-sessions-api

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The scheduled meetings feature shipped undocumented — docs/API.md covered neither the two tables nor any of the /api/scheduled-sessions endpoints. This is docs-only; no code changes.

What's added

Three things worth writing down

  1. inviteeEmails is the complete desired guest list, not a delta. Omitting it leaves the list alone; sending [] removes everyone. Included a table of which email goes out for which kind of change, since that is not obvious from the route.

  2. Removing an invitee does not revoke access. The join_code is shared and is not rotated on removal, so a removed invitee can still join with the code from their original email, and there is no endpoint to re-issue a scheduled meeting's code. Recorded as a known limitation rather than leaving removal to look like a lockout — worth a follow-up if it should actually lock people out.

  3. scheduled_session_invitees must not get a permissive anon policy. Documents why the dropped USING (true) pair was dangerous (RLS cannot restrict which columns an UPDATE touches, so it exposed every invite_token) and why the table needs no browser-facing policy at all.

One correction made while writing

scheduled_sessions.session_id is never written by any code path — I verified no route or component sets it. It stays NULL even after a meeting starts, so the docs say that outright instead of implying it gets populated. The link to the live room is the join_code, which handleStart passes to POST /api/sessionscreate_session(p_join_code => ...).

Every claim was checked against the routes, the Zod schemas, and both migrations rather than inferred.

🤖 Generated with Claude Code

The scheduled meetings feature shipped undocumented: docs/API.md covered
neither the two tables nor any of the /api/scheduled-sessions endpoints.

Adds the scheduled_sessions and scheduled_session_invitees schemas, the
create/list/read/update/cancel endpoints, and the invitee-facing RSVP
endpoints at /api/invite/[token].

Three things worth writing down rather than rediscovering:

- PATCH takes `inviteeEmails` as the complete desired guest list, not a
  delta, and which email goes out depends on what the diff turns up.
  Omitting the field leaves the list alone; sending [] clears it.
- Removing an invitee does not rotate the shared join_code, so it does
  not actually revoke access. Recorded as a known limitation instead of
  leaving removal to look like a lockout.
- scheduled_session_invitees must not get a permissive anon policy —
  the dropped USING (true) pair leaked every invite_token to the browser.
  Notes why the table needs no browser-facing policy at all.

Also corrects an assumption while documenting: scheduled_sessions.session_id
is never written by any code path, so it stays NULL even after a meeting
starts. The link to the live room is the join_code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

170 finding(s)

HIGH/CRITICAL: 16 | MEDIUM: 48 | LOW: 106

Severity Rule Location
HIGH sh-eval-expansion .githooks/pre-commit:33
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:691
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:715
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:813
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:815
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:894
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:910
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:1064
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:1066
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:1128
HIGH secret-generic-credential apps/livekit/fly.toml:12
HIGH sh-remote-script-execution apps/livekit/setup-livekit-server.sh:93
HIGH sh-remote-script-execution apps/turn/deploy-droplet.sh:62
HIGH secret-generic-credential apps/turn/fly.toml:11
HIGH secret-generic-credential docs/API.md:747
HIGH secret-generic-credential docs/API.md:753
MEDIUM insecure-temp-file .githooks/commit-msg:19
MEDIUM insecure-temp-file .githooks/post-commit:22
MEDIUM insecure-temp-file apps/desktop/src/main/streaming/index.test.ts:64
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:370
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:393
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:402
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:438
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:48
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:73
MEDIUM js-unescaped-html-sink apps/web/src/app/c/[handle]/page.tsx:191
MEDIUM js-unescaped-html-sink apps/web/src/app/l/[joinCode]/page.tsx:165
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:128
MEDIUM js-unescaped-html-sink apps/web/src/app/live/page.tsx:145
MEDIUM js-unescaped-html-sink apps/web/src/app/page.tsx:122
MEDIUM js-unescaped-html-sink apps/web/src/app/pricing/page.tsx:284
MEDIUM js-open-redirect apps/web/src/app/pricing/UpgradeButton.tsx:50
MEDIUM js-unescaped-html-sink apps/web/src/app/u/[username]/page.tsx:282
MEDIUM manifest-install-lifecycle-script package.json:54
MEDIUM sql-template-interpolation packages/ai-core/src/prompts.ts:36
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:81
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:96
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:112
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:133
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:160
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:189
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:214
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:237
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:273
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:298
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:324
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:346
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:383
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:154
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:160

…and 120 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

ralyodio and others added 3 commits August 11, 2026 02:34
Removing an invitee deleted their row and emailed them "your join code no
longer applies" — which was not true. The code is shared by the whole guest
list and was never rotated, so a removed invitee could still open the room
with the code from their original invitation. The email said one thing and
the system did another.

Now a PATCH that drops at least one invitee rotates the meeting's join_code
and emails the replacement to everyone still invited, which makes the
existing removal copy honest.

Notes on the edges:

- Rotation is skipped once the meeting has started. create_session() copies
  the code onto the live sessions row, so rewriting the scheduled row would
  not evict anyone — it would only claim a lockout that did not happen.
  /api/sessions/{id}/regenerate-code is the lever for a running session.
- A failed rotation write does not fail the PATCH. The removal already
  succeeded, and a stale code beats a half-applied edit; retained invitees
  are simply not told the code changed.
- The update email hardcoded "Your join code (unchanged)", which would have
  become a lie. It now switches on a codeChanged flag.
- Adding an invitee never rotates. Only removal revokes.

Because the code is shared, there is no way to revoke one person without
reissuing to everybody. That is inherent to a single shared code.

getUniqueJoinCode moved out of the create route into lib/join-code.ts so
both paths generate codes the same way, alongside a liveSessionExistsForCode
helper for the already-started check.

Also fixes the test service mock: it returned [] for unmatched tables, and
Boolean([]) is true, so the live-session probe would have read as "already
started" and silently suppressed every rotation under test.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
CI runs format:check across **/*.md and the new scheduled-sessions section
did not match prettier's table and code-fence formatting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same CI format:check gate as the previous commit; the rotation tests were
added without a prettier pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 7660afe into master Aug 11, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant