AlbumApplicationResponse used to carry YouTubeAlbumId/ThumbnailUrl as two always-present-but-usually-null fields for the sake of a single flat shape. Since one of these fields IS genuinely exclusive to external albums, an inheritance split reads better than the flattened shape: AlbumApplicationResponse now holds only the fields shared by every album, and ExternalAlbumApplicationResponse adds ExternalId (renamed from YouTubeAlbumId, matching the source-generalized naming used everywhere else) and ThumbnailUrl. [JsonPolymorphic]/[JsonDerivedType] are required here, not optional: without them System.Text.Json serializes strictly by the declared CLR type at each call site (both the bare return type and nested inside AlbumSearchItemResponse.Album), so an ExternalAlbumApplicationResponse instance would silently lose its extra fields on the wire. This also gives every response a "source" discriminator field for free, consistent with AlbumSearchItemResponse.Source, and the generated OpenAPI schema now models it as a proper oneOf/discriminator instead of nullable fields.
Musify
Music streaming platform: upload your own tracks (or import them from YouTube),
process them asynchronously (audio transcoded to .m4a, thumbnails generated)
and stream them without the bytes ever going through the API.
backend/ .NET 10 — Domain / Application / Infrastructure / Api / Worker / StreamingGateway / Tests
web-player/ SvelteKit 2 + Svelte 5 + Tailwind 4 web client
deploy/ Docker Compose stacks — self-contained, no wrapper scripts
docs/ What each piece does and why (in Spanish)
dotnet test backend/Musify.slnx # backend/Tests/ — Docker running (Testcontainers: Postgres, SeaweedFS)
Run it
Development (Docker Desktop):
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d
Infrastructure plus stream-ticket keys, database migrations and Zitadel OIDC
provisioning — all as one-shot jobs baked into the compose file. Apps run from
the IDE by default; add --profile apps up -d --build as a second command to
run them in Docker instead.
Production, on a server with a domain:
cp deploy/.env.prod.example deploy/.env.prod # fill in the domain and secrets
docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml up -d
docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml --profile apps up -d --build
Idempotent — run the same commands again to deploy an update. See deploy/README.md for the details, the port map and the production prerequisites.
Components
| Service | Port (dev) | Role |
|---|---|---|
Musify.Api |
5111 | REST API, issues presigned upload URLs and stream tickets |
Musify.StreamingGateway |
8081 | YARP proxy that validates the stream ticket and serves the audio |
Musify.Worker |
— | MassTransit consumers: transcoding, thumbnails, YouTube downloads |
web-player |
5173 / 3000 | Web client (SvelteKit, server-side OIDC session) |
| Zitadel | 8080 | Identity (OIDC/OAuth2) |
| PostgreSQL / RabbitMQ / SeaweedFS / Jaeger | 59000 / 5672 / 8333 / 16686 | Metadata, messaging, object storage, traces |
More in docs/: architecture, authentication, storage, streaming, media processing, development.