Self-hosted TikTok Stream Relay backend for Eclipse.
- Node.js 20+
- npm
- FFmpeg available on the host for local transcoding fallback
- Optional Torbox token for Debrid-first resolution
- Install dependencies:
npm install - Create an optional
.envfile:
PORT=3000
CACHE_ROOT=./cache
DEBRID_ENABLED=true
TORBOX_TOKEN=
REDIS_URL=
STREAM_TIMEOUT_MS=30000
STREAM_LOCAL_TTL_MS=172800000
STREAM_CACHE_MAX_BYTES=53687091200
SEARCH_MEMORY_TTL_MS=1800000
SEARCH_DISK_TTL_MS=86400000
LIVE_SEARCH_MAX_RESULTS=36
SEARCH_MAX_LIMIT=60
TIKTOK_AUTH_COOKIE=
SEARCH_RETRY_MAX_ATTEMPTS=2
SEARCH_RETRY_BASE_DELAY_MS=250
TRENDING_REFRESH_ON_STARTUP=false
TRENDING_REFRESH_INTERVAL_MS=21600000
TRENDING_SEED_QUERIES=sabrina carpenter,billie eilish,chappell roan,doechii,benson boone,lady gaga bruno mars
TRENDING_MAX_ITEMS=12
ADDON_CONFIG_ENABLED=true
ADDON_LIFECYCLE_ENABLED=true
ADDON_LINK_SIGNING_KEYS=
ADDON_CRYPTO_SECRET=
PUBLIC_LAUNCH_MODE=false
ADMIN_TELEMETRY_TOKEN=
PUBLIC_ALLOWLIST_IPS=For local use, blank addon secret values are fine and the service will use config/runtime-secrets.json.
If you do set ADDON_LINK_SIGNING_KEYS manually, use either v1:<secret> or a single raw secret, which is treated as v1.
- Start the dev server:
npm run dev - Build for production:
npm run build - Run the built service:
npm start - Refresh the live trending list:
npm run refresh:trending
The search endpoint is sound-first. For live keyword queries it prefers TikTok sound metadata from the music payload, then uses the matching video only as the source stream URL.
The server also refreshes config/trending-sounds.json automatically on startup and on a timer.
The repo now ships two compose entrypoints:
local-compose.yaml: local source-build path for direct self hosting.compose.yaml: template stack path for Traefik and prebuilt GHCR images.
Build the image manually:
docker build -t tiktok-stream-relay .Run the container:
docker run --rm -p 3000:3000 \
-e DEBRID_ENABLED=true \
-e TORBOX_TOKEN=your-token \
tiktok-stream-relayThe image installs FFmpeg in the runtime layer and serves the addon on port 3000.
Run local hosting compose:
docker compose -f local-compose.yaml up -d --buildRun template stack compose (Traefik style):
docker compose up -dTemplate compose prerequisites:
TIKTOK_STREAM_RELAY_HOSTNAMEset in.envDOCKER_NETWORKandDOCKER_NETWORK_EXTERNALaligned with your root stack settings- Traefik running in the same compose project
If you are using a VPS template stack (Traefik + shared external network), this is the recommended flow.
- Copy
env.templateto.env. - Set required stack values:
TIKTOK_STREAM_RELAY_HOSTNAME(public host, for examplerelay.example.com)DOCKER_DATA_DIR(for persistent cache and config mounts)DOCKER_NETWORKandDOCKER_NETWORK_EXTERNAL(same values used by your root template)
- Keep image settings for out of box startup:
GHCR_IMAGE=ghcr.io/ibbylabs/tiktok-stream-relayIMAGE_TAG=latest
- Render config to validate variables:
docker compose config
- Start:
docker compose up -d
Template mode notes:
compose.yamlusesexposeand Traefik labels, not direct host port mapping.- If you are not using Authelia yet, leave middleware commented out in
compose.yaml. - If you are not using Debrid yet, leave
TORBOX_TOKENempty and setDEBRID_ENABLED=false.
Deployment identity:
COMPOSE_PROJECT_NAME: container project name prefix.GHCR_IMAGE: container image repository to pull from in template mode.IMAGE_TAG: image tag to deploy (latestor a version tag).
Template reverse proxy:
TIKTOK_STREAM_RELAY_HOSTNAME: required in stack mode, host used by the Traefik router rule.DOCKER_NETWORK: compose network name matching your root template.DOCKER_NETWORK_EXTERNAL: whether that network is external.DOCKER_DATA_DIR: base path for persistent app data mounts.
Core runtime:
PORT: internal service port.CACHE_ROOT: cache root path.REDIS_URL: required for public launch mode.
Debrid and source access:
DEBRID_ENABLED: enable Torbox-first stream resolution.TORBOX_TOKEN: token used when Debrid is enabled.TIKTOK_AUTH_COOKIE: optional auth cookie to improve search fetch reliability.
Streaming and cache controls:
STREAM_TIMEOUT_MS: stream resolver timeout.STREAM_LOCAL_TTL_MS: local stream cache TTL.STREAM_CACHE_MAX_BYTES: max size of stream cache.SEARCH_MEMORY_TTL_MS: in-memory search cache TTL.SEARCH_DISK_TTL_MS: disk search cache TTL.
Search controls:
LIVE_SEARCH_MAX_RESULTS: max results collected from source query.SEARCH_MAX_LIMIT: API max allowedlimitvalue.SEARCH_RETRY_MAX_ATTEMPTS: retry attempts for transient search failures.SEARCH_RETRY_BASE_DELAY_MS: base delay for retry backoff.
Trending controls:
TRENDING_REFRESH_ON_STARTUP: refresh trending list on boot. Default isfalseso normal starts use the shipped cached list.TRENDING_REFRESH_INTERVAL_MS: periodic refresh interval.TRENDING_SEED_QUERIES: seed queries for trending discovery.TRENDING_MAX_ITEMS: max curated trending items.
Public portal and security:
ADDON_CONFIG_ENABLED: enable/configureportal.ADDON_LIFECYCLE_ENABLED: enable lifecycle mutation endpoints.ADDON_LINK_SIGNING_KEYS: required for secure public mode. Usev1:<secret>for explicit versioning. A single raw secret is accepted for local use and is treated asv1.ADDON_CRYPTO_SECRET: required for secure public mode.PUBLIC_LAUNCH_MODE: enforce strict public safety checks.ADMIN_TELEMETRY_TOKEN: required for/public/metricsand/public/events.PUBLIC_ALLOWLIST_IPS: optional allowlist for sensitive paths.
GHCR pull examples:
docker pull ghcr.io/ibbylabs/tiktok-stream-relay:latest
docker pull ghcr.io/ibbylabs/tiktok-stream-relay:v0.6.0Use the manifest URL in Eclipse:
http://localhost:3000/manifest.json
If you run the addon remotely, replace localhost with your host or VPS domain.
If you generate a tokenized link from /configure, use the exact URL provided. It now uses a path-safe format:
http://localhost:3000/addon/<token>/manifest.json
GET /manifest.jsonGET /healthGET /search?q=<query>GET /search?q=<query>&limit=<n>GET /search?q=<query>&limit=<n>&cursor=<opaque-cursor>GET /search?q=<query>&refresh=trueGET /album/:idGET /artist/:idGET /playlist/:idGET /stream/:id?url=<tiktok-url>&format=<mp3|aac|flac|m4a|wav|ogg>GET /media/:fileNameGET /cache/statsGET /configurePOST /api/config/previewPOST /api/config/createPOST /api/config/:linkId/updatePOST /api/config/:linkId/rotatePOST /api/config/:linkId/revokePOST /api/config/:linkId/rollbackGET /public/metricsGET /public/eventsGET /settingsPOST /settings
The stream endpoint accepts an optional format query param. Supported values are mp3, aac, flac, m4a, wav, ogg. The default when omitted is mp3.
Example:
curl -s "http://localhost:3000/stream/<track-id>?format=flac"Unsupported format values return 400 { "error": "unsupported_audio_format" }.
Every stream response includes a quality field that describes what you are actually receiving:
quality value |
Meaning |
|---|---|
source |
Direct URL from Debrid provider. Original quality, no transcoding. |
transcoded_standard |
Local ffmpeg transcode to mp3, aac, m4a, or ogg. |
transcoded_lossless_container |
Local ffmpeg transcode to flac or wav. |
| Field | Type | When present |
|---|---|---|
url |
string | Always — playable media URL. |
format |
string | Always — resolved format (mp3, aac, etc.). |
quality |
string | Always — one of the quality values above. |
provider |
string | Debrid responses only — identifies the stream source. |
expiresAt |
number | When the source provides an expiry — Unix timestamp in seconds. |
Important caveat for TikTok sources: TikTok stores audio as lossy compressed streams (typically AAC at variable bitrate). Requesting flac or wav produces a lossless container but the audio content is still derived from a lossy source. The transcoded_lossless_container label accurately describes the container and codec used, not the original recording quality.
flacandwavproduce significantly larger cache files thanmp3oraacfor the same track. A typical 3 minute TikTok sound transcoded to WAV (PCM 16-bit) will be 30 to 50 times larger than the equivalent mp3.ogg(libvorbis) andaacare efficient and behave similarly tomp3in cache footprint.- If disk space is limited, adjust
STREAM_CACHE_MAX_BYTESdownward when enabling lossless output formats, or keep the default format asmp3and let users select lossless on demand.
Albums, artists, and playlists returned by this addon are derived from TikTok search results. They are not a canonical TikTok catalog.
- Album — tracks with the same title and artist appearing in the same search result set, grouped together.
- Artist — tracks by the same creator appearing in the same result set, grouped together.
- Playlist — all results for a given query, surfaced as a browsable collection.
Because groups are derived from search results, detail pages (/album/:id, /artist/:id, /playlist/:id) are stable for approximately 10 minutes after the search that produced them. A fresh search for the same query refreshes the detail pages.
Grouping is normalized: minor differences in casing or whitespace in artist or title names may produce separate groups rather than merging entries.
Album and playlist detail responses include a description field. Album descriptions follow the pattern TikTok sounds by <artist>. Playlist descriptions are Trending sounds on TikTok for the trending playlist or TikTok sounds matching "<query>" for keyword playlists.
- Open
GET /configureto manage addon link generation and lifecycle operations. - Generated links are tokenized and rotatable.
- Streaming can use addon token credentials through
addonTokenquery param orx-addon-link-tokenheader. - Lifecycle mutations require a valid addon token whose link identity matches the route link id.
Example stream with addon token:
curl -s "http://localhost:3000/stream/<track-id>?addonToken=<token>"- Per-IP route class limits apply to portal, lifecycle, and stream surfaces.
- Adaptive throttling increases restrictions for repeated violations.
- Feature flags control portal and lifecycle exposure:
ADDON_CONFIG_ENABLEDADDON_LIFECYCLE_ENABLED
- Metrics and events are available at:
GET /public/metricsGET /public/eventsGET /public/events?limit=<n>— optional, cap the number of returned entries (1 to 500, default 50).
- Both telemetry endpoints require
x-admin-tokento matchADMIN_TELEMETRY_TOKEN. - In public launch mode (
PUBLIC_LAUNCH_MODE=true), the service requires secure values for:ADDON_LINK_SIGNING_KEYSADDON_CRYPTO_SECRETADMIN_TELEMETRY_TOKENREDIS_URL
Use authelia-rules.template.yaml for protected deployments.
Template stack flow (Viren style):
- Set
TIKTOK_STREAM_RELAY_HOSTNAMEin your stack.env. - In your Authelia compose environment, set
TEMPLATE_TIKTOK_STREAM_RELAY_HOSTNAME: ${TIKTOK_STREAM_RELAY_HOSTNAME?}. - Copy the rule block from
authelia-rules.template.yamlintoconfiguration.ymlbefore wildcard catch-all rules. - In
compose.yaml, uncomment the optional middleware label forauthelia@docker.
Standalone flow:
- Replace
{{ env "TEMPLATE_TIKTOK_STREAM_RELAY_HOSTNAME" }}with your public host. - Copy the same rules into your Authelia
access_controlsection before broad one_factor or two_factor rules. - Keep media routes (
/manifest.json,/addon/*/manifest.json,/search,/stream,/media) on bypass and protect the rest with your default policy.
Authelia is optional. Both compose paths still run without it.
Release flow:
npm run release:patch
npm run release:minor
npm run release:majorEach release script now:
- runs
lint,test, andbuild - refreshes
config/trending-sounds.jsonvianpm run refresh:trending - bumps
package.jsonandpackage-lock.json - syncs the add-on version in manifest.json
- refreshes versioned GHCR pull examples in README.md
- generates the matching CHANGELOG.md entry from commits since the previous tag
- stages all files touched by the release workflow (
git add -A) - commits the release, creates the semantic tag, and pushes
mainwith tags
Pushing a semantic tag like v0.1.1 triggers release.yml:
- create GitHub release notes from the matching CHANGELOG.md entry
- publish a GitHub release for that tag
- build and push multi-arch GHCR images (
linux/amd64,linux/arm64) - publish both
${tag}andlatestimage tags
The workflow keeps release notes, add-on versioning, changelog entries, and image tags aligned to the same release tag.
Example request:
curl -X POST http://localhost:3000/settings \
-H "content-type: application/json" \
-d '{"debridEnabled":true,"torboxToken":"token-value-1234"}'Install FFmpeg on the host and confirm ffmpeg is on PATH.
- Try
GET /search?q=trending - Try a live sound query like
GET /search?q=espresso - Try a broader sound query like
GET /search?q=random music - Check whether the source TikTok page changed its embedded payload structure
- Refresh the local trending list with
npm run refresh:trending - Use cached results while updating the parser extraction patterns
First page:
curl -s "http://localhost:3000/search?q=tate%20mcrae%20leaks&limit=20" | jqFollow-up page (use the returned nextCursor):
cursor="<nextCursor-from-previous-response>"
curl -s "http://localhost:3000/search?q=tate%20mcrae%20leaks&limit=20&cursor=${cursor}" | jqResponse fields:
tracks: normalized search results for the pagehasMore: whether another page may be availablenextCursor: opaque cursor for the next page whenhasMore=truepartial: true when throttling occurred after collecting partial results
- Verify the TikTok URL is absolute and uses a supported TikTok host
- If Debrid is enabled, confirm the token is still valid
- Retry with local fallback by disabling Debrid through
POST /settings
- Inspect
GET /cache/stats - Clear a specific search cache entry with
GET /search?q=<query>&refresh=true - Update Debrid tokens through
POST /settingsto clear search cache after token changes
npm run typechecknpm testnpm run lintnpm run format:checknpm run refresh:trending
This project is licensed under the MIT License. See LICENSE.