Tunecamp leverages two primary technologies to enable a decentralized music ecosystem: ActivityPub for social federation and federated HTTP/NodeInfo discovery for finding other instances. It also provides full Subsonic API compatibility for mobile and desktop clients.
Tunecamp discovers other instances by gossip over HTTP — there is no central relay and no shared registry. An instance crawls outward from a set of seed instances (its ActivityPub-followed TuneCamp sites plus TUNECAMP_FEDERATION_SEEDS), validates that each peer is a live TuneCamp via its NodeInfo, and stores the reachable set in local SQLite (federated_instances). Implemented in src/server/modules/network/federated-discovery.service.ts.
History: earlier versions used the Zen decentralized graph for instance signaling, and before that for user identity (SEA keypairs), Zen-first auth, wallet derivation, and cross-instance roaming. Zen has been removed entirely (PRs #369/#370/#372): authentication is username/password (JWT), discovery is the HTTP gossip described here, and catalogs are exchanged directly over HTTP. The
zendependency and allTUNECAMP_ZEN_*env vars are gone.
- Seed-based crawl: discovery starts from AP-followed TuneCamp actors and
TUNECAMP_FEDERATION_SEEDS, then gossips outward (capped depth/breadth, dead instances pruned). - Liveness check: a peer is only added if it answers as a live TuneCamp (NodeInfo type-check), not merely HTTP 200.
- Music Discovery: the "Network" page reads the federated set, then fetches catalogs directly via HTTP (
/api/catalog), cached stale-while-revalidate. - Downloadability is advertised, not assumed: every release and track in
/api/catalog/fullcarries adownloadableflag, evaluated as an anonymous stranger — the caller a federated fetch actually is. A release on sale, one published as streaming-only, or one pointing at an external store advertisesfalse, and a client (a remote Network page, Sidecamp) offers no download for it. Streaming is unaffected either way; only the file transfer is withheld, and the seller's own download routes enforce it per request regardless of what a client does. An instance older than the flag still publishes its release's mode and price, which is enough for the consumer to apply the same policy locally. - Peer-track federation (opt-in): when an instance enables Federate Peer Tracks, its currently-shared peer tracks ride along in
/api/catalog/fulland appear on remote Network pages (taggedPEER), streamable — and, if downloads are allowed, importable — cross-instance. These entries are ephemeral and use a short cache window so they vanish quickly when the peer disconnects. See Sidecamp. - Cross-instance peer search (opt-in): under the same Federate Peer Tracks flag, a logged-in user's global search fans out server-side to known federated instances (bounded 10, parallel, 3s timeout, SSRF-guarded) and merges their connected peers' matching tracks, tagged with
origin. Single hop only; remote hits stream from the origin's publicfederated-streamendpoint (search + stream, no download). Exposed asGET /api/peers/federated-search. - One-click follow: an admin can follow an instance directly by URL.
GET /api/community/instance— this instance's own descriptor.GET /api/community/peers— known peers (the gossip surface).GET /api/community/sites— aggregated discoverable sites (local + federated + ActivityPub). CORS-enabled for external directories.POST /api/community/register— self-registration: submit your instance URL to a directory instance and appear immediately, without waiting for the next 6-hour crawl. See below.
Instead of waiting up to 6 hours for gossip to reach a directory instance, admins can self-register:
POST /api/community/register
Content-Type: application/json
{ "url": "https://your-instance.example.com" }The directory instance will:
- Probe your URL via NodeInfo (
/.well-known/nodeinfo) to verify it is a reachable TuneCamp instance. - Store the metadata immediately — your instance appears in
GET /api/community/sitesright away.
Responses:
| Status | Meaning |
|---|---|
200 OK |
Registered. Appears in /api/community/sites immediately. |
400 |
Missing or invalid url field. |
422 |
URL is not a reachable TuneCamp instance (NodeInfo check failed). |
429 |
Rate limited — 1 registration per IP per hour. Retry-After header included. |
The endpoint is public (no auth required) and CORS-enabled — wildcard, credential-less, independent of TUNECAMP_CORS_ORIGINS — so the community website can call it directly from the browser. The rate limit is enforced per IP to prevent abuse.
Note that the CORS policy of the directory instance is what matters here: the URL being registered is only a payload, and the instance it points at is contacted server-to-server (NodeInfo), where CORS plays no part at all. An instance that fails the 422 check is unreachable or not answering NodeInfo — not missing a CORS header.
TUNECAMP_FEDERATION_SEEDS(backend): comma/space-separated origin URLs of seed instances to bootstrap discovery. Optional — AP-followed TuneCamp sites also seed the crawl.
ActivityPub allows Tunecamp to communicate with other platforms like Mastodon, Pleroma, Funkwhale, and Lemmy.
- Artist Profiles: Every artist on Tunecamp is an ActivityPub "Person" actor.
- Listener Accounts: Every account is also a "Person" actor at
/users/{username}, artist link or not — being followable from the Fediverse is not gated on publishing rights. A listener actor is a bare profile: listeners cannot publish (seeROLES.md), so nothing is ever broadcast from it. - Outbound Broadcasting: Tunecamp focuses on a "Broadcaster" model. When an artist publishes a new release or a post, Tunecamp broadcasts a "Create Note" activity to all followers across the Fediverse.
- Inbound Engagement: Users on other Fediverse instances can follow Tunecamp artists and like/favorite/comment on their releases and posts. Replies and comments are federated back to Tunecamp.
- Interoperability: Tunecamp supports WebFinger and standard ActivityPub inboxes/outboxes. Note: Tunecamp no longer maintains an internal, Mastodon-style consumption timeline or client-side following mechanics to prioritize lightweight performance.
- Keys: RSA 4096-bit keypairs are automatically generated for every artist. Listener accounts get their own keypair too, written at registration, at password/FID login (
ensureUserKeys), or — for FID SSO — derived deterministically from theapSeed(seeFID-IDENTITY.md). - An actor only exists once its keys do: the actor dispatcher returns
nullfor an account with noap_public_key, so/users/{handle}and its WebFinger record both 404. The webapp therefore asks the server rather than guessing the handle —GET /api/users/me/fediversereturns{ hasActor, handle, actorUri }, and the Profile page hides the Fediverse Identity panel whenhasActoris false. - Attachments: Broadcasts include "Audio" attachments (direct stream links) and "Image" attachments (cover art).
- Public URL: Federation requires
TUNECAMP_PUBLIC_URLto be correctly configured withhttps.
TUNECAMP_PUBLIC_URL: Required for Federation.- ActivityPub relay (optional): To broadcast beyond direct followers, set the relay URL at runtime in the admin panel (stored as the
relayUrlsetting). This is not an environment variable.
Remote servers (e.g. Mastodon) cache an actor's public key the first time they
see it and keep verifying signatures against that cached copy. If an actor's key
later changes — or if the same /users/{handle} URL previously served a
different identity (for example a listener user actor before the account
became an artist) — the remote keeps the stale key and rejects every activity
with 401 {"error":"Public key not found for key .../users/{handle}#main-key"}.
Fix it from Admin → Identity → the artist card → "Refresh federation"
(POST /api/admin/artists/:id/refresh-identity). This ensures the artist has a
valid RSA key pair and broadcasts a signed Update(Person) to followers and the
relay, forcing remotes to re-fetch the actor document and replace the cached key.
The action is idempotent and safe to re-run.
Beyond ActivityPub, Tunecamp can follow plain RSS/Atom feeds (podcasts, Owncast streams, blogs) so their items show up alongside federated content.
- Follow a feed:
POST /api/admin/network/rss/followwith the feed URL. - Storage: a followed feed is stored as a remote actor with
type = 'rss'; each item becomes aremote_contentrow. - Refresh: feeds are refreshed by the RSS service (
src/server/modules/network/rss.service.ts), independently of the ActivityPub outbox fetcher.
Tunecamp is compatible with Funkwhale instances for music-specific federation.
- NodeInfo: Tunecamp exposes metadata at
/.well-known/nodeinfoincluding Funkwhale-compatible fields (library.federationEnabled,supportedUploadExtensions,funkwhaleVersion). - Federation Libraries:
GET /api/v1/federation/librariesreturns Tunecamp's music catalog in Funkwhale's expected format. - NodeInfo 2.0 API:
GET /api/v1/instance/nodeinfo/2.0provides instance metadata for Funkwhale-style discovery. - Actor Types: Artists are exposed as
["Person", "Artist", "MusicArtist"]with Funkwhale namespace extensions. - Audio Attachments: Release broadcasts include
Audioobjects withfunkwhale:bitrateandfunkwhale:durationproperties.
Tunecamp exposes a full Subsonic REST API at /rest (API version 1.16.1), enabling connection from any Subsonic-compatible client.
| Method | Format | Description |
|---|---|---|
| Clear-text | p=password |
Plain password in query |
| Hex-encoded | p=enc:hex |
Password hex-encoded |
| Token+Salt | t=md5(password+salt)&s=salt |
Secure token-based auth |
When a Subsonic client scrobbles a track (scrobble.view), Tunecamp records the play in the local SQLite database (play_history table). All scrobbling and playback statistics are stored locally on the server.
| Feature | Technology | Scope |
|---|---|---|
| Artist Following | ActivityPub | External (Mastodon, etc) |
| Likes / Favorites | ActivityPub | External (Mastodon, etc) |
| Release Notification | ActivityPub | External (Mastodon, etc) |
| Funkwhale Federation | ActivityPub | External (Funkwhale) |
| Instance Discovery | Federated HTTP / NodeInfo gossip | Internal (Tunecamp Nodes) |
| Mobile Streaming | Subsonic API | External (Any client) |
| Starred / Favorites | Subsonic API | Local (per user) |