Sign inSign up

getcoral/tide

By getcoral

•Updated 11 days ago

Self-hosted torrent downloader with queue controls and live piece maps.

Image
1

5.5K

getcoral/tide repository overview

Tide logo — a wave crossing a glowing waterline, coral above and teal below

⁠Tide

A web torrent client for the download box next to Jellyfin.

GitHub Sponsors Discord MIT license Docker Hub

Tide is the torrent client in the Coral⁠ ecosystem. It runs as one container, shows the queue in the browser, and is built to sit beside a Jellyfin server: optional Jellyfin sign-in, and a small API other Coral modules use to see what finished.

You add magnet links. Tide enforces a cap on how many downloads and seeders run at once, lets you prioritise files by the pieces they need, and pauses the swarm when the container's memory gets tight. State lives in SQLite. There is no second database and no desktop app to keep open.

⁠Why Tide, and when to stay with qBittorrent or Transmission

qBittorrent and Transmission are general-purpose BitTorrent clients, and they are the better choice for a lot of people.

Stay with qBittorrent when you want libtorrent, a desktop app, RSS, search plugins, categories, tags, and .torrent files as well as magnets. Its Web UI attaches to that app.

Stay with Transmission when you want a small daemon (libtransmission) with the web UI that ships with it, remote clients, and the same kind of broad .torrent workflow.

Run Tide when the download box is a memory-capped container next to Jellyfin, and you want the website itself to be the client.

TideqBittorrentTransmission
How you use itThe website on port 3000 is the whole appDesktop app; Web UI is optionalDaemon with a built-in web UI
Active downloads and seedersBoth caps pause anything past the limit, in queue orderBoth capsDownload queue and seed queue
Container memoryPauses torrents and drops peers as RSS nears the cgroup limit, then resumes below a lower lineLeaves memory to the process and the cgroup killerLeaves memory to the process and the cgroup killer
Who can sign inOptional Jellyfin accounts. Anyone signed in can watch the board. Only Jellyfin administrators get /manageIts own usersHTTP auth on the daemon
After a download finishesOther Coral modules read downloads.list and downloads.events. On one mount, completion is a rename from incomplete to completeA watch folder, or an *arr stackA watch folder, or an *arr stack
What you can addMagnet linksMagnets, .torrent files, RSS, pluginsMagnets and .torrent files
EngineWebTorrent⁠ in Nodelibtorrentlibtransmission

Tide has no RSS feeds, search plugins, categories, or tags, and the UI does not upload .torrent files. The published image also leaves out WebRTC peer connections. Tracker announces and DHT still run from Node.

A fuller walkthrough of every setting is in the Tide docs⁠.

⁠Screenshots

Taken from a running Tide. The film in the queue is Big Buck Bunny, a Creative Commons download.

⁠Torrent board

The home view: live totals, a magnet box, the combined piece map, and the queue.

Tide torrent board while Big Buck Bunny is downloading, with speed, peers, and the piece map

⁠Management

Queue caps, speed limits, and the default seeding goals applied to every new torrent.

Tide management page with magnet add and engine settings for queue caps and seeding goals

Open a torrent on the board for its own piece map, tracker status, peer list, and a Watch link that streams a video file with HTTP range requests. Whether it plays depends on the browser understanding that container.

⁠Quick start

Save this as compose.yaml. Change the password before the port is reachable from a network you do not trust. To keep it on the machine you are sitting at, publish 127.0.0.1:3000:3000 instead.

services:
  tide:
    image: getcoral/tide:latest
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      TIDE_DATA_DIR: /data
      TIDE_DOWNLOADS_DIR: /downloads/complete
      TIDE_AUTH_USERNAME: admin
      TIDE_AUTH_PASSWORD: change-me
    volumes:
      - ./tide-data:/data
      - ./downloads:/downloads
    mem_limit: 4g
    healthcheck:
      test:
        [
          "CMD",
          "node",
          "-e",
          "fetch('http://127.0.0.1:3000/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
        ]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 20s
docker compose up -d

Open http://localhost:3000⁠. The same image is ghcr.io/get-coral/tide:latest.

./tide-data holds tide.sqlite (queue, controls, sessions, Jellyfin URL). ./downloads/complete is where finished files land. Tide puts partials in ./downloads/incomplete, the sibling of TIDE_DOWNLOADS_DIR, so a finished torrent is moved with a rename. Split those two directories onto different mounts and every completion becomes a full copy.

mem_limit is the cap Tide reads from cgroups. With a cap and no TIDE_MEMORY_*_MB overrides, it pauses active torrents around 90% of that limit and resumes once RSS falls to about 75%. For an 8 GB NAS limit, a reasonable explicit set is:

TIDE_MEMORY_LIMIT_MB=8192
TIDE_MEMORY_PAUSE_MB=7168
TIDE_MEMORY_RESUME_MB=6144

Jellyfin, Aurora, Tide, and Librarian in one file — including the single library mount hardlinks need — is the Coral compose guide⁠.

⁠What you get

  • Paste a magnet, or paste straight from the clipboard, and watch the queue over a live stream
  • Cap active downloads and active seeders. Anything past the cap waits, in queue order
  • Global and per-torrent download and upload limits
  • A default ratio goal and seed-time goal for every new torrent, with per-torrent overrides that stop seeding when the rule hits
  • Per-file priority that selects pieces, plus a piece map, tracker status, and peer list
  • A Watch link on the torrent for a video file already in the swarm
  • HTTP basic auth in front of the production server, the same shape of front door as Transmission
  • Optional Jellyfin sign-in. Tide stores the server URL and no API key
  • Service tokens so another Coral module can read the download list

