Preview resource budgets
Hosting is available to all verified Plus, Pro, and Enterprise accounts, as well as verified @tuturuuu.com accounts. Eligibility uses the current confirmed authentication email and the host’s personal subscription; session claims cannot override it. Other accounts can join by invitation. Workspace membership and Calendar permissions still apply when creating or scheduling a meeting. A room lasts up to two hours for eligible Free hosts, ten hours for Plus, and twenty-four hours for Pro/Enterprise. The server resolves the host’s personal subscription in both Meet and the workspace token endpoint. The deadline starts with the first admitted participant; reconnects keep it. Signed room limits default to eight publishers and 96 viewers and may be lower. Server hard ceilings still apply to every role and tier. Each publisher can register at most six media tracks. The server accrues participant-milliseconds through the room deadline. Expiration ends the room, disconnects clients and persists outstanding SFU closures before provider cleanup. Failed cleanup retries with exponential backoff from ten seconds to one hour. Progress is persisted after each completed provider session so a later failure does not repeat already-confirmed work. Unconfirmed tracks remain recorded for retry/operator reconciliation. The UI shows the room deadline and explains resource-limit failures. Browser bandwidth counters are diagnostic, not billing evidence. Monthly workspace media aggregation and provider invoice reconciliation remain prerequisites for selling meeting overages. Internal or sponsored workspaces do not bypass these ceilings.Empty rooms and owner restoration
Meet and Parley calls end automatically after five consecutive minutes with no admitted participant connected to the room. The durable timer starts at the last disconnect, including recoverable transport loss; the shorter media-presence recovery grace does not extend it. An admitted reconnect cancels the deadline. Quiet or muted participants with an open connection keep the meeting live; waiting-room connections do not. Rooms that have never admitted anyone do not start this timer. The persisted deadline survives room-object hibernation, and an alarm from an older connection generation cannot end a newer call. Only the meeting’s original owner may restore an ended call. Restoration requires a fresh room-state version, the same authenticated actor, a same-origin request, and a server-authorized owner command. It preserves the original room expiration, participant usage, capacity limits, and outstanding provider cleanup. It never creates a new resource allowance: expired rooms and rooms whose historical budget cannot be established cannot be restored. Restoring clears old live-assistant and share sessions. A restored empty room gets the same five-minute disconnect rule until an admitted participant connects. Ended-room browser storage is an actor-scoped hint, not authorization or permanent state. Meeting-list actions revalidate it against the server so a restored room can become joinable again. Delayed restoration work cannot clear another account’s hint or finish a newer attempt. The destination still checks access independently. Regression coverage lives inroom-lifecycle.test.ts,
room-do-empty-lifecycle.test.ts, the room-state route tests, and
restore-room-button.test.tsx. These source tests do not establish actual deployed
alarm behavior or browser reconnection; verify both on the deployed commit.
Cloudflare validation runs the explicit durable-object test files from
RUNNER_TEMP, using absolute checkout paths. Running bun test from the repository
root invokes its preload and redirects to the monorepo test suite instead.
Pieces
Meet’s pre-join preview and in-call controls are rendered from
packages/meet-core/src/features/call. Keep that package in the Tailwind source
scan of apps/meet/src/app/[locale]/meet.css when moving shared call UI. The scan
must be in the stylesheet that imports the shared UI theme; a separate CSS import
containing only @source does not generate those utilities for Meet.
room.ts is a pure reducer: it returns the next room snapshot plus the messages
to fan out and any Cloudflare work to perform. Both transports run the same
reducer, so the Bun server and the Durable Object cannot drift.
The app secret never reaches a browser. The client receives only a short-lived
HMAC join token; the room server performs every SFU call on its behalf.
Running a call locally
Create an SFU app under Cloudflare dashboard → Realtime → SFU and put the values inapps/meet-realtime/.dev.vars (gitignored). See the
apps/meet-realtime README for variable names.
NEXT_PUBLIC_MEET_REALTIME_URL, which defaults to
ws://127.0.0.1:8786/realtime.
Verifying without a full environment
Two checks run against real Cloudflare without needing Supabase, auth or seed data. Signaling and host controls — boots the room server, connects a host and a guest, and exercises the lobby, chat, hand raise, force mute and removal:http://127.0.0.1:7898/?peer=a and ?peer=b. Each tab reports
bytesReceived and framesDecoded, and renders the other peer’s video.
Two contract details worth remembering
Both of these were found only by running against the live API, and both produce confusing errors if reintroduced:- Read
transceiver.midonly aftersetLocalDescription(). It isnullbefore that, and publishing without it fails with406 tracks[0]: Missing mid in track. - Do not send a
sessionDescriptionwhen pulling remote tracks. Cloudflare returns the offer, which the client answers viasfu.renegotiate. Sending an empty SDP is rejected as a malformed event.
{} and {autoDiscover: true} are both rejected.
Attributing remote tracks
An inbound track arrives with an opaque id, so nothing on theRTCPeerConnection says who published it. Attribution comes from the track
name, which is deliberately <deviceIdentity>-<kind>: the subscribe response returns a
mid per track, the client maps mid → userId (the device identity, not
the account ID), and the track event is matched against that map.
This is load-bearing. Keying inbound streams by the browser’s track id instead
means the UI cannot tell participants apart, and every tile ends up rendering
whichever stream arrived first.
One MediaStream is accumulated per participant, since their audio and video
arrive as separate tracks but belong to one tile. The map and the streams are
both cleared on reconnect, because mids are only meaningful within a session.
Reconnection
Signaling drops are expected: servers restart, networks blip, laptops sleep. The client reconnects with exponential backoff and full jitter, so a room server restart does not produce a synchronised retry stampede from every participant. Two details make it actually work:- Each attempt fetches a fresh join token from Meet’s Cloudflare-local
POST /api/meet-call/<meetingId>/token, preserving the current device identity. Join tokens are short-lived and calls are not, so reusing the original token would let a client reconnect for ten minutes and then fail forever on “Reconnecting…”. Membership is re-checked on every refresh. - A reconnect rebuilds room state. The client re-announces itself with
presence.joinand clears its subscription ledger and peer connections, so every remote track is pulled again. Without that it would rejoin a room it could not see.
1000) and a host removal (4403) are decisions, not blips, and
are never retried.
Media survives a signaling drop: the peer connections to the SFU are
independent, so audio and video keep flowing while signaling is down.
Meeting codes
Every meeting has a shareable code and a/r/<code> link, surfaced by the Copy invite button in the call header.
The code is a reversible Crockford base32 encoding of the meeting id rather
than a stored random string, so there is no schema change, no collision risk
and no lookup table — an invalid code is rejected before any query runs.
Decoding tolerates case, spaces, dashes, and the I/L/O/U characters
Crockford omits, folding them onto the digits the sender meant.
Only signed-in accounts with a verified @tuturuuu.com address can create
online meetings. Other signed-in users can follow an invitation and ask the
host to admit them. Sign-in preserves the room link, and an existing profile
display name is reused. A missing name is saved to the profile before joining.
Joining and admission feedback
Meet distinguishes connecting to signaling from waiting for host approval. A confirmed waiting response means the request was sent; the lobby shows approval copy and a cancel-and-leave action without a spinner. After 60 seconds it explains that approval is still outstanding and suggests contacting the host or returning later. It does not resend the request, auto-admit the participant, or treat a slow host response as a connection failure. Host presence is not inferred from the absence of a presence snapshot in a waiting response. The signaling readiness deadline is 10 seconds per attempt, including refreshed token resolution and the server’sready response. Automatic recovery is limited
to 12 retries. The budget resets only after the socket stays open for 30 seconds
after protocol-ready; repeated ready/disconnect cycles still exhaust it. The first
pending admission reconnect uses normal backoff immediately; subsequent attempts are
limited to one per minute, including failed token refreshes. Both the stability
window and throttle use a monotonic clock, unaffected by wall-clock changes. The
server currently re-announces pending admission on every connection,
even for an existing waiting entry; this client limit bounds those notifications
but does not provide server-side deduplication across tabs or page reloads. Late
responses
from expired attempts cannot revive them. Connection errors offer retry and leave;
room-capacity refusals cannot leave a disabled Connecting spinner. Preparing the
page or controls for more than 30 seconds replaces its spinner with an explanation
and a way back. Retrying a join starts from the invitation; it does not grant access.
Admitted participants see confirmation before choosing Join now. Declined
requests and host removal show distinct messages with a way back, rather than
claiming the participant voluntarily left. Ended rooms retain the existing ended
screen. Invalid or inaccessible room links show a privacy-preserving unavailable
message; room-page failures offer retry. Meet’s retained Next routes and native
router use the same feedback components. Shared Meet/Parley call controls ship
English and Vietnamese copy; admission and workspace access rules are unchanged.
Regression coverage: join-flow.test.tsx exercises pending-to-admitted,
pending-to-denied, prolonged waiting, removal, recovery, and loading deadlines.
signaling-readiness.test.tsx covers hung token resolution, missing protocol-ready,
bounded retries, cancellation and late-event fences. room-route-feedback.test.tsx
covers the native route’s bilingual missing/error feedback. These fixtures do not
establish successful hosted SFU/TURN connectivity or reproduce a specific user’s
production account failure.
Meet’s first-paint theme initializer is a literal script, not a serialized
function. Worker bundlers that preserve function names can inject __name helpers
inside functions; serializing such a function with toString() loses those helper
bindings. The Meet theme provider leaves the upstream initializer inert and keeps
its hydrated theme context, while its own initializer applies the same configured
theme before hydration. Native and retained Next providers both use it; other
satellites keep their existing provider. theme-provider.test.tsx reproduces the
helper failure against the installed upstream script and executes the replacement
with saved/system/forced themes. Require exact-commit CI and deployed first-paint
verification; this console repair alone does not prove an authenticated join works.
Call controls and admission
The sending and receiving arrow badges open connection details. The displayed latency is the selected SFU connection’s round-trip time, not a measurement to another participant. Unavailable measurements show a dash. A device stays busy until its SFU publication and peer connection are ready; a local preview alone does not prove another participant receives media. Choose automatic, grid, spotlight or sidebar layouts. Each camera and shared screen can be focused independently or opened fullscreen. Videos fit their frames without cropping. Fullscreen uses a viewport-sized fallback when the browser denies native fullscreen. Camera color filters and soft portrait processing run locally and affect the transmitted video; no image is sent to an AI provider for these effects. Soft portrait is subtle whole-frame smoothing and lighting, not face-aware cosmetic placement. Reactions are temporary and rate limited. The host’s People panel contains persistent approved participants. Admission adds a signed-in identity to this room’s approved list, so reconnects and later visits bypass the lobby. Forgetting approval requires admission on the next visit; removing a current participant also removes their saved approval. Transcript and notes sharing is private by default. The host can enable it for admitted and approved participants, including guests outside the workspace. Every non-host notes read checks the signed room policy on the Durable Object; removing permission blocks subsequent reads. Existing downloaded information cannot be recalled. Transcription mutations remain host-only.Who can read transcripts and notes
Product decision (30 October 2026): a meeting’s transcript and AI notes are readable only by participants of that call and by workspace admins of the meeting’s own workspace. Membership alone is not enough, and an outside guest reads only the calls they were a participant of.
Sharing only narrows participant access; it can never grant access to anyone
who is not a participant or an admin. A participant is an identity the room
service recorded as present or approved for that meeting; there is no
workspace-wide participant list. A guest’s access is scoped to the meeting
whose room approved them and is not carried to another meeting or workspace.
Surfaces: the call and ended-call notes panels, the meeting detail page, the
/api/v1/workspaces/{wsId}/meetings/{meetingId}/review route used by web and
mobile, and the Meet AI session routes all decide through the room policy plus
the admin check, and fail closed (a policy or admin lookup failure never grants
access). The recordings list returns a recording transcript only to the
identity recorded on that session, the host, and workspace admins.
meet_ai_sessions, meet_ai_chunks and private.recording_transcripts hold no
client read grant or policy; only the service role behind these gates reads
them. Recorded audio carries the same spoken content, so it follows the same
rule: the playback route (chunk listing and one-hour signed URLs, which are also
the download URLs) and the audio_chunks listing are limited to the identity
recorded on the session, the host and workspace admins. A non-participant member
and an outsider are denied, and a failed admin lookup fails closed. Signed URLs
already issued stay valid until they expire.
Exceptions: the recordings list and audio playback are stricter than the call
rule, because the database records only the recording identity, not every call
participant. Other participants read that call through the ended-call notes and
do not get the audio. Cost and token
breakdowns remain host-only.
Regression coverage: packages/meet-core/src/features/meeting-ai/server/room-access.test.ts,
packages/meet-core/src/features/meeting-ai/server/notes-admin.test.ts,
apps/web/src/app/api/v1/workspaces/[wsId]/meetings/[meetingId]/review/route.test.ts,
apps/web/src/app/api/v1/workspaces/[wsId]/meetings/[meetingId]/recordings/route.test.ts
apps/database/supabase/tests/meet-transcript-notes-read-boundary.sql, and for
audio the playback route test under legacy-api-routes/.../recordings/[sessionId]/play/
and apps/database/supabase/tests/meet-audio-chunk-read-boundary.sql.
Leaving stops local capture and signaling immediately while recording/notes
finalization continues on the post-call screen. The host can leave alone or end
the room for everyone. Ending is durable, disconnects participants and prevents
rejoining that room. Approved participants can still read shared post-call
notes while the host’s sharing setting permits it. Room policy and approval
state live in Cloudflare Durable Object storage; no self-hosted server or new
database migration is required.
Missing microphone or camera
If enabling a device reports that none was found, the browser returnedNotFoundError before media reached Cloudflare. Connect an input device or use a
browser that can see it. Permission denial and a device that cannot start are
reported separately; check browser/system privacy settings for denied access,
and other apps using the device for startup failures. Repeated activation uses
one notification per device rather than stacking identical warnings.
Recording
A host can record from inside the call. The control creates a row inrecording_sessions through the existing /record endpoint, captures local
audio with MediaRecorder, uploads through the existing /upload endpoint, and
broadcasts recording.state so every participant sees the indicator.
Because it reuses those endpoints, a call recording is indistinguishable from
one started on the meeting page and feeds the same transcription pipeline.
Cloudflare hosting
Meet’s frontend is packaged with OpenNext and runs on thetuturuuu-meet
Worker. Realtime runs on tuturuuu-meet-realtime, with one Durable Object per
meeting. Browsers send audio, camera and screen media directly to Cloudflare
Realtime SFU. Neither Worker requires an internal host, Docker or a Tunnel.
Shared platform authentication and APIs remain at tuturuuu.com, with the
existing managed Supabase database; this does not move platform API ownership.
apps/meet/wrangler.jsonc configures frontend assets, image optimization,
self-reference and the tuturuuu-meet-cache R2 bucket. apps/meet/open-next.config.ts
configures the incremental cache. Build with bun run build:cloudflare from
apps/meet; use NEXT_WEBPACK_BUILD=1 for the existing local Webpack fallback.
The normal CI build uses Turbopack. bun run preview:cloudflare runs the built
frontend in workerd for runtime verification. Run bun run cf:typegen after
binding changes and bun run cf:typecheck to validate the generated environment
with Worker types separately from browser types. CI runs this check after building.
The frontend requires NEXT_PUBLIC_SUPABASE_URL,
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, SUPABASE_SECRET_KEY and
MEET_REALTIME_TOKEN_SECRET. Configure a separate app-coordination signing secret
only if the platform uses one. Public Supabase values are also build inputs;
private secrets belong only in Worker runtime configuration. Realtime requires
its dedicated token secret and the two Cloudflare SFU credentials from its README.
Direct Worker verification requires a wss:// endpoint; ws:// is accepted
only for loopback development (localhost, 127.0.0.1, or [::1]). Endpoints
must not include credentials, a query, or a fragment. Connected participants get
a ten-minute heartbeat grace period for throttled tabs; the Worker closes and
removes silent connections after that bound so ghost participants cannot persist.
All call token issuers and verifiers must share MEET_REALTIME_TOKEN_SECRET;
Supabase service keys are not used for call tokens.
.github/workflows/meet-cloudflare.yaml validates both Workers and deploys
realtime before the frontend on production. It uses
MEET_CLOUDFLARE_API_TOKEN, or the existing account deployment credential
COLAB_CLOUDFLARE_API_TOKEN while bootstrapping. The deployment token needs
Workers Scripts:Edit and Workers R2 Storage:Edit on the Tuturuuu account, plus
Workers Routes:Edit limited to tuturuuu.com. Store the dedicated token in the
meet-production GitHub environment, restricted to the production branch.
A Worker-only token can upload signaling but fails when OpenNext populates the
R2 cache, so verify the complete frontend deployment and authenticated smoke check. Automatic Vercel Meet production
deployment is disabled in the CI switchboard. Preserve the Vercel deployment until the canonical
Cloudflare sign-in and call checks pass so rollback remains available.
Before cutover, deploy the frontend with an explicit verification configuration
that has no custom-domain routes. Verify public rendering and API guards there, then deploy the checked canonical
configuration. Sign-in returns to the registered canonical hostname, so complete
signed-in navigation, API proxying and an authenticated two-participant call
after cutover. Do not change DNS until both Worker versions and runtime secrets
are verified, and confirm the active versions after deployment.
For a direct Worker protocol check, set MEET_CHECK_REALTIME_URL to its
WebSocket endpoint when running integration-check.ts. The same variable works
with media-check.ts --call-controller; localhost then hosts only the test page,
not signaling. The check uses a fresh synthetic room and performs no database
meeting writes. A health response alone cannot detect broken SFU calls.
Native fetch must retain its global receiver in Workers. The SFU client binds
globalThis.fetch to globalThis; calling an unbound native fetch as an object
method throws Illegal invocation, even though Bun-based checks pass.
URL shapes
Meet matches the other satellites: workspace routes sit directly under the workspace segment, with no/workspace prefix.
plans and r are static segments, which is what frees the bare /<wsId>
position — public plan pages previously occupied it, so /<wsId> and
/<planId> could not coexist.
Every retired shape is permanently redirected in proxy.ts:
/workspace/* → /*, /call/<wsId>/<meetingId> and /join/<code> → /r/<code>,
and a bare 32-hex segment → /plans/<planId>, so shared plan links keep working.
Hosting eligibility and call verification
The meetings POST handler checks the creator’s current confirmed authentication email and personal subscription. Verified Plus, Pro, and Enterprise accounts can host regardless of email domain; verified@tuturuuu.com accounts remain eligible
on Free. Workspace membership is required, and Calendar scheduling additionally
requires Calendar permission. Both instant creation and scheduled creation use a
trusted server writer only after these checks; the actor is always server supplied.
The older restrictive direct-client INSERT policy remains in place. No production
migration is needed to unlock paid hosting through the application API.
Existing workspace members may still join calls regardless of their email domain.
Signed-in invitees outside the workspace can join a meeting whose creator is an
eligible verified host. They wait in the lobby until the host admits them; the
invite grants no workspace membership, recordings, or archived transcript access.
Guests see a transcription capability notice before joining and in the
call. Meeting-only access is rechecked by the Cloudflare-local
POST /api/meet-call/:meetingId/token route whenever the call reconnects. The
creator must still belong to the workspace to receive host privileges.
Anonymous invite navigation preserves /r/<code> (including locale) through the
central login and token verification handoff. The call page resolves invite access
directly rather than redirecting non-members through a workspace dashboard. If a
login seems to lose the invite, inspect the nested returnUrl/nextUrl chain and
the call authorization separately.
Signed-in root navigation opens online meetings. Hosts can start an instant
meeting, and members can enter a room code or paste a same-origin room link.
Legacy UUID room links remain accepted.
The client keeps an audio playback element mounted when cameras are off, selects
the screen track while someone presents, and restores the camera after a share
ends. Publishing uses stable senders with replaceTrack for device toggles and
share restarts. Subscribe negotiations are serialized and retried because track
announcements can arrive before the SFU has finished publishing. A ten-second
heartbeat keeps presence alive. Reconnect refreshes the token, preserves the
member’s speaker role for calls, and republishes enabled media.
To test the actual React call controller and participant tile against the live
Cloudflare SFU, run the local-only harness:
http://127.0.0.1:7898/?peer=a and http://127.0.0.1:7898/?peer=b in separate
tabs. Peer A is a host and B is a speaker. The harness supplies synthetic audio,
camera, and screen sources; it does not capture the desktop or microphone.
Verify nonzero received audio energy, zero energy while muted, decoded video
frames, screen-to-camera transitions, two-way chat, host mute, and reconnect.
Stop the harness after testing. It uses scratch room identities and does not
create application database records. This proves the media path through the
real SFU, but does not certify production authentication, OS device permissions,
physical devices, or restrictive corporate networks. Those require additional
checks on the canonical deployed app.
The restrictive insert policy protects authenticated direct database writes.
Satellite app-session requests use a service-role client, so their creation gate
is the API check against the current confirmed email fetched from Supabase Auth; service-role
requests bypass RLS. Neither path trusts handoff or user-editable profile metadata for the
creation domain. The policy is not an additional RLS layer on service-role inserts.
If a browser blocks remote autoplay, use Play meeting audio. Any keyboard or
pointer interaction also retries playback. The controller harness deliberately
rebuilds its client bundle on each reload so ongoing controller edits are exercised
without restarting its signaling room.
Presence stays active while its Durable Object WebSocket is open, even when a
background browser delays heartbeats beyond the normal expiry window. Close/error
handlers remove disconnected participants. The direct Worker integration check
includes a 40-second idle interval to verify presence and host controls survive
browser timer throttling.
The meet-production GitHub environment must allow only the production branch
and contain MEET_REALTIME_TOKEN_SECRET matching the Workers and platform API.
Deployment checks required Worker secret names before rollout, then runs the
signed-token realtime/SFU integration check against the canonical endpoint.
Gemini transcript and post-call notes
Meet’s own Cloudflare frontend Worker serves/api/meet-ai/:wsId/:meetingId and
its /chunks endpoint. These routes do not depend on a self-hosted recording
server. Configure GOOGLE_GENERATIVE_AI_API_KEY as a secret on tuturuuu-meet;
without it, the panel reports that Gemini is not configured. Do not put the key
in public environment variables or the browser. Existing deployment credentials
do not grant permission to transfer a Gemini key to a new destination.
Apply 20260907040000_meet_ai_sessions.sql through the normal database release
process before enabling the feature. A missing schema produces a visible
unavailable state without breaking the call. Browser reads require workspace
membership; only the meeting creator can start transcription, submit audio, or
finalize notes. The server independently verifies the signed Meet app session,
workspace membership, meeting ownership, and same-origin mutation requests.
The host opens Transcript & notes, tells participants that audio will be sent
to Gemini, then starts live transcription. The panel and call header show an
active-session indicator to members. The browser mixes its current microphone
track with received participant audio; it does not request another microphone or
capture unrelated desktop audio. Muted tracks contribute silence. AudioWorklet
emits independent mono 16 kHz WAV chunks about every ten seconds, skipping silence.
Transcripts arrive after Gemini processes each chunk. This is chunked live
transcription, not word-by-word captions, and overlapping speech is not assigned
to guessed speaker identities.
Stop & generate notes flushes pending audio and generates a summary, decisions,
action items, and open questions. Leaving through the call’s leave control does
this automatically for the capturing host. Keep the page open while it finishes.
If finalization fails, leaving still works and notes can be recovered from meeting details.
Abrupt tab closure or a browser crash can lose the current unsubmitted chunk;
return to meeting details and finish the active session to generate notes
from saved text. Interrupted or failed captures are identified as incomplete.
A stuck notes attempt becomes eligible for manual recovery after two minutes;
this can incur another provider charge and the earlier unknown usage remains
visible in statistics. Notes are AI generated and should be reviewed.
Only text, notes, and provider usage are retained in Supabase, not audio files.
After a transcription provider result has been generated and settled, the server
retries only its same-attempt transcript write up to three total times, including
returned storage errors and rejected save promises. The transcript, structured
speaker usage, and cost stay identical; this retry does not regenerate audio or
repeat settlement. A missing attempt row is a conflict, and failed-attempt cleanup
only changes a still-processing row, protecting an uncertain completed commit.
Exhaustion returns a safe failure, not a completed receipt. This is request-local
persistence recovery: a later request may reserve a new failed attempt and incur
another provider charge; there is no cross-request charging guarantee. Synthetic
chunk regressions cover thrown/returned errors, CAS conflict, exhaustion, and the
completed-row cleanup predicate; they do not prove live database or provider behavior.
The capture queue holds up to 60 chunks in browser memory and retries each stable
chunk ID for up to five minutes. A recovery status shows pending chunks. Tab closure
still loses buffered audio; keep the page open until uploads finish. Overload and
unrecoverable chunks remain explicit gaps. Sessions retain the three-hour/1,080-chunk
limit. Apply 20260909153000_meet_transcription_recovery.sql before enabling server
retries: row locks and attempt leases prevent overlapping provider calls and stale
writes. Up to five provider attempts are allowed; uncertain prior costs remain unpriced.
The default model is gemini-3.1-flash-lite. Costs are informational estimates in
USD, computed on the server from Gemini usage metadata and a dated price snapshot:
text input $0.25, audio input $0.50, and output including thinking $1.50 per million
tokens (standard API pricing checked September 7, 2026). The panel separates
transcription and notes costs, submitted audio minutes, and input/output tokens.
Missing provider usage and interrupted attempts are shown explicitly; totals are
not invoice totals and do not debit workspace AI credits. Review
Google’s pricing when changing the
model or rates. Never log raw audio, transcript text, or credentials.
Validation: use the Meet AI API/audio tests, AI package usage tests, a real browser
AudioWorklet check, and the disposable database runner with
--test supabase/tests/meet-ai.sql --typegen packages/types/src/supabase.ts.
The disposable runner copies Git-tracked files; new migrations and tests must be
tracked before running it. Run the Meet Cloudflare build before deployment and
verify a signed-in live transcript plus persisted notes against Gemini afterward.
Production upload and provider diagnostics
If an audio chunk is rejected before reaching the Worker, inspect the request’s Ray ID in Cloudflare Security Analytics. Binary WAV uploads can trigger OWASP949110: Inbound Anomaly Score Exceeded. Meet has a logged managed-rule exception
(6bcb3c2398ca4f2896222b76bc1ae9b2) before the managed rulesets, skipping only rule
6179ae15870a4bb7b2d480d4843b323c in the OWASP ruleset for this expression:
502 response. Worker logs contain only a fixed
failure category and provider HTTP status, never provider messages, request URLs,
credentials, audio, or transcripts. Use those categories to distinguish invalid
keys, disabled APIs, quota, access, location, timeout, and runtime failures.
An accepted chunk is not automatically resubmitted to the provider; preserve its
unknown-cost record when investigating a failed attempt.
Google’s request logs identified the production synthetic failure as
412 FAILED_PRECONDITION: User location is not supported for the API use.
The Meet frontend Worker therefore uses placement.region: aws:ap-southeast-1
to run near Singapore, which Google lists as a supported Gemini API region.
Signaling and media remain on the separate globally distributed realtime Worker
and Cloudflare SFU. This is a Cloudflare placement hint, not an AWS deployment or
a strict geographic guarantee; verify the provider path after each release.
See Cloudflare placement
and Gemini supported regions.
Missing keys are classified as missing_configuration and return a safe 500;
provider failures return a safe 502 with fixed-category diagnostics.
Media connection readiness
SFU_HTTP_425 means the SFU rejected an operation before the session was ready.
After the first offer/answer exchange, subsequent publish and subscribe operations
wait for the browser peer connection to reach connected (up to 12 seconds).
SFU_CONNECTION_TIMEOUT and SFU_CONNECTION_FAILED distinguish a stalled or
failed transport from device permissions. Rejoining creates fresh peer connections.
Receiving a successful SDP response alone does not prove playback: the client
reconciles negotiated receiver tracks with participant ownership and only marks
live, attached tracks subscribed. This also handles receiver reuse without another
browser track event. Production verification should include late guest admission,
rapid microphone/camera activation, and media flowing in both directions.
Muted tracks and media reconnection
Cloudflare expires tracks after 30 seconds without media packets. Keep muted microphone/camera tracks attached withenabled = false, which sends silence or
black frames, so unmuting resumes the existing subscription. Detaching a sender
with replaceTrack(null) silently invalidates that track after the timeout.
Stopped screen shares are explicitly closed at the SFU with force: true,
removed from the publication ledger, and published afresh when sharing resumes.
Media connectivity is independent of the signaling WebSocket. Meet rebuilds a
failed peer after one second, a disconnected peer after ten seconds, or an
unfinished connection handshake after twenty seconds. Brief disconnections that
recover within the grace period retain the existing connection. Publisher
recovery republishes enabled sources; subscriber recovery clears its ledger and
pulls the current room tracks again.
Diagnosing media on another device
Open Connection in the call header to inspect outgoing and incoming media transport states and packet counts. Reconnect media rebuilds both media connections without leaving the room. Copy diagnostics includes only transport states, aggregate media counters, and receiver track states; it omits SDP, IP addresses, track identifiers, participant names, and authentication data. A working local camera preview proves capture succeeded, but does not prove other participants can receive it. If enabling a device shows a transmission warning, record its diagnostic code and the action that triggered it.SFU_HTTP_* identifies
an upstream SFU rejection; SIGNALING_* identifies the signaling connection or
request timeout; RTC_* identifies a browser peer-connection failure. The code
contains no raw provider message, SDP, credential, or meeting content. A successful
retry clears the previous warning for that device. Reproduce on the affected device
before changing transport settings; synthetic sources in another browser do not
establish physical-device compatibility. Browser audio probes should attach the
received stream to a playing media element before measuring decoded audio energy;
packet counts or an unattached analyser alone do not establish working playback.
Participant names and admission
Meet reuses the participant’s saved profile display name. If it is missing, the invite page suggests an available account name and asks the participant to confirm or edit it before opening the realtime session. It saves the name through the current-user profile API, and refreshes the same invite. Later joins reuse the saved display name. A failed save leaves the form open for retry; a temporary profile lookup failure does not invite the user to overwrite an existing name. The lobby separates connection progress from waiting for host admission. Guests can leave while waiting, and their preview is not published until they join. Host admission actions are labeled, and a muted participant has one mute status indicator rather than an additional redundant mute action.Cloudflare browser integrations
Meet must not mount Vercel Analytics or Speed Insights: their/_vercel/* script endpoints are not hosted by the Cloudflare Worker. A 404 for those scripts is separate from SFU media delivery.
The platform user-config API uses the shared satellite app-session audience list in apps/web/src/lib/api-auth-audiences.ts. Keep Meet in that list and in the Rust current-user session targets. A successful local Meet call-token refresh does not verify authorization for proxied platform APIs such as SHOW_VERSION_BADGE.
A recovered publisher announces a new SFU session for its stable microphone,
camera, or screen track name. The room retires older registrations for that
participant and track and broadcasts track.closed before the replacement
track.published, so listeners discard obsolete receivers. Snapshot consumers
also deduplicate old session registrations left by earlier Worker versions.
SFU commit boundary
Both realtime transports useMeetCommandExecutor. SFU operations are serialized
per participant, while other participants and leave/end controls stay responsive.
A track registration or closure is persisted and broadcast only after Cloudflare
returns success without per-track errors. The completed operation is applied to
the latest room snapshot so concurrent host settings are preserved; a participant
who left during the request cannot be resurrected. Closing a track requires the
registered owner to match the signed caller.
Recording finalization targets the specific session with an idempotent status
update, never another toggle. SPA unmount and leave share the same finalizer. The
post-call navigation buttons wait for finalization while capture and signaling
are already stopped. A hard browser/process termination can still interrupt an
upload; do not claim a saved recording without its storage receipt.
Screen audio and in-call controls
Screen sharing requests audio separately from the microphone, including browser hints for window and system audio. The browser picker remains authoritative: capabilities depend on browser, operating system, and the chosen source. Select Share audio when offered. The in-call badge reports whether captured audio is available; a video-only capture still works. Remote screen tiles play shared audio, and muting the microphone does not mute shared audio. Stopping screen sharing stops both its audio and video tracks. The host’s shared audio is included in the existing consent-controlled transcription mixer. The host can click the meeting title to rename it in a dialog, without changing the meeting time or calendar linkage, and hover or keyboard-focus a participant’s name to mute their microphone. Touch layouts keep the action visible. Participants cannot remotely unmute someone. Raised hands use a prominent orange tile badge and outline; reactions use larger, labeled overlays with reduced-motion support. Transcript and Notes have separate tabs, with expandable usage and session notes. Receiving badges distinguish an enabled remote device from an arriving track. A connected SFU receiver with a live track is not proof of packet delivery. Meet checks receiving counters with a 20-second grace period and rebuilds only the subscriber when an expected stream never delivers packets or remains muted and stalled. Automatic rebuilds are limited to one per minute. Missing statistics, normal silence/DTX, and a static screen with an unmuted track do not trigger this recovery. This is a recovery mechanism, not evidence that an affected physical network or device is fixed; verify each audio/video direction on those devices. Packet counters that are unavailable or invalid remain unknown rather than being counted as zero. The tile indicator distinguishes unknown packet statistics from confirmed receiving and waiting states. Ended tracks are excluded from stall recovery. Persisted title announcements are acknowledged by the room and retained across signaling reconnects until delivery; intentional leave clears that outbox. A browser can keep its signaling socket inCLOSING while waiting for a remote
close handshake. Removal, admission denial, and meeting-end messages stop retries
immediately, before the delayed policy close event arrives. Meet watches that state and retries after a five-second grace
period instead of waiting for the browser’s longer timeout. Events from the old
socket cannot change the replacement connection or settle its pending requests.
Intentional leave and promptly received policy-close codes still stop retries.
Media timeout and managed TURN fallback
SFU_CONNECTION_TIMEOUT means ICE/DTLS did not connect to the media service.
A working local microphone preview does not prove that audio can reach the SFU.
Meet now obtains expiring Cloudflare TURN credentials with each admitted SFU
session and installs them on both publishing and receiving peer connections
before negotiation. UDP, TCP, and TLS on port 443 provide alternative network
paths; no self-hosted relay is required. Port 53 is filtered because browsers
restrict it. Direct connections remain eligible, avoiding unnecessary relaying.
Create a dedicated Meet TURN app in Cloudflare and install only
CLOUDFLARE_TURN_KEY_ID and CLOUDFLARE_TURN_API_TOKEN in the
tuturuuu-meet-realtime Worker before deploying this change. The permanent key
never reaches the browser or logs. Only authenticated, admitted participants
can create sessions and receive the 24-hour credentials. Each replacement
session receives fresh credentials. Missing configuration or generation failures
log a fixed warning and preserve direct ICE connectivity. Production checks still
fail if relay credentials are unavailable, so degraded coverage stays visible.
To verify restrictive-network behavior, run the isolated call-controller harness
against the production Worker and open peers with &relay=1. Confirm a selected
relay candidate, incoming audio energy and decoded video in both directions;
repeat with &relay=tls to allow only TLS/443 and rule out direct UDP success. Synthetic
media avoids recording real microphones or cameras. This does not substitute
for checking previously affected physical devices.
Audio works but a remote camera stays blank
The receiving watchdog also handles video tracks with no initial RTP report when another incoming track proves that the browser exposes inbound statistics. WebRTC may create an inbound report only after the first packet, so an absent video report is not always an unsupported-statistics case. A live, muted video track with enabled remote camera and working sibling inbound stats gets a 20-second grace period before subscriber recovery. With no usable inbound statistics at all, the watchdog continues to avoid guessing. Video that receives packets but reports zero decoded frames also gets a separate 20-second first-frame deadline. Incoming byte growth does not reset this deadline. A successfully decoded static camera or screen does not trigger this condition. Disabling media, replacing a publication, or ending its track clears the deadline. Recovery rebuilds the subscriber connection while preserving local publishing and room membership, using the existing one-minute recovery throttle. This corrects missed recovery cases; it does not identify a codec or network fault on a device that has not been inspected.Decoded video with an avatar-only tile
If incoming diagnostics show decoded video frames and a live, unmuted receiver, inspect presentation before resetting the SFU. Tile readiness uses a React external store so an unmute event between rendering and listener installation cannot leave an old avatar visible. Audio and muted video have separate media elements; camera replacement preserves the audio element’s stream. Playback retries on metadata and can-play events after a replaced stream interruptsplay(). The incoming-video
retry action preserves local publishing. Rejected subscription responses and stale
publisher sessions cannot take ownership of a replacement receiver. Unexpected socket closure retains presence and SFU tracks for a 30-second grace period. A resumed device keeps its media connections; intentional leave still releases them immediately.
Call alerts and post-meeting privacy
Join, admission-request, chat and raised-hand changes display actionable in-call notifications. Short sound cues are unlocked by browser interaction, rate-limited, and can be muted from the header. Initial snapshots, reconnect snapshots and the participant’s own actions do not produce notification bursts. Live notes sharing and post-meeting sharing are separate room settings. Both are disabled by default. The host may enable post-meeting access for approved people from the transcript panel or centered ended-meeting screen. Ended invite/detail pages perform a signed read-only room-state check and never open a call socket. Notes open on demand with transcript and notes tabs; there is no join button for an ended room. The authenticated host-only sharing mutation reaches the room Worker over signed HTTP and also works after its WebSocket has closed. AI read responses use explicit field projections. Non-admin readers receive no raw provider usage and null cost/token fields; cost totals and breakdowns render only for the room admin. Sharing permission is rechecked on every notes request.Room settings, recordings, and devices
The in-call settings dialog contains audio/video input, supported audio output, connection diagnostics, notification sounds, and admin-only cost breakdowns. Changing an input replaces its track on the existing call. Output selection usesHTMLMediaElement.setSinkId where supported; unsupported browsers explain the
limitation instead of presenting a non-working selector.
Each browser call generates a device ID. The server derives a signed device
identity from the account and device IDs; media and SFU sessions use that identity,
while admission approvals use the account ID. Joining another device offers a
switch or an additional connection. Switching only removes that account’s other
connections. Keep microphones or speakers muted on nearby devices to avoid echo.
Recording permissions start disabled for participants. Admins can independently
share saved recordings and allow participants to start/stop recording. The Durable
Object holds one recording lease for the room; a remote stop cannot be overwritten
by a delayed start. The recording device composites its available video and mixes
audio locally, and must remain available until saving completes. Recordings stop
at 55 MiB and upload directly to the creator’s personal Drive using a scoped signed
URL, followed by server-side metadata verification. Failed saves offer a local copy.
This is browser capture, not a Cloudflare server-side recording service.
Chat and Mira
Room chat supports Markdown, avatars, and up to five 25 MiB attachments per message. Chat saving defaults on; admins can stop retaining new messages in Settings without deleting earlier messages. The recent 500-message snapshot survives Worker eviction; chat retries reuse a stable message ID to avoid duplicates after lost acknowledgements. Files use the creator’s personal Drive provider and storage capacity checks. Room admission controls attachment access; a signed URL is short-lived. Mention@Tuturuuu or @ttr (case-insensitive) to ask Mira using the recent 40 room
messages. Uploaded file contents
are not automatically read by Mira. The mention author’s personal AI allowance is
checked and reported tokens are deducted from that author’s quota. Mira uses the plan-default Google language model for checks, generation, and deduction. It has current-time, admitted-participant, and Google Search tools; web replies include source links. The browser supplies the requester timezone, and participant counts distinguish people from multiple devices. All generation steps and reported search queries count toward the requester quota. Search cost estimates use paid list rates before project-wide free allowances; missing provider usage or grounding coverage stays incomplete. Cost estimates use its catalog prices and reported cached input; missing cache prices or tiered pricing remain incomplete.
Requests are deduplicated by chat message ID. The internal server-service token
scope is never included in browser join tokens.
Bandwidth and cost attribution
Balanced mode caps camera sending at 900 kbps / 24 fps and screen sharing at 1.8 Mbps / 15 fps. Data saver lowers camera resolution and frame rate, while screen sharing retains text resolution at a lower frame rate. Higher-quality camera mode allows 1.6 Mbps. These are ceilings, not promised usage or minimum bandwidth. Speech has a separate 48 kbps ceiling; screen audio uses 96 kbps. Video shares the available outgoing bandwidth estimate with a reserve for audio. Congestion reduces quality promptly, while recovery requires four measured healthy samples to avoid oscillation. Missing statistics never trigger an upgrade. Video pauses when its share of estimated capacity is too small after reserving speech bandwidth. Sending pauses when the participant is alone and resumes when an audience joins. Sender parameter failures preserve the working transport rather than forcing a reconnect. Initial encodings are capped before negotiation. Admins see client-reported received RTP bytes and server request counters in settings. Estimates use SFU pricing and Durable Object request pricing, checked September 8, 2026, before account-wide allowances or discounts. TURN-to-SFU traffic is not charged twice. Reports are cumulative and deduplicated across samples. Per-device accounting retains at most 4,096 identities per room; reports beyond that limit are marked as incomplete rather than growing storage without bound. These are partial usage estimates, not invoices. RTP payload counters omit transport overhead and reports can miss disconnected devices. Worker CPU, Durable Object duration/storage, R2 operations/storage, and shared plan charges are not yet attributed per room and are shown as unallocated. Reconcile against Cloudflare account billing before using these estimates as a complete budget. Unknown AI costs are shown as missing coverage rather than zero-cost requests. Merely reading a cost panel does not force a Durable Object snapshot write.Diagnosing call preparation
The centered Preparing your call screen checks the account’s device session before opening the lobby. Its request isPOST /api/meet-call/<meetingId>/token;
a failure here is distinct from a media or WebSocket connection failure inside
the call. The browser bounds each preparation request to 12 seconds and retries
transient failures twice. Switching an existing device is not automatically
retried, because it can disconnect that device.
The token handler checks the realtime Worker’s /room-device endpoint with a
five-second timeout. An unavailable upstream returns HTTP 503 with
MEET_REALTIME_UNAVAILABLE and Retry-After: 2, without bypassing device policy.
Use the request status and Cloudflare Ray ID to distinguish an edge block from
an unavailable Worker; a successful /health check alone does not prove the
authenticated device endpoint works. Never collect cookies or bearer tokens.
Cloudflare’s Meet-only managed-rule exception skips cookie-character checks
942420 and 942421 for GET and POST requests to meet.tuturuuu.com. Other rules
and request-body inspection remain active, and exception matches are logged.
Inspect the contributing rules before changing this scope; do not disable an
entire ruleset to resolve a preparation failure.
After the lobby opens, a WebSocket that remains in its initial connecting state
for ten seconds is replaced through the normal bounded reconnect policy. Late
events from the abandoned socket cannot replace the current connection.
Room policy verification
bun apps/meet-realtime/src/room-controls-check.ts verifies lobby chat privacy,
one room-wide recording, private recording/cost access, and same-account device
switching against the Worker. Set MEET_CHECK_REALTIME_URL and
MEET_REALTIME_TOKEN_SECRET; the default endpoint is local port 8799. The check
uses a fresh synthetic room, ends it after success, and never uploads files or
calls Gemini. The production deployment workflow runs this after the existing
signaling/SFU check. Each room-controls connection has a ten-second deadline;
failed or closed attempts dispose their listeners and socket without retrying.
The signaling check’s ALL CHECKS PASSED line does not establish room-controls
success: require its own assertions and the complete workflow result. Lifecycle
regressions live in apps/meet-realtime/src/check-socket.test.ts. Abandoned Mira requests release their per-account pending
limit after two minutes; late provider costs can still be recorded.
Assistant identity and panel interactions
Mira replies carry a server-only assistant marker and reserved sender identity. The Nova avatar and official badge require both; choosing the display name Mira does not grant either. Standalone@Tuturuuu and @ttr
mentions (case-insensitive) open the assistant mini profile on hover, keyboard
focus, or tap. Code and existing Markdown links are not rewritten into
mentions. Paragraphs containing raw HTML are also inert for assistant requests
and highlighting. Chat message toasts are suppressed while chat is open;
admission and other call alerts remain active.
Desktop chat, participants, and transcript panels have a draggable separator.
Focus the separator and use left/right arrows to adjust it with a keyboard.
The meeting settings button is last in the call header. Returning from the
post-call screen invalidates cached meeting lists and room state before
navigation, including personal-workspace aliases, so newly created meetings
are fetched.
Private workspace tool reviews in Meet
When composing an assistant mention, choose the workspace for task, calendar, finance, and time-tracking tools. Personal workspace is the default; billing always uses the requester personal AI allowance. Meet reuses the platform tool definitions and executors, checks membership and permissions on every generation, and only sends detailed schemas for selected tools. Workspace tool calls require the requester to review and approve the exact inputs. Their results remain in a private Mira review; Share with meeting is a separate explicit action. Other participants cannot read, approve, or share those drafts. Draft continuations remain server-side in the room Durable Object and are omitted from room broadcasts and browser review responses. Revisions and an execution claim prevent duplicate approvals from repeating a mutation. If a request remains in an uncertain executing state, do not repeat it; inspect the platform result first. Failed or timed-out reviews can be discarded without making their actions executable again, including after the room ends. Web search runs in an isolated request containing only the explicit question; model-supplied query rewrites and recent room history are not forwarded. Questions containing known participant names or room-title terms are blocked at that boundary. Missing grounded sources produce an unavailable-search result. Web search is disabled while continuing private workspace results. Unsupported platform-only tools such as E2EE key setup, file conversion, and client UI controls are not exposed in the Meet tool set. Verify private reviews with the production React compiler enabled: let a review arrive inline while its detail query is still loading, then deny the action. The list can load before the detail query, so callback inputs such as the revision must remain guarded during that interval. An uncompiled development fixture alone does not cover compiler-generated callback dependency reads. Private reviews appear inline beneath the request that created them. Approval, denial, and the separate sharing decision stay requester-only; orphaned reviews whose request has left the retained chat history appear at the end of the thread. Provider citation IDs are resolved only against their exact returned sources. Older unmapped references show an unavailable-reference badge rather than a guessed link, and the source footer is displayed as an expandable list of source cards. Source metadata is limited to eight URLs and 4,000 characters within the 16,000-character message limit; oversized prose is safely truncated. Empty final synthesis reuses only an available grounded public-search answer, without another paid call. Otherwise generation fails visibly after incurred usage is accounted for; pending approvals stay private. Legacy sources-only replies display an incomplete-answer notice. Sources alone never count as a completed answer.Fast return to ended meetings
Meeting cards remember an observed ended state on the current browser, scoped to its signed-in account. Only meeting IDs and observation timestamps are retained, for up to 30 days and 256 rooms per account. Returning from an ended call updates this hint immediately, so known ended cards do not queue another room-state request or poll the realtime Worker. Active and unknown rooms still refresh. The cache is only a display hint: opening notes or transcripts always performs server permission checks. No room names, chat, transcripts, or access grants are stored in this cache, and unavailable browser storage does not block navigation.Echo and microphone processing
Settings → Devices exposes native echo cancellation, background noise reduction, and automatic voice leveling. All three default on. Changes apply to the current microphone and subsequent device switches in that call, without unmuting it. The status under each control comes from the captured track’sgetSettings();
a requested setting is not proof that a browser or device applied it.
Screen capture excludes whole-system audio and requests restrictOwnAudio to
avoid returning meeting playback to peers. These are browser hints, so browser
compatibility still matters. Share a browser tab with audio when presenting media.
Each remote camera or screen feed also has a local mute control: it silences only
that feed on the listener’s device, without changing room participant permissions.
For persistent echo, identify the participant or shared-audio feed returning the
sound. Enable echo cancellation on the source device, use headphones, or mute
additional microphones and speakers in the same physical room. Native processing
cannot guarantee cancellation across multiple nearby devices. Verify actual
speaker/microphone calls separately from synthetic audio and unit tests.
Nearby participants and same-room audio
Meet automatically pauses the later device’s microphone when the same verified account already has an active microphone in the call. For different accounts, a local audio monitor looks for repeated matching speech dynamics and changing frequency patterns between existing microphone tracks. Only the later device backs off, using join time and device ID as a stable tie-breaker. This avoids two devices muting each other. The microphone is disabled before a compact audio-control popover suggests Share nearby audio. Closing the popover keeps the microphone paused. Confirming sharing also silences this device’s meeting playback, voice assistant and notifications; video, chat and notes remain available. Use my microphone, or explicitly pressing the microphone control, restores this device’s own audio. Device replacement and pending permission requests cannot reopen a paused mic. If the identified primary device leaves, local playback resumes and the microphone stays paused until the user chooses to speak. Analysis runs only on already-enabled microphone tracks, never screen-share audio. It creates no extra capture, cloned tracks, recordings or network payloads, and compares at most eight incoming microphones. Several consecutive matching windows are required; silence and steady tones are insufficient. An explicit override suppresses the current device pair. Unsupported Web Audio falls back to native browser echo cancellation and the manual sharing control. This is a conservative audio-overlap heuristic, not proof of physical proximity or speaker identity. It does not use IP addresses or merge independent recordings on the server. Headphones and one primary microphone/speaker pair remain useful for rooms where acoustic conditions defeat detection. The meeting deadline appears as a compact clock button with a circular countdown ring. Hover or focus shows the remaining time and localized deadline; tap opens the same details. The ring represents the time remaining when this device joined, while the deadline always follows the server’s room expiry. Resource-limit errors still appear as actionable notices.Audio recovery and meeting previews
In call settings, Restart microphone reacquires the selected input and reconnects media without leaving the room. The control remains disabled while muted and never intentionally unmutes the user. Use this when the browser input degrades; synthetic playback tests do not verify physical microphone acoustics. Transcription retries receive their five-minute window when the upload starts, not while waiting behind another upload. Credit admission failures stop immediately and require the user to check AI Hub; they do not advertise a five-minute network reconnection. Other buffered retry feedback describes transcription retry rather than claiming the connection failed. Notes generation failures retain saved transcript chunks for an explicit retry. Empty saved speech completes with empty notes, zero cost and no provider invocation, even when capture is incomplete; missing-audio warnings alone do not admit generation. Real saved speech still generates with its incomplete warning. This applies to Meet and Parley notes finalization and is covered byserver/sessions.test.ts. The failure UI uses localized fixed
guidance with allowlisted HTTP status and provider category diagnostics; raw
provider bodies, credentials and meeting identities are excluded. Regression
coverage: chunk-recovery.test.ts, use-meeting-ai.test.ts,
server/sessions.test.ts and failure-toast.test.tsx. The per-session notes warning identifies
incomplete results without repeating a global warning after the call.
Room preview images are generated from the same explicitly public meeting policy
as link metadata. Image responses use private, no-store; revoked public access
falls back to generic branding on the next request. External chat clients can
retain their own old previews. Preview descriptions omit repeated titles and UTC
times because crawlers cannot know the recipient’s timezone. Opened meeting pages
and the rightmost call timer use the viewer’s browser timezone and locale.
For intermittent outgoing speech stutter, capture connection diagnostics before
refreshing. Audio streams include jitter (milliseconds), reported packet loss,
and incoming concealment counters when the browser exposes them. Outgoing values
come from the SFU’s remote-receiver feedback; they describe that transport hop,
not every listener’s playback. Unsupported metrics remain unknown, not zero.
Compare successive reports to distinguish ongoing loss from historical totals.
This report includes no audio, addresses, SDP, or raw track identifiers. Stuttering
is distinct from low microphone volume; reconnection is a recovery action, not
proof of the underlying cause.
Worker rendering verification
The pinned OpenNext adapter patch preserves component function names during its server bundling step. React uses these names to resume partially prerendered boundaries; renaming a component can trigger React 419 recovery even when the HTTP response is 200. Keep this patch when updating the adapter until its bundler preserves names upstream.scripts/ci/meet-cloudflare-render.test.js reproduces
the failure with a real React prerender/resume cycle and checks the installed
adapter configuration.
After a Meet deployment, verify a fresh browser load as well as the signaling
checks. Check page errors and the rendered interface; a successful HTTP response
or a page that recovers after refreshing does not prove server rendering passed.
Mobile screen sharing and notifications
Native Meet presents join, room chat, and host admission-request notices with a shortcut to the relevant sheet. Initial presence and replayed chat stay quiet; reconnecting does not repeat previously seen messages or pending requests. Hidden browser tabs retain at most the newest 50 notices and display them once when the tab becomes visible; disconnect, room end and lost admission discard that queue. Obsolete admission requests are omitted. Notification sound can be disabled separately from meeting audio. OS volume, silent mode, and Do Not Disturb may suppress the sound. The floating navigation bar owns Leave. Hosts retain the leave/end decision; ending the meeting remains restricted by server authorization.
Run synthetic fixtures without joining real meetings or publishing real screen,
voice, or chat. Validate failed publication, interrupted consent, removal, room
end, denied admission, reconnect, and repeated start/stop. Device testing must
use controlled fixtures and explicit capture consent. No signing entitlement or developer-account capability is added. A returned WebRTC
track alone does not prove ReplayKit produced frames; verify actual receiver frames
before declaring native capture acceptance complete.
References: Android media projection,
Apple ReplayKit, and
browser display capture.
The installed WebRTC socket implementation previously left a listener open when
capture stopped before its first connection.
ios/MeetSocketConnection.m is a
repository-owned copy with bounded cleanup changes. The Podfile selects it in
the generated flutter_webrtc target and fails closed if the expected source
layout changes; it does not mutate shared Pub cache files. Revalidate this patch
when upgrading the plugin. Native CI must compile/link this replacement.
The iOS reader requires a minimum 10 KiB body to avoid overreading coalesced tiny
frames. Meet pads JPEG trailing bytes; an independent reader model passed 100
synthetic fragmentation/coalescing cases with 10 frames each. The installed
macOS CoreImage runtime decoded a synthetic 8×8 JPEG padded from 707 bytes to
10 KiB and rendered the expected red pixel. iOS receiver-frame decoding and
interruption on physical devices remain release acceptance gates.
The draft parity implementation at fe6b020b95b2799fad8314969784d06ccd7fadda
passed exact-head Android, iOS simulator, macOS, Linux, and Windows CI builds,
mobile analysis/tests, TypeScript type checks, and lint. Android artifact
11182160313 belongs to run 36899884533; iOS artifact 11181837360 belongs
to run 36899884528. Build success confirms native compilation and linkage,
but does not establish capture acceptance. Revalidate subsequent changes at
their own exact head.
Those development CI artifacts use the default localhost API/Supabase settings
and do not inject a Supabase public key. They have no authenticated synthetic
Meet room or SFU fixture. Two-peer acceptance therefore requires a separate
fixture artifact configured for an isolated test service, synthetic host/guest
sessions, and its existing authorized SFU credentials. Do not create accounts,
change production settings, or substitute a real meeting to bypass this gate.
The available local runtime inventory contains an Android 11/API 30 image and
iOS 27 simulators, with no attached physical devices. Android 14+ OS-chip/lock
revocation and physical iPhone ReplayKit background/rotation/receiver-frame
acceptance remain explicit release gates.
Android’s installed WebRTC plugin ignores MediaProjection.Callback.onStop.
The repository-owned android/overlays/flutter_webrtc capturer forwards that
callback to the Meet bridge and releases the capture. Each capturer claims an
immutable owner; repeated callbacks and callbacks from a previous share cannot
stop a new share. The assistant’s separate projection service does not claim
this owner. Gradle copies plugin Java sources into generated build storage,
replacing exactly this file after checking the reviewed version and upstream
SHA256. It fails closed on an upstream change and never modifies the Pub cache.
Revalidate this overlay when upgrading WebRTC, and require exact-head Android
CI plus OS Stop/lock/restart fixture acceptance for overlay changes.
Capture stops are generation-scoped and preempt media negotiation. Native revoke
and track-ended events trigger one cleanup per capture; a stalled screen track
does not discard the camera. Android engine detach stops its claimed capturer.
The iOS listener accepts one broadcast connection and closes owned descriptors
with Core Foundation socket ownership enabled. Browser Stop retains its capture
until unpublication succeeds, so a failed unpublish can be retried. Regression
coverage lives in meet_screen_capture_test.dart, meet_media_health_test.dart,
local-media-controls.test.ts and use-call-notifications.test.ts.
The iOS notice uses an app-owned short WAV loaded through Flutter’s asset lookup
and registered as a UI System Sound, rather than a private numeric system-tone
identifier. It plays only while the app is active and does not change the call’s
audio session. Apple’s System Sound Services
owns sound-effects policy. Device mute/Focus behavior remains part of the user’s
post-merge physical acceptance.
Collaborative programming
The in-call coding control opens the shared Learn programming/playground workbench with file cursors and transient pointers. Hosts select the resource; the server checks admission and resource access before issuing tickets. A verified@tuturuuu.com admin elevates admitted participants only within that room.
See programming playgrounds for
Drive ownership, limits and the intentional entitlement exception.
Web screen capture depends on browser getDisplayMedia support. Android and iOS
browsers that lack it can receive screens and co-program, but cannot open a
native screen picker. Missing support and capture denials now display guidance.
See mobile capture diagnostics.
Native mobile sharing
Tuturuuu mobile calls use Android MediaProjection with a foreground capture service and iOS ReplayKit with the broadcast extension. The in-app Share screen control starts native OS consent and publishes the resulting track through the same meeting SFU. Native stop, meeting exit and revoked admission stop capture. On unsupported mobile browsers, Open mobile app preserves the room code in a verified platform app link. The app opens the native call; capture still requires a separate user action and OS consent. Tokens and capture permission are never carried in the link.Collaborative documents and mobile parity
Every admitted Meet participant can edit the room’s private rich-text document. This document does not require a paid playground entitlement. It belongs to the meeting workspace and is created once per meeting. Cloudflare merges and persists Yjs updates, then checkpoints the rich-text content to the private workspace Document. Cursor labels and selected ranges use authenticated account identity; multiple connections from the same account remain separate sessions. Editing the workspace Document through another surface creates a conflict: a room checkpoint never overwrites that external edit silently. The durable room state is retained for reconciliation. Web and native editors display a backup conflict warning; transient checkpoint failures display a delayed-backup warning. Editing continues, and a successful checkpoint clears the warning. Rejoining restores the current backup status. The Worker rejects client-authored backup status notifications. The room Durable Object posts each checkpoint to the platform API from a Workerfetch, which carries no User-Agent. The web API proxy treats an anonymous
/api/* request without one as scanner traffic (HTTP 400, with an egress IP
block on repeat), so an unidentified checkpoint never reaches its route and the
room keeps retrying and re-announcing “backup delayed”. Behind that guard, the
proxy’s account-assurance step treats every other bearer token as a Supabase
access token and answers 401 before the route runs, which rejects the Worker’s
HMAC ticket just the same. Every Worker-to-platform call therefore sends the
shared service headers from packages/realtime/src/core/service-request.ts, and
the proxy’s default trusted-bypass rules exempt exactly the two checkpoint routes
for a POST whose bearer verifies as an unexpired realtime service ticket with
the audience and kind bound to that route (tuturuuu.channels /
document-checkpoint for documents; tuturuuu.collaboration / checkpoint or
runner-files for programming; packages/utils/src/realtime-service-ticket.ts).
Other methods, path variants, audiences, kinds and ordinary JWTs fall through to
the normal proxy checks. The route verifies the same ticket again together with
its topic, ids and version. A ticket may be replayed within its 30 s lifetime;
the route’s checks decide the outcome and tickets are not single-use. The checkpoint route passes the private RPC status through (a 40001
conflict is HTTP 409, not 503) so the room reports a real conflict instead of an
endless delay. Regression coverage:
apps/meet-realtime/src/channel-checkpoint-request.test.ts (request identity and
status mapping in the Durable Object), realtime-service-ticket.test.ts (proxy
exemption bounds) and
apps/web/src/app/api/v1/realtime/documents/checkpoint/route.test.ts.
The shared document surface is the panel itself: padding lives on the editable
area, the writing surface fills the panel height so a click below the last line
still places the caret, and the surface shows no focus ring (the caret, remote
cursors and toolbar state convey focus). document-panel.test.tsx asserts that
class contract because jsdom computes no layout.
Native mobile opens the same shared web rich-text and programming editors inside
an authenticated app-owned WebView. This preserves Yjs, formatting, remote
cursors, pointers, selections and reconnect behavior across web and mobile.
The app bridges a small allowlist of meeting-scoped API actions; it never sends
platform tokens to editor JavaScript or runner preview content. A separate
loopback capability serves private preview assets. Android denies mixed content;
Chromium treats loopback as trustworthy, preserving the private 127.0.0.1 preview exception.
External navigation and previews cannot invoke the bridge; calls continue.
Native collaboration pins the account separately from the admitted device ID.
Account/device changes, revoked admission and ended calls suppress late results;
preview capabilities never transfer access. Server admission remains authoritative.
Task cubits cancel subscriptions and close only default-owned clients/APIs;
injected clients remain caller-owned. Refresh cleanup failures reconnect; disposal fences late cleanup.
Navigation denies malformed, hostless and non-HTTP schemes without throwing.
One-shot broadcasts preserve ticket/handshake errors; subscribed channels reconnect.
Per-send socket setup remains a tradeoff; shared-channel reuse is not implemented.
Coverage: meet_collaboration_owner_test.dart, meet_preview_proxy_test.dart, meet_collaboration_policy_test.dart, task_broadcast_lifecycle_test.dart and cloudflare_channel_test.dart.
Both clients refresh short-lived channel tickets on the existing socket. Actual
reconnects reauthorize admission and replay missing document updates. Expired
access closes quiet connections; disconnected editing and persistence failures
must remain visible. Room programming editors can save and run the selected
resource; only its owner can grant permanent collaborators outside the meeting.
Regression coverage includes the real local Cloudflare document/channel probes,
shared caret/range tests, mobile channel tests and meeting admission/entitlement
checks. Native WebView bridge, private-preview and MediaProjection/ReplayKit
acceptance additionally require configured exact-commit mobile CI artifacts.
In-call document workspace
Meet and Parley explicitly enable the rich-text editor collaboration binding with the room Yjs document and provider. This applies to every admitted editor: passing a provider without enabling the binding must never create a local-only document.document-panel.test.tsx guards the binding; shared provider tests cover peer
updates and reconnect reconciliation. Hosted multi-user verification remains a
separate delivery gate.
Meet and Parley render the shared document editor edge to edge, without a separate
Shared document heading or padding outside the editor. The editor keeps its own
text padding. Backup status sits in the editor toolbar; secondary formatting,
media, and copy controls open from Tools, while bold, italic, and strike remain
directly available. Other shared-editor surfaces keep their full toolbar.
Healthy live synchronization stays quiet. Reconnecting
is announced as a status; delayed or conflicted backups use a keyboard-focusable
warning icon with an explanatory tooltip, without pushing the editor downward.
Document and programming controls use the same icon-button tooltips as media
controls, and meeting action triggers expose consistent tooltips. No room access,
checkpoint behavior, or document broadcasting policy changes.
Regression coverage: document-panel.test.tsx checks the toolbar status placement
and seamless editor container; fixed-toolbar-contents.test.tsx exercises actual
Tools opening and formatting actions alongside the unchanged full toolbar.
document-status.test.tsx verifies quiet healthy state, accessible reconnection,
and distinct backup-warning labels without inline alerts.
Programming preparation and resource discovery
Meet web hosts open Programming into a blank Python project when the room has no selected resource. Opening an existing selection reuses it; participants only join the host’s selection and never create a project automatically. The existing authorized collaboration ticket, hosted runtime, Run, and problem Test controls remain responsible for connecting and executing code. Creating a meeting project keeps its runnable entry file empty; standalone playground template defaults are unchanged. Internal checkpoint uploads explicitly permit zero-byte source files only with valid positive Drive quota and verified usage accounting; over-quota or unknown quota still blocks creation. Signed browser uploads retain their nonempty file policy.empty-playground-storage.test.ts exercises actual capacity guards
for both Drive providers, including blank creation, quota denial, and cleanup.
Hosts choose existing personal projects or authorized workspace problems through
shared UI dropdowns. Problem discovery supports cursor pagination, while project
discovery checks the response actor against the signed-in account. The native
collaboration bridge retains its existing explicit resource-ID selection until
that bridge provides authenticated discovery metadata. Regression coverage lives
in prepare-programming.test.ts and playground-service.test.ts.
Private My day panel
During a Meet call, My day shows the current authenticated account’s assigned Tasks feed and next seven days of its personal workspace Calendar. It is closed by default and local to that participant; it is never room state, signaling, or shared document content. Team meeting workspaces are never used as a fallback. Account identity is verified before and after reads, and queries are scoped by account ID. Calendar collection GET accepts a signed Meet session only for that actor’s personal workspace, with MEMBER and manage_calendar permission checks. Team-workspace reads and Calendar mutations do not gain Meet-session admission. Temporary failures retain same-actor, same-workspace source snapshots with an explicit stale-source notice; definitive access denials clear that source. Dedicated Tasks and Calendar links open the full apps separately. Parley does not show this Meet-only action.personal-tools.test.ts covers personal scope, actor changes, missing personal
workspaces, and partial source availability. calendar-event-meet-read.test.ts
uses signed sessions to cover personal scope, actor/audience/signature/expiry
denials, membership and permission failures; Calendar events route tests keep
the audience opt-in exclusive to GET.
Meeting-list startup and hosting eligibility
The signed-in Meet workspace heading renders after session and workspace membership are verified. Hosting eligibility is resolved inside the content Suspense boundary, so a slow verified-identity or account-tier lookup does not block the heading. Creation controls remain unavailable until that lookup confirms eligibility; lookup errors retain the unavailable notice and never grant creation. Session and workspace redirects stay outside streaming boundaries. This applies to the workspace Meetings list, not room admission or meeting creation authorization. Regression evidence:authorized-meetings-content.test.tsx exercises deferred identity reads, workspace
denial, eligible accounts, restriction notices, and failed eligibility reads.
Measure signed-in server response and meaningful-content times separately. A fast
public homepage is not evidence that an authenticated app is fast, and a Suspense
skeleton is not a loaded meeting list. Re-measure the deployed source after rollout.
Meeting-detail policy and permission reads
After the signed-in actor, joined workspace, and workspace-scoped meeting are verified, Meet details starts the read-only room-policy lookup and workspace permission lookup together. The policy still decides the ended-room shortcut before any Calendar event access; Calendar links continue to require the existingmanage_calendar permission. Permission failures are captured immediately, ignored
for ended rooms as before, and propagated only after a successful active-room
policy lookup. Policy failures retain precedence. No reads start before the
workspace and meeting gates, and this changes neither room admission nor creation.
page-loading.test.tsx covers deferred overlap, scoped identity, ended-room bypass,
and rejection precedence; meeting-calendar-event.test.ts retains denied-access
coverage. This source change still requires deployed meaningful-content timing to
establish a customer-visible latency improvement.
Collaboration layout and document presence
Opening Meet’s shared document or Programming keeps participant cameras and shared screens visible alongside the active content. The existing layout selector also controls collaboration: automatic/sidebar uses a compact desktop rail, spotlight emphasizes content with a narrower rail, and grid balances content and participants. Narrow screens keep a horizontally scrollable filmstrip beneath the content. These choices are local to the active call; they never alter admission, media publication, or another participant’s layout. The same media stage and keyed camera/screen tiles stay mounted when content, layout, or focus changes. Local playback mute state and media element identity remain intact, and no second stage duplicates audio. Regression evidence:collaboration-stage-layout.test.tsx exercises real participant tiles and
camera/screen audio elements through content and layout transitions.
Shared-document avatars represent users in that document provider’s actual
collaboration awareness, not everyone connected to the meeting. Clients with the
same authenticated identity appear once; departures and provider switches remove
old identities, and disconnected document transport hides the roster until it
reconnects. Four avatars are shown in the editor toolbar beside backup status, with an accessible overflow count.
document-panel.test.tsx verifies that the live provider awareness is connected
to that toolbar roster.
document-presence.test.tsx uses real awareness updates for join, rename,
departure, duplicate clients, malformed identity fields, and provider/disconnect
transitions. Source fixtures do not establish deployed multi-client delivery.
Recovering the final AI notes write
Meet retries a failed final notes persistence step at most twice after its initial attempt, reusing the already generated and settled notes, usage and cost. These retries retain the same session/attempt predicate; they do not repeat provider generation, credit admission or billing. A missing compare-and-set row is a conflict and stops immediately. Failure cleanup only changes a still-processing row, so an uncertain transport response cannot demote already completed notes. If every persistence attempt fails, the request reports failure without claiming that notes were saved. Saved transcript chunks remain unchanged. This is bounded recovery within one request, not durable cross-request output recovery: a later explicit retry can generate and bill again. The existing unpriced-attempt counter also remains conservative when generation succeeded but persistence failed; this packet does not reconcile that counter against settled billing runs.server/sessions.test.ts reproduces successful generation followed by a returned
storage error or a thrown unknown-commit error, and covers equal output/usage/cost,
same attempt predicates, exhausted recovery, safe errors and immediate conflict.
These source fixtures do not identify the original incident’s provider status or
establish deployed database/provider delivery.
Transcription capture scope
Transcription capture and its pending uploads belong to one verified actor, workspace and meeting lifetime. Admission changes only when that lifetime commits; a suspended or abandoned render cannot revoke the still-committed capture. A cleanup epoch also rejects operations held across lifecycle cleanup/replay. Queued overflow finalizers and duration callbacks check their captured epoch before calling the current finalizer, so an old callback cannot end a replacement capture. Departing that scope disposes local audio and aborts recovery. Returning to the same IDs creates a new lifetime; delayed old responses cannot install capture, alter current state, finalize an old session or refetch into the current view. The call shell also binds its account ID to the verified actor. Requests already admitted by the server are not promised to be cancelled or rolled back. A delayed start allocation received after departure does not issue an authenticated finalizer from the new scope. That server session remains subject to existing reservation expiry and authorized recovery. Same-scope finishing still waits for the final worklet flush, batch drain and queued uploads before generating notes. Explicit retries of ended sessions remain available to the current actor.use-meeting-ai-scope.test.tsx exercises held start/upload responses across actor,
workspace and meeting departures, including switches away and back, new capture
survival, retained callbacks, missing/mismatched actor admission, suspended
transitions, held preparation across cleanup/replay and old overflow/duration
callbacks after an Activity hide/show effect replay. Existing
capture, batching and recovery tests retain final-audio ordering coverage. This
does not establish the original incident’s reconnect/provider cause or real
Radix workspace-selector interaction; those runtime checks remain separate.
Public deployment identity
Only exactGET /api/build-info skips Meet app-session refresh so a
credential-free deployment probe can read the bundled production identity. The
shared API guard still applies, including security and abuse controls. Other
methods, trailing-slash or prefix lookalikes, and private APIs keep their existing
session admission; this exception grants no meeting or workspace access.
The production verifier requires the exact app, full source SHA, production
ref, and environment. HTTP failures report only a validated numeric status,
never response bodies or raw transport diagnostics. Proxy regressions cover
anonymous and stale-session GET admission, the real metadata handler’s no-store
response, protected API/method/lookalike denial, and preserved MFA/rate guard
failures. A 403 without an app-assurance or challenge marker remains unclassified;
these source fixtures do not establish the blocking layer or deployment identity.
Assistant workspace choices
Meet and Parley’s shared assistant picker shows compact loading, lookup-failure, and empty states in English and Vietnamese. Choices stay disabled until private visibility and workspace data are available; guest and hidden workspaces remain excluded. Lookup failures expose a safe localized message and an explicit retry through the existing actor-scoped visibility and workspace queries. They never expose a raw transport/provider error or grant membership. Actual Live dialog regressions cover Radix pointer selection, personal/member context, focus retention, loading/error/empty feedback, explicit retry, and a held account A→B→A lookup. Pointer selection passes this fixture; the demonstrated repair is missing state feedback, not proof of the reported production portal failure. Browser/provider runtime verification remains separate.Offline room document checkpoint budget
Meet’s private rich-text document keeps its durable unsaved state when checkpoint callbacks fail. Without a currently valid editor connection, it reserves at most three automatic attempts within two minutes, at least 30 seconds apart. Reservations persist before callback I/O; duplicate alarms, restarts and failed completion writes cannot replenish them. Exhaustion or age expiry stops automatic checkpoint work. An authorized editor rejoin can recover the retained document; a viewer rejoin cannot restart the stopped job. Active editor ticket leases keep their existing checkpoint behavior. This rule applies to meeting-document channel rooms, not programming project checkpoints, media cleanup or a platform spending cap. Regression evidence:channel-retry-budget.test.ts counts operations and tests
failure/clock/restart/recovery boundaries; test-fixtures/channel-retry verifies
reservation persistence against real local Worker SQLite with controlled callback
failures. Hosted alarm delivery and actual workspace storage remain separate gates.
Host identity and transcript reads
A meeting’s creator is immutable after insertion, including when a member edits other meeting fields. Clients cannot transfer or clear that host identity. Meet and Parley transcript sessions and chunks are read through the application API. Direct anonymous and authenticated Data API reads are denied, including for hosts. The API retains its host, workspace, room-sharing, and participant checks before service-role reads. All first-party transcript consumers use that API; room sharing remains the authority for non-host access. Database coverage:meeting-identity-transcript-access.sql and meet-ai.sql.
Application coverage: packages/meet-core/src/features/meeting-ai/server/access.test.ts.
Private transcription evaluation
The M1 transcription evaluation CLI lives at packages/ai/src/meetings/eval; its README defines the private manifest, environment variables, bounds and scoring contract. Run it only with consented clips and UTF-8 references stored outside Git. Reports also stay outside Git and contain aggregate counts and scores without transcript or glossary text. Gemini Flash-Lite and an explicitly configured Flash candidate are evaluated through an evaluation-local SttProvider boundary. OpenAI gpt-4o-transcribe requires explicit evaluation opt-in and is never wired to production traffic. The harness does not choose or change the production model. D1 requires real consented corpus results and coverage review; synthetic regression tests establish the metrics and privacy boundaries, not Vietnamese provider quality.Legacy meeting and recording access
Legacy meeting lists and recording session metadata remain readable by full workspace members. Audio chunks (listing and playback) are limited to the recorded identity, the meeting host and workspace admins; see Who can read transcripts and notes. Anonymous users have no direct privileges on these legacy tables; guests and removed members do not gain access through ownership. Private recording transcripts continue through the existing service-role API boundary. Meeting UPDATE and DELETE require the meeting creator or the existing workspaceadmin permission. Recording session INSERT, UPDATE and DELETE require the
recording creator (user_id) or that workspace permission. This recording admin
exception is intentional; ownership does not grant writes after membership ends.
Existing sessions cannot change their creator or parent meeting. In-call room
recordings in Drive and Meet AI sharing rules use their existing access gates.
The legacy recording controls show Stop only for a creator or admin. Both the
recording toggle and direct status/delete APIs verify permission before writing
and require a returned row. A denied or zero-row update returns HTTP 403; the UI
keeps the active recording and explains that it has not stopped. Active-session
queries include the creator/admin capability rather than inferring it from
workspace membership. Web and mobile consumers keep the existing read response
fields; can_manage is additive.
Regression coverage: legacy-meet-rls.sql, the meeting identity/transcript pgTAP
suite, the recording route unit tests and meeting-actions.test.tsx. Rollbacks
drop only the five meeting/session write policies, the recording identity trigger
and its function. Privilege revokes, the authenticated audio policy target and
creator/admin audio write fences remain in place, including during rollback.
Legacy audio chunk INSERT, UPDATE and DELETE require the parent recording creator
or the workspace admin permission. Audio chunk SELECT is fenced by the
restrictive policy legacy_audio_chunk_select_participant
(20261030110000_restrict_audio_chunk_reads; the rollback only drops that
policy), and the playback API applies the same check before it lists or signs
audio. The chunk and complete-recording upload APIs check the write capability
before parsing or storing audio. Meet mounts recording capture only for that
capability. A viewer who did not take part can read permitted recording metadata
but not the audio. Coverage is in legacy-meet-rls.sql,
meet-audio-chunk-read-boundary.sql, the playback route test and the upload
route unit tests.
Transcription room protocol
Meet and Parley share the transcription protocol inpackages/realtime/src/meet.
This protocol milestone adds no capture or UI behavior. A host sends
transcription.state {state: {active, sessionId, startedBy, language, consentRequired}}.
Language is auto | vi | en | vi+en; consent is required. Active state requires
a session UUID and the initiating host’s participant UUID. Only admitted hosts
may change it, regardless of delegated recording or participant-management scopes.
An admitted participant sends transcription.consent {userId, allowed} using
their own participant/device UUID, including for guests. Account IDs and other
participants’ IDs cannot stand in for that identity. Missing consent means
excluded; false withdraws consent. A new session clears the prior roster.
Only that participant and admitted hosts receive live consent decisions. Client snapshots contain only the participant’s own decision and aggregate counts, including for hosts. Transcription
state reaches admitted participants and replays on admission or reconnection.
Waiting sockets receive neither state nor consent.
The existing room Durable Object validates these messages and executes the shared
reducer through its serialized command executor. Its broadcast path excludes
unadmitted sockets; consent uses direct delivery to admitted hosts rather than
the broader participant-manager audience. Existing clients ignore unknown server
types in the call reducer’s default branch, so realtime can ship first.
Regression coverage lives in transcription.test.ts,
transcription-routing.test.ts, and transcription-compat.test.ts.
In-room consent survives DO hibernation only under the active session UUID and is cleared on session or room end. This is not the legal consent record; private.meet_ai_consents belongs to M1-B/M1-C. WebSocket messages over 256 KiB (UTF-8 bytes for text) are rejected before JSON parsing. Legal consent persistence, audio filtering and notices belong to subsequent milestones.