Implementation Contracts
This page records implementation contracts that affect correctness across modules. Read it before changing HTTP/gRPC handlers, Provider playback generation, playback workers, read-replica routing, file storage, or SQLx queries.
Transport and Impl Layers
Section titled “Transport and Impl Layers”HTTP and gRPC are transport adapters. They parse paths, query strings, JSON/protobuf bodies, headers, status codes, and streaming bodies, then call synctv-api/src/impls.
Shared behavior belongs in impls and synctv-core:
- permissions and actor validation;
- playback state transitions and resource observation;
- Provider calls, cache use, URL expiry, and lifecycle fanout;
- chat, file, room, media, and livestream business rules;
- download/upload stream handling.
Protobuf request types are the shared parameter contract. HTTP handlers may fill fields from paths or query strings before calling impls. gRPC handlers pass protobuf values directly into the same impl path. Add transport-specific code only for parsing, encoding, metadata extraction, and stream adaptation.
Provider Playback
Section titled “Provider Playback”MediaProvider::generate_playback is the Provider playback decision boundary. Each Provider chooses every mode it returns, including upstream/direct modes, proxy_* siblings, default_mode, headers, subtitles, danmaku, thumbnails, manifests, expiry, cache metadata, and live resource lifecycle data.
Provider signing happens while generating playback. Shared helpers may create mechanical proxy_* sibling URLs, but Provider code decides when to call them, which headers remain visible, and which mode is default.
VersionedPlayback is the cache payload and proxy lookup index. The version
maps generated /stream, /m3u8, /mpd, subtitle, danmaku, thumbnail, FLV,
and HLS segment URLs back to the same provider-owned playback result. Response
finalization only passes the current request’s signing key, room/user binding,
version, and expiry into the Provider rewrite callback; mode names, default
mode, header exposure, manifest metadata, and live lifecycle data remain
provider decisions.
App clients can usually consume provider-native URLs, so Providers should return usable upstream/direct modes and proxy sibling modes together when both are valid. The Provider chooses the safest default for that source: for example, header-bound HLS or DASH may default to proxy_*, while simple direct files can default to the upstream URL.
Proxy generation and proxy resolution are one contract. A Provider that emits /stream, /m3u8, /mpd, subtitle, danmaku, thumbnail, FLV, HLS playlist, or HLS segment URLs must implement and test the matching resolver path in the same Provider. Cached playback entries and refreshed playback entries must produce the same usable URL shapes.
Provider playback content contract:
| Provider | Playback content contract |
|---|---|
| Direct URL | Return the upstream mode; return a proxy_* mode when SyncTV can proxy it; make proxy the default for sources that need headers; rewrite HLS proxy manifest segment URLs; verify real Range requests. |
| Alist | Return upstream/direct or transcode modes; return proxy_* siblings; choose the default by Provider/client capability; keep thumbnail, subtitle, HLS segment, and stream proxy actions resolvable. |
| Emby/Jellyfin | Return upstream/transcode modes and proxy siblings; upstream token headers may be exposed by product policy; proxy siblings must cover direct stream, HLS/transcode, and subtitles. |
| Bilibili | Return usable direct/proxy modes for anonymously playable content; default DASH/MPD to the proxy_* manifest; proxy manifests must keep segments on SyncTV; subtitles, danmaku, thumbnails, cache metadata, and CDN headers belong to the Bilibili Provider. |
| RTMP | Playback URLs point at SyncTV-managed HLS/FLV provider-proxy actions; publish key, stream info, HLS playlist/segment, FLV, and idle cleanup are one lifecycle. |
| live proxy | External RTMP/HTTP-FLV is pulled and registered as a local publisher on the first viewer request; HLS/FLV URLs point at SyncTV; idle cleanup stops the external pull and unregisters the publisher. |
For every new mode, every generated URL must have a resolver. Cache hits, cache expiry, URL expiry, headers, manifests, segments, subtitles, danmaku, and thumbnails stay inside the same Provider.
Manual Provider verification covers the complete PlaybackResult:
- Direct URL, Alist, Emby, Jellyfin, Bilibili anonymous playback, RTMP, and live proxy;
- every playback mode and every URL returned to clients;
- direct URLs, proxy URLs, HLS/DASH manifests, indexed segments, subtitles, danmaku, thumbnails, FLV, Range requests, cache hit/miss, expiry, and cleanup;
- CLI setup with the built
synctvbinary pluscurlrequests for the returned URLs; - dynamic playlist path listing, media resolution, playback generation, target switching, cover/thumbnail routes, and auto-advance target changes;
- RTMP and live proxy full lifecycle: create through CLI, publish or pull a real upstream, fetch stream info, fetch HLS playlist/segments, fetch FLV, disconnect, then verify idle cleanup.
Add synctv CLI coverage for workflows that require manual verification, then exercise them through CLI plus curl.
Playback Workers
Section titled “Playback Workers”Playback background workers use this process’s active rooms as their scheduling input. Active means at least one Realtime connection is registered in the local ConnectionRuntime.
This contract applies to duration probing, auto-advance, and playback resource lifecycle work. Workers run on every node. A room can be active on several nodes, and duplicate attempts converge through persistence:
- duration probe claims use database locks and
SKIP LOCKED; - auto-advance uses playback state transactions and optimistic versions;
- dynamic playlist work joins
room_playback_progress.target_hashso work stays bound to the current selected target.
Use ConnectionRuntime::active_room_ids() for these workers. Use presence and hot-room statistics for lists, admin views, analytics, and metrics. Use leader election for global singleton jobs such as partition maintenance and cleanup.
Duration-probe queries must keep both state.room_id = ANY($active_room_ids) and progress.target_hash = metadata.target_hash. The room filter preserves the per-process active-room scheduling boundary. The target hash binds dynamic playlist probing to the current playback target.
Auto-advance must move playback through a playback-state transaction and version write. When several nodes scan the same active room, only one state advance should commit.
When a finite sequential playlist reaches the end, auto-advance must persist a stable end state through the same state-write path. The persisted end state lets later scan intervals skip the already-finished source.
Read Replica Routing
Section titled “Read Replica Routing”Read-replica use is opt-in per repository method. Route only allowlisted, eventually consistent list/discovery reads through DatabasePools::read().
Keep these operations on the primary pool:
- writes, transactions, migrations, and status checks;
- authentication and authorization checks;
- cache prewarm and connection-building inputs that can expose stale credentials;
- post-write notification fanout;
- snapshot-plus-cursor reads where the cursor comes from the primary;
- playback worker claims and state transitions;
- file upload/download finalization and ownership checks.
When adding a read-replica query, document why replica lag is acceptable and keep response cursors, cache writes, and side effects consistent with the same source.
File Storage
Section titled “File Storage”File storage has one object registry and separate byte/session tables:
file_objectsis the object registry.validated_atmarks a fully usable object;NULLmarks a pending session placeholder.file_blob_partsstores database-backend object bytes. Pending uploads write parts under theupload_session_key, then finalization promotes those rows to the finalobject_keywith anUPDATE.file_upload_sessionsstores upload state for database and S3 single-object or multipart sessions.file_upload_session_partsstores uploaded part progress and provider ETags/checksums.
Database multipart uploads validate per-part SHA-256 while writing each part. Finalization recomputes the manifest digest from the stored part checksums, then uses UPDATE to promote file_blob_parts.object_key from the upload_session_key to the final object_key. Pending bytes and final bytes share the same part table and use the object key as state, which keeps resume/idempotency checks cheap.
Single-object upload sessions still use the server-decided manifest plan, but write the complete object in one PUT and complete the session without part progress rows. S3 multipart sessions return direct client-to-S3 presigned part URLs. Server-proxy upload/download paths remain available for environments that need them. Database backend URLs point at SyncTV because the database bytes live behind the application.
Downloads are streaming. HTTP responses are normal binary responses backed by a streamed body; gRPC responses are protobuf chunk streams. Core services return FileObjectDownload so both transports share the same storage implementation and avoid full-object buffering on the main path.
The upload part size is server-planned. Changing it affects new sessions; open sessions keep their stored part_size_bytes and manifest. Code that validates parts must use the session’s stored plan so existing sessions keep resuming against their original manifest after the default changes.
SQLx Query Cache
Section titled “SQLx Query Cache”Repository code uses SQLx checked macros. Keep checked macros for query shape validation and update .sqlx with SQL changes:
cargo sqlx prepare --workspace -- --all-targetsSQLX_OFFLINE=true cargo check --workspace --all-targetsTreat .sqlx metadata as part of the change whenever SQL or query-returned columns change.
Repository queries that need compile-time SQL validation should stay on checked macros. Updating .sqlx is the correct workflow; switching to unchecked queries to avoid cache updates removes offline CI coverage for parameters, returned columns, and nullability.
Playback worker SQL changes also require active-room E2E evidence: no metadata probing without a local WebSocket, metadata probing with a local WebSocket, target-hash binding for dynamic playlists, and one committed auto-advance when multiple workers can see the same room.