⁠Access control

Both layers are optional, and both are off until you turn them on.

HTTP basic auth. Set TIDE_AUTH_USERNAME and TIDE_AUTH_PASSWORD and the production server challenges every request before it reaches the app. It is one shared credential. pnpm dev does not apply it.

Jellyfin sign-in. Tide can require a Jellyfin account on its own, or on top of basic auth.

  1. On /manage, set the Jellyfin server URL under Access (or set TIDE_JELLYFIN_URL). Tide checks the URL against /System/Info/Public before storing it.
  2. Turn on Require a Jellyfin sign-in. Tide refuses to enable this until a server answers, so you cannot lock yourself out of a Tide that has nowhere to authenticate.

Once sign-in is required:

Signed outSigned inJellyfin administrator
Board, live updates, streaming, read-only APINoYesYes
/manage, adding and removing torrents, settingsNoNoYes

Authentication uses Jellyfin's public AuthenticateByName endpoint. Signing out of Tide also revokes the Jellyfin access token it was issued. Sessions live in the same SQLite file as the rest of Tide's state and survive restarts.

If the server moves and you are locked out, start Tide with TIDE_REQUIRE_LOGIN=false. The environment variable overrides the stored setting in both directions.

⁠Memory guard

Tide watches its own RSS.

  • When it can read the container memory cap from cgroups, the guard turns itself on.
  • TIDE_MEMORY_LIMIT_MB, TIDE_MEMORY_PAUSE_MB, and TIDE_MEMORY_RESUME_MB override or force the thresholds.
  • Crossing the pause line pauses active torrents and disconnects peers.
  • Activity resumes only after RSS falls below the resume line.
  • TIDE_MEMORY_CHECK_INTERVAL_MS is how often it looks. Default 5000.

Without mem_limit (and without TIDE_MEMORY_LIMIT_MB) there is no cap to read and nothing to pause against.

⁠Coral modules

Tide publishes a manifest at /api/coral/manifest and, to a caller with a bearer token, the downloads.list and downloads.events capabilities. That is how Librarian⁠ learns a download has finished. Mint a token on /manage, or set CORAL_SERVICE_TOKEN for a compose-only setup. The environment token is full access and is never written to SQLite.

/api/coral/* is checked by that bearer token. HTTP basic auth does not sit in front of it, so another container can call Tide without the shared password.

⁠Configuration

VariableDefaultPurpose
TIDE_DOWNLOADS_DIR./data/downloadsWhere finished downloads land. Partials go in a sibling incomplete directory
TORRENT_DOWNLOADS_DIR—Legacy alias, used only when TIDE_DOWNLOADS_DIR is unset
TIDE_DATA_DIR./dataDirectory for tide.sqlite
TIDE_AUTH_USERNAME—HTTP basic auth. Set both or neither
TIDE_AUTH_PASSWORD—HTTP basic auth
TIDE_JELLYFIN_URL—Jellyfin server used as an identity provider
TIDE_REQUIRE_LOGINstored settingForces sign-in on or off, overriding SQLite
TIDE_MEMORY_LIMIT_MBcgroup capOverrides the detected container memory limit
TIDE_MEMORY_PAUSE_MB~90% of the limitPause torrents above this RSS
TIDE_MEMORY_RESUME_MB~75% of the limitResume only once RSS falls below this
TIDE_MEMORY_CHECK_INTERVAL_MS5000How often the guard re-checks
CORAL_SERVICE_TOKEN—Bearer token with full access for other modules
HOST0.0.0.0Bind address
PORT3000Listen port

/healthz returns 204.

⁠Develop

Node.js 24 LTS. Node 22.5+ is the floor: Tide uses node:sqlite, which is not in Node 18 or 20. Package manager is pnpm 10.32.1.

pnpm install
cp .env.example .env
pnpm dev

The app is at http://localhost:3000⁠.

pnpm dev        # Dev server on :3000
pnpm build      # Production build
pnpm start      # Production server (this is what applies basic auth)
pnpm typecheck  # TypeScript
pnpm check      # Biome lint + format
pnpm test       # Vitest
ToolRole
TanStack Start⁠Full-stack React
TanStack Router⁠File-based routing
TanStack Query⁠Server state
Tailwind v4⁠Styling
Biome⁠Lint and format
WebTorrent⁠BitTorrent engine
@get-coral/jellyfin⁠Jellyfin API client
Vitest⁠Tests

⁠CI

WorkflowWhenWhat
ci.ymlPull requests and pushes to mainTypecheck, lint, test, build, Docker build
docker-publish.ymlPush to main and version tagsgetcoral/tide on Docker Hub and ghcr.io/get-coral/tide on GHCR
release-please.ymlPush to mainRelease PR; the image publishes when it merges

Releases use Release Please⁠. feat: bumps minor, fix: bumps patch, feat!: or fix!: bumps major. docs: and chore: do not bump.

⁠License

MIT⁠. Copyright (c) 2026 Elian Van Cutsem.

⁠Part of Coral

Tide is one module in the Coral⁠ ecosystem. Read the contributing guide⁠ before opening a pull request.

Tag summary

Content type

Image

Digest

sha256:68768fa5e…

Size

112.2 MB

Last updated

11 days ago

docker pull getcoral/tide