Development Guide
Prerequisites
Section titled “Prerequisites”SyncTV is a Rust workspace. Local development usually needs:
- Rust stable toolchain.
- PostgreSQL.
- Redis.
- Node.js and npm for the
docs/site. - Docker and Docker Compose for quickly starting dependencies.
- Protobuf/gRPC build dependencies depending on platform and crate build requirements.
Check versions:
rustc --versioncargo --versiondocker --versiondocker compose versionnode --versionnpm --versionRepository Layout
Section titled “Repository Layout”- synctv application binary and CLI
- synctv-core business logic, configuration, services, repositories
- synctv-api HTTP/gRPC API
- synctv-livestream RTMP/HLS/HTTP-FLV
- synctv-cluster cluster coordination
- synctv-proxy media proxy and slice cache
- synctv-proto protobuf definitions
Directoryhelm
- synctv Helm chart
- docs Astro Starlight documentation site
Start Development Dependencies
Section titled “Start Development Dependencies”Use the Makefile entrypoints. make help lists all development targets.
make dev-upmake dev-up starts PostgreSQL and Redis, then waits until they are ready. For the full media and auth dependency stack:
make dev-stackmake dev-stack starts PostgreSQL, Redis, OpenList, Emby, Jellyfin, RustFS, and Casdoor, then initializes the Casdoor database, RustFS bucket, OpenList local media directory, Emby/Jellyfin admin accounts, and local media libraries. Common endpoints:
| Service | URL | Default credentials |
|---|---|---|
| SyncTV | http://127.0.0.1:8080 | root / LocalDevRootPass2026! |
| PostgreSQL | 127.0.0.1:5432 | synctv / synctv |
| Redis | 127.0.0.1:6379 | no password |
| OpenList | http://127.0.0.1:5244 | admin / synctv-openlist |
| Emby | http://127.0.0.1:8096 | MyEmbyUser / synctv-emby |
| Jellyfin | http://127.0.0.1:8097 | root / synctv-jellyfin |
| RustFS | http://127.0.0.1:9000 | rustfsadmin / rustfsadmin |
| RustFS Console | http://127.0.0.1:9001 | rustfsadmin / rustfsadmin |
| Casdoor | http://127.0.0.1:8000 | admin / 123 |
Stop containers and keep volumes:
make dev-downRemove containers and volumes:
make dev-cleanDevelopment Compose only starts local dependency services. The SyncTV backend runs on the host with cargo run.
Casdoor/OAuth2 providers are configured through runtime settings. After Casdoor starts, create an application in the Casdoor console, then write oauth2.providers with synctv settings update oauth2.
Run Locally
Section titled “Run Locally”The Makefile exports the local runtime environment:
make dev-serveThis starts PostgreSQL and Redis first, then runs SyncTV with local cargo run. Start make dev-stack first when the full dependency set is needed.
For a background process with the same development settings:
make dev-startmake dev-stopmake dev-start builds target/debug/synctv, writes .dev-data/run/synctv.pid, then waits for the management socket and HTTP readiness. make dev-smoke uses this same startup path.
Manual flow:
-
Start dependencies:
Terminal window make dev-up -
Export local settings for this shell:
Terminal window export SYNCTV_DATABASE_URL="postgresql://synctv:synctv@localhost:5432/synctv"export SYNCTV_REDIS_URL="redis://localhost:6379"export SYNCTV_JWT_SECRET="local-compose-jwt-secret-not-for-production-2026"export SYNCTV_CLUSTER_SECRET="local-compose-cluster-secret-2026"export SYNCTV_SECURITY_CREDENTIAL_ENCRYPTION_KEY="222102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"export SYNCTV_SECURITY_OPAQUE_SERVER_SETUP_SECRET="local-compose-opaque-server-setup-secret-not-for-production-2026"export SYNCTV_BOOTSTRAP_CREATE_ROOT_USER="true"export SYNCTV_BOOTSTRAP_ROOT_PASSWORD="LocalDevRootPass2026!" -
Validate configuration:
Terminal window cargo run -p synctv --bin synctv -- config validate -
Start the service:
Terminal window cargo run -p synctv --bin synctv -- serve
With a config file:
cargo run -p synctv --bin synctv -- serve --config synctv.yamlDry-run startup:
cargo run -p synctv --bin synctv -- serve --config synctv.yaml --dry-runConfiguration Validation
Section titled “Configuration Validation”After changing configuration structures:
cargo run -p synctv --bin synctv -- config --config synctv.yaml validateInspect effective config with secrets redacted:
cargo run -p synctv --bin synctv -- config --config synctv.yaml showcargo run -p synctv --bin synctv -- config --config synctv.yaml show --output jsonSee How Configuration Works for precedence and environment variable naming.
Database Migrations
Section titled “Database Migrations”Service startup runs embedded SQLx migrations automatically. To preflight database state without starting the service, run:
cargo run -p synctv --bin synctv -- db migrateCheck database and migration state:
cargo run -p synctv --bin synctv -- db statusIf database state looks wrong, check:
SYNCTV_DATABASE_URLordatabase.url.- PostgreSQL container health.
- Migration files.
- Whether the current branch changed schema or SQL queries.
Schema Boundaries
Section titled “Schema Boundaries”Keep migrations focused on durable storage shape and stable data integrity. Use the database for primary keys, foreign keys, required storage columns, lookup indexes, unique indexes, and low-volatility physical constraints.
Do not encode volatile business policy in SQL constraints. Business enum value lists, review states, room/member roles, provider kinds, permission rules, runtime-setting allowlists, playback limits, room policy, moderation workflow, and other product-level validation belong in the domain/service layer with typed inputs and tests. Store enum-like fields such as status, role, signup method, message type, and review state as numeric codes (SMALLINT is usually enough), not strings or database enum types.
If a direct SQL write can create invalid business state, tighten the service write path, add a consistency check, or build repair tooling.
UNIQUE is appropriate for stable identity or near-permanent uniqueness invariants when database atomicity is useful: usernames, emails, provider instance names, OAuth account identities, idempotency keys, and one-current-row-per-stable-scope patterns are typical examples. Do not use UNIQUE for policy that may be relaxed, re-scoped, or reinterpreted during ordinary product work. Only move a rule into the database when it is a storage invariant that should remain true independently of product policy.
User-facing labels and display names, such as room names, are product policy by default unless they are explicitly designed as stable identity keys. When concurrency matters, enforce them in the service layer with transactional locks or another coordination mechanism.
Tests and Checks
Section titled “Tests and Checks”CI-equivalent non-ignored tests:
cargo nextest run --workspace --locked --run-ignored default --nffDocumentation tests:
cargo test --workspace --doc --lockedIgnored tests that require Docker/PostgreSQL/Redis:
cargo nextest run --workspace --locked --run-ignored only --nffOne crate:
cargo nextest run -p synctv-api --locked --run-ignored default --nffOne test:
cargo nextest run -p synctv-api test_name --locked --run-ignored default --nffFormat:
cargo fmt --all --checkClippy:
cargo clippy --workspace --all-targets --locked -- -D warningsDo not use --all-features for the full workspace: some feature pairs are
intentionally mutually exclusive, such as mimalloc/jemalloc and
tls-aws-lc/tls-ring. Check non-default builds with explicit feature sets.
The TLS contract is that tls-aws-lc / tls-ring select the rustls crypto
provider, while tls-webpki-roots / tls-native-roots select trust root
sources where upstream crates expose those switches. A few dependencies have
their own limits: SQLx 0.8 exposes native roots only through its ring backend,
reqwest 0.13 uses the platform verifier, and baseline email TLS keeps webpki
roots because lettre requires a verifier/root feature whenever rustls is
compiled. When changing TLS-related code, check all four provider/root
combinations:
cargo check -p synctv --no-default-features --lockedcargo check -p synctv --no-default-features --features "tls-aws-lc,tls-webpki-roots" --lockedcargo check -p synctv --no-default-features --features "tls-aws-lc,tls-native-roots" --lockedcargo check -p synctv --no-default-features --features "tls-ring,tls-webpki-roots" --lockedcargo check -p synctv --no-default-features --features "tls-ring,tls-native-roots" --lockedcargo check -p synctv --no-default-features --features "k8s,mimalloc,tls-aws-lc,tls-webpki-roots,tls-native-roots" --lockedRun focused tests first for the area you changed, then broader checks.
Provider and Playback End-to-End Verification
Section titled “Provider and Playback End-to-End Verification”After changing Providers, playback URL signing, proxying, manifests, subtitles, danmaku, thumbnails, RTMP, live proxy, playback background workers, or resource lifecycle behavior, run real service checks in addition to unit tests:
- start a built
synctvbinary so startup, config, migrations, and routes are covered; - use the
synctvCLI to create users, rooms, Providers, media, dynamic playlists, RTMP publish keys, and playback state; - keep a room active with a real WebSocket connection;
- use
curlto request every returned direct/proxy URL, manifest, indexed segment, subtitle, danmaku, thumbnail, and Range URL; - cover Direct URL, Alist, Emby, Jellyfin, Bilibili anonymous playback, RTMP, live proxy, HLS, FLV, cache MISS/HIT, URL expiry, and resource cleanup;
- cover dynamic playlist returned paths, media resolution, playback, switching, cover/thumbnail routes, and target changes after auto-advance;
- test RTMP/live proxy with a real upstream publisher, then verify stream info, HLS playlists and segments, FLV, and idle cleanup after disconnect;
- add
synctvCLI coverage before accepting a workflow missing CLI support.
Use the returned PlaybackResult as the verification checklist. Request every mode and auxiliary URL, including media resolved from dynamic playlists. Playback background workers use the current process’s ConnectionRuntime::active_room_ids(), so active-room verification should be driven by a real WebSocket.
SQLx Offline Metadata
Section titled “SQLx Offline Metadata”SQLx macros validate SQL at compile time and use .sqlx/ metadata for offline builds. Refresh metadata after changing SQL, migrations, or query return types.
Repository queries that need compile-time validation should stay on SQLx checked macros. Updating .sqlx is part of the normal development flow, and metadata changes are part of SQL changes.
Playback background queries must also keep room-scoped filtering and the join to the current room_playback_progress.target_hash. When those SQL statements change, update code, .sqlx metadata, and the active-room E2E evidence together.
Refresh flow:
make dev-cleanmake dev-up
local_database_url="postgresql://synctv:synctv@localhost:5432/synctv"
SYNCTV_DATABASE_URL="$local_database_url" \cargo run -p synctv --bin synctv -- db migrate
DATABASE_URL="$local_database_url" \cargo sqlx prepare --workspace
SQLX_OFFLINE=true cargo check --workspace --all-targetsOpenAPI Feature
Section titled “OpenAPI Feature”Built-in OpenAPI docs require the openapi feature.
Check:
cargo check --workspace --all-targets --features openapiRun with Swagger UI:
cargo run -p synctv --features openapi --bin synctv -- serveSee OpenAPI Access for URLs.
Documentation Site
Section titled “Documentation Site”The docs site is an Astro Starlight project under docs/.
Install dependencies:
cd docsnpm installLocal preview:
cd docsnpm run devBuild:
cd docsnpm run buildValidate:
cd docsnpm run validateIf deploying under a subpath, set SYNCTV_DOCS_BASE. Set SYNCTV_DOCS_SITE for canonical URLs and sitemap output.
cd docsSYNCTV_DOCS_SITE=https://example.com SYNCTV_DOCS_BASE=/synctv npm run buildSYNCTV_DOCS_BASE is only the path prefix, such as /synctv or /docs. Build output is docs/dist and should not be committed.
When Changing CLI
Section titled “When Changing CLI”CLI definitions live under synctv/src/cli/. After changing commands or arguments, check:
cargo run -p synctv --bin synctv -- --helpcargo run -p synctv --bin synctv -- user --helpcargo run -p synctv --bin synctv -- provider --helpUpdate CLI Reference for new commands.
When Changing Helm or Compose
Section titled “When Changing Helm or Compose”Helm:
helm lint ./helm/synctvhelm template synctv ./helm/synctvhelm template synctv ./helm/synctv --set ingress.grpc.enabled=trueCompose:
docker compose -f docker-compose.dev.yml configdocker compose configRendering only proves syntax and templates are valid. Still run SyncTV config validation or an actual startup check.
When Maintaining Documentation
Section titled “When Maintaining Documentation”- When adding a configuration field, update the Configuration Index, the Full Configuration Example, and the matching configuration topic.
- When adding a CLI command, update the CLI Reference.
- When changing deployment templates, update Docker Compose Deployment or Helm Deployment.
- When changing common errors, ports, Ingress behavior, authentication, or proxy behavior, update Troubleshooting.
- When changing runtime surfaces, dependencies, provider/proxy boundaries, or cluster behavior, update Architecture Overview.
- When changing login, 2FA, OAuth2, token, management, or permission boundaries, update Authentication and Security Model.
- When changing users, rooms, reviews, providers, runtime settings, or management CLI semantics, update Administer SyncTV.
- When changing room roles, permission bits, room settings, member overrides, or user preferences, update Rooms, Permissions, and Preferences.
- When changing hot-reload fields registered in
SettingsRegistry, update Runtime Settings Reference. - When changing the OpenAPI feature, Swagger UI path, or OpenAPI JSON path, update OpenAPI Access.
- When changing cluster, admin, or service protobuf files, update gRPC Debugging.
- When changing HTTP, gRPC, Realtime, or protobuf semantics, update API and Protobuf Evolution, SDK and API Examples, and Errors.
- When changing metric names, labels, or meanings, update Metrics Catalog and Observability Runbook.