4.2 KiB
4.2 KiB
Architecture
Principles
- Clean architecture / layers in
backend/: the domain and application layers don't depend on infrastructure; infrastructure implements their interfaces (Application/Contracts). - CQRS: every operation is a
CommandorQuerywith its own handler (Mediator). Handlers returnArdalis.Result(Ok/NotFound/Unauthorized/Conflict/Invalid…), which the API translates into HTTP status codes. - Heavy bytes never touch the API: uploads use presigned URLs (client → S3) and playback goes through a dedicated reverse proxy (gateway → S3). The API only moves metadata and issues permissions.
- Async processing: creating a track doesn't transcode it on the spot. It publishes an event, and a worker does the heavy lifting, with compensation (MassTransit sagas/routing slips).
Components
| Component | Responsibility | Why it's separate |
|---|---|---|
| Musify.Api | REST API (users, tracks, playlists), authorizes requests, issues presigned URLs and stream tickets | A thin entry point, it doesn't process media or serve bytes |
| Domain/Application/Infrastructure | Domain + use cases + shared infrastructure (EF, S3, MassTransit, ffmpeg) | Logic shared between the API and the Worker |
| StreamingGateway | Reverse proxy (YARP) that validates the ticket and serves the audio (.m4a) from SeaweedFS |
Scales with byte traffic independently of the API, and keeps SeaweedFS private behind it |
| Worker | Consumes MassTransit events: transcodes audio to .m4a and generates thumbnails |
Keeps CPU/IO-heavy work off the HTTP request path |
PostgreSQL (shared, runs in the separate Infrastructure repo) |
Metadata (users, tracks, playlists, upload intents) | n/a |
| RabbitMQ (shared, runs in Infrastructure) | Event queue + transactional outbox | Decouples creation from processing |
| SeaweedFS (shared, runs in Infrastructure) | Object storage (S3 + filer) | Stores originals and derivatives (.m4a audio, thumbnails) |
| Zitadel (shared, runs in Infrastructure) | Identity (OIDC/OAuth2) | Login and JWT issuance |
Main flows
1. Uploading a track
- The client requests
POST /tracks/upload-urls. The API creates UploadIntents (reservations) and returns presigned PUT URLs (image + audio). - The client uploads the files directly to SeaweedFS using those URLs (never through the API).
- The client calls
POST /trackswith theintentIds. The API validates the intents, creates theTrack(+UserHasTrack), and publishesCreateTrackResourcesEvent. - The Worker consumes the event: moves the originals into place, generates thumbnails, transcodes the audio to
.m4a, and updates the track's status.
See media-processing.md and storage.md for details.
2. Playback (streaming)
- The client requests
GET /tracks/{id}/stream(authenticated). The API authorizes it and returns{ manifestUrl, ticket }(a URL to the.m4aplus an RS256 ticket scoped to that track's folder). - The player opens the audio URL with
?t=<ticket>appended (or anX-Stream-Ticketheader) and downloads it via HTTP range requests. - The StreamingGateway validates the ticket and forwards the request to the SeaweedFS filer. The bytes flow storage → client.
See streaming.md.
3. Authentication
- The client logs in via OIDC against Zitadel and gets an access token (JWT).
- Musify.Api validates the JWT's signature, issuer and audience.
- On every validated token, the local user is synced (name and picture created or updated). This is just-in-time provisioning.
See authentication.md.
Data model (entities)
- User:
Idis Zitadel's numericsub(long), plus a name andProfilePictureUrl. - Track: original/derivative file names,
AudioFolderName(the transcoded.m4a's folder), processing state (ProcessingStatus),LifeCycleStatus. - PlayList: a user's playlist (
UserId). - PlayListHasTrack: the tracks inside a playlist (with
Position). - UserHasTrack: ownership, which user owns which track.
- UploadIntent: an upload reservation (key, bucket, expected size, status, expiration) used for validation and quota.