Mosaic models two orthogonal kinds of organization:

  • Categories — exactly one per torrent (or none). Carries an optional default_save_path. Mental model: a folder.
  • Tags — many per torrent. Pure labels. Mental model: a hashtag.

Both are first-class in the UI and the API.

Categories

A category is {id, name, default_save_path, color}. The save path is what makes categories more than a label: when you add a torrent and a category is selected, the desktop Add modal consults the category’s default_save_path and pre-fills the save-path field — the UI is the layer that does this lookup. The AddMagnet / AddTorrentFile / AddTorrentBytes API calls do not take a category_id, so save-path inheritance from category at add-time is a UI affordance, not a server-side automatic.

Examples:

Category default_save_path
Movies /media/movies
TV Shows /media/tv
ISOs /srv/iso
Games /srv/games

A torrent can be uncategorized (category_id = null), but a torrent cannot have two categories. The UI renders the chip with the category’s color for at-a-glance scanning.

Operations

Action Endpoint
List GET /api/categories
Create POST /api/categories
Update PUT /api/categories
Delete DELETE /api/categories/{id}
Set on torrent POST /api/torrents/category (infohash, category_id or null)

Deleting a category does not remove the torrents in it; their category_id becomes null.

Tags

A tag is {id, name, color}. Tags can be assigned to as many torrents as you like, and a torrent can carry as many tags as you like. The relationship lives in a join table (torrent_tags).

Tags are good for cross-cutting concerns categories don’t capture:

  • to-watch, seed-forever, archived
  • linux-iso, documentary, 4k
  • temp, friend-shared

Operations

Action Endpoint
List GET /api/tags
Create POST /api/tags
Delete DELETE /api/tags/{id}
Assign POST /api/tags/assign (infohash, tag_id)
Unassign POST /api/tags/unassign (infohash, tag_id)

Each TorrentDTO carries a tags: TagDTO[] field with the full per-torrent tag set, so you don’t need a second round-trip to render tag chips.

When to use which

  • If the thing implies a save path → category.
  • If the thing is a label that doesn’t dictate location → tag.
  • If you find yourself wanting two categories for the same torrent → that’s a tag.

A torrent can absolutely have a category and tags simultaneously, and that’s the common case.

Persistence

Categories live in the categories SQLite table; tags in tags; assignment in torrent_tags. Foreign keys cascade on delete:

  • Deleting a tag drops its assignments.
  • Deleting a category sets the affected torrents’ category_id to null.

Both are stable across restarts and survive RestoreOnStartup.