Skip to content

Development Guide

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:

Terminal window
rustc --version
cargo --version
docker --version
docker compose version
node --version
npm --version
  • 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

Use the Makefile entrypoints. make help lists all development targets.

Terminal window
make dev-up

make dev-up starts PostgreSQL and Redis, then waits until they are ready. For the full media and auth dependency stack:

Terminal window
make dev-stack

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

ServiceURLDefault credentials
SyncTVhttp://127.0.0.1:8080root / LocalDevRootPass2026!
PostgreSQL127.0.0.1:5432synctv / synctv
Redis127.0.0.1:6379no password
OpenListhttp://127.0.0.1:5244admin / synctv-openlist
Embyhttp://127.0.0.1:8096MyEmbyUser / synctv-emby
Jellyfinhttp://127.0.0.1:8097root / synctv-jellyfin
RustFShttp://127.0.0.1:9000rustfsadmin / rustfsadmin
RustFS Consolehttp://127.0.0.1:9001rustfsadmin / rustfsadmin
Casdoorhttp://127.0.0.1:8000admin / 123

Stop containers and keep volumes:

Terminal window
make dev-down

Remove containers and volumes:

Terminal window
make dev-clean

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

The Makefile exports the local runtime environment:

Terminal window
make dev-serve

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

Terminal window
make dev-start
make dev-stop

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

  1. Start dependencies:

    Terminal window
    make dev-up
  2. 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!"
  3. Validate configuration:

    Terminal window
    cargo run -p synctv --bin synctv -- config validate
  4. Start the service:

    Terminal window
    cargo run -p synctv --bin synctv -- serve

With a config file:

Terminal window
cargo run -p synctv --bin synctv -- serve --config synctv.yaml

Dry-run startup:

Terminal window
cargo run -p synctv --bin synctv -- serve --config synctv.yaml --dry-run

After changing configuration structures:

Terminal window
cargo run -p synctv --bin synctv -- config --config synctv.yaml validate

Inspect effective config with secrets redacted:

Terminal window
cargo run -p synctv --bin synctv -- config --config synctv.yaml show
cargo run -p synctv --bin synctv -- config --config synctv.yaml show --output json

See How Configuration Works for precedence and environment variable naming.

Service startup runs embedded SQLx migrations automatically. To preflight database state without starting the service, run:

Terminal window
cargo run -p synctv --bin synctv -- db migrate

Check database and migration state:

Terminal window
cargo run -p synctv --bin synctv -- db status

If database state looks wrong, check:

  • SYNCTV_DATABASE_URL or database.url.
  • PostgreSQL container health.
  • Migration files.
  • Whether the current branch changed schema or SQL queries.

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.

CI-equivalent non-ignored tests:

Terminal window
cargo nextest run --workspace --locked --run-ignored default --nff

Documentation tests:

Terminal window
cargo test --workspace --doc --locked

Ignored tests that require Docker/PostgreSQL/Redis:

Terminal window
cargo nextest run --workspace --locked --run-ignored only --nff

One crate:

Terminal window
cargo nextest run -p synctv-api --locked --run-ignored default --nff

One test:

Terminal window
cargo nextest run -p synctv-api test_name --locked --run-ignored default --nff

Format:

Terminal window
cargo fmt --all --check

Clippy:

Terminal window
cargo clippy --workspace --all-targets --locked -- -D warnings

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

Terminal window
cargo check -p synctv --no-default-features --locked
cargo check -p synctv --no-default-features --features "tls-aws-lc,tls-webpki-roots" --locked
cargo check -p synctv --no-default-features --features "tls-aws-lc,tls-native-roots" --locked
cargo check -p synctv --no-default-features --features "tls-ring,tls-webpki-roots" --locked
cargo check -p synctv --no-default-features --features "tls-ring,tls-native-roots" --locked
cargo check -p synctv --no-default-features --features "k8s,mimalloc,tls-aws-lc,tls-webpki-roots,tls-native-roots" --locked

Run 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 synctv binary so startup, config, migrations, and routes are covered;
  • use the synctv CLI to create users, rooms, Providers, media, dynamic playlists, RTMP publish keys, and playback state;
  • keep a room active with a real WebSocket connection;
  • use curl to 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 synctv CLI 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 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:

Terminal window
make dev-clean
make 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-targets

Built-in OpenAPI docs require the openapi feature.

Check:

Terminal window
cargo check --workspace --all-targets --features openapi

Run with Swagger UI:

Terminal window
cargo run -p synctv --features openapi --bin synctv -- serve

See OpenAPI Access for URLs.

The docs site is an Astro Starlight project under docs/.

Install dependencies:

Terminal window
cd docs
npm install

Local preview:

Terminal window
cd docs
npm run dev

Build:

Terminal window
cd docs
npm run build

Validate:

Terminal window
cd docs
npm run validate

If deploying under a subpath, set SYNCTV_DOCS_BASE. Set SYNCTV_DOCS_SITE for canonical URLs and sitemap output.

Terminal window
cd docs
SYNCTV_DOCS_SITE=https://example.com SYNCTV_DOCS_BASE=/synctv npm run build

SYNCTV_DOCS_BASE is only the path prefix, such as /synctv or /docs. Build output is docs/dist and should not be committed.

CLI definitions live under synctv/src/cli/. After changing commands or arguments, check:

Terminal window
cargo run -p synctv --bin synctv -- --help
cargo run -p synctv --bin synctv -- user --help
cargo run -p synctv --bin synctv -- provider --help

Update CLI Reference for new commands.

Helm:

Terminal window
helm lint ./helm/synctv
helm template synctv ./helm/synctv
helm template synctv ./helm/synctv --set ingress.grpc.enabled=true

Compose:

Terminal window
docker compose -f docker-compose.dev.yml config
docker compose config

Rendering only proves syntax and templates are valid. Still run SyncTV config validation or an actual startup check.