Skip to content
5 changes: 5 additions & 0 deletions .changeset/vast-yaks-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"trackio": minor
---

feat:Add server_url and TRACKIO_SERVER_URL for self-hosted servers; space_id and TRACKIO_SPACE_ID take precedence when both are set
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ Trackio's main features:
```
and keep your existing logging code.

- **Local-first** design: dashboard runs locally by default. You can also host it on Spaces by specifying a `space_id` in `trackio.init()`.
- Persists logs in a Sqlite database locally (or, if you provide a `space_id`, in a private Hugging Face Dataset)
- Visualize experiments with a **Svelte 5** dashboard locally (or, if you provide a `space_id`, on Hugging Face Spaces)
- **Local-first** design: dashboard runs locally by default. You can send metrics to a **Hugging Face Space** with `space_id` or to a **self-hosted Trackio server** you run yourself with `server_url` (or `TRACKIO_SERVER_URL`). If both a Space and a self-hosted URL are configured, the Space wins.
Comment thread
abidlabs marked this conversation as resolved.
Outdated
- Persists logs in a Sqlite database locally (or on the remote target you chose: Space, or the machine hosting your self-hosted server)
- Visualize experiments with a **Svelte 5** dashboard locally, on Hugging Face Spaces, or on your own host when you self-host the server
- **LLM-friendly**: Built with autonomous ML experiments in mind, Trackio includes a CLI for programmatic access and a Python API for run management, making it easy for LLMs to log metrics and query experiment data.
- Use `trackio query project --project <name> --sql "SELECT ..."` for read-only SQL when `trackio list` and `trackio get` are not enough
- See the storage schema and direct query reference at https://huggingface.co/docs/trackio/storage_schema
Expand Down Expand Up @@ -155,6 +155,18 @@ trackio.init(project="my-project", space_id="username/space_id")

it will use an existing or automatically deploy a new Hugging Face Space as needed. You should be logged in with the `huggingface-cli` locally and your token should have write permissions to create the Space.

## Self-hosted Trackio server

You can run the Trackio dashboard and API on your own machine or infrastructure and point training jobs at it over HTTP. Pass the **full URL including the `write_token` query** (as printed when the server starts, or use the `full_url` return value from `trackio.show()`):

```py
trackio.init(project="my-project", server_url="http://127.0.0.1:7860?write_token=YOUR_TOKEN")
```

You can also set `TRACKIO_SERVER_URL` to that full URL instead of passing `server_url`. If `space_id` / `TRACKIO_SPACE_ID` and `server_url` / `TRACKIO_SERVER_URL` are both set, Trackio uses the Hugging Face Space and ignores the self-hosted URL.
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README instructs passing a server_url with write_token, but the current RemoteClient drops URL query params when constructing API requests, so that token will not reach endpoints that require it (e.g., rename/delete). Please either implement token propagation/enforcement for self-hosted servers or adjust the README wording to match current behavior (token only for UI/mutations, not for metric logging).

Copilot uses AI. Check for mistakes.

See the documentation: [Self-host the Server](https://huggingface.co/docs/trackio/self_hosted_server).

## Syncing Offline Projects to Spaces

If you've been tracking experiments locally and want to move them to Hugging Face Spaces for sharing or collaboration, use the `sync` function:
Expand Down
2 changes: 2 additions & 0 deletions docs/source/_toctree.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
title: Track
- local: launch
title: Launch the Dashboard
- local: self_hosted_server
title: Self-host the Server
- local: deploy_embed
title: Deploy and Embed Dashboards
- local: manage
Expand Down
12 changes: 12 additions & 0 deletions docs/source/environment_variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ export TRACKIO_DIR="/path/to/trackio/data"

Note: This environment variable applies as long as Trackio is not running in a Space with persistent storage enabled. If Trackio is running in a Space with persistent storage enabled (which is detected with the `PERSISTANT_STORAGE_ENABLED` env variable), then the Trackio data will be stored in `/data/trackio`.

### `TRACKIO_SERVER_URL`

Full URL of a self-hosted Trackio server (HTTP or HTTPS), **including the `write_token` query parameter** (same URL as the dashboard’s write-access link or the `full_url` from `trackio.show()`). When set, `trackio.init()` sends metrics to that server. Equivalent to passing `server_url=` to `trackio.init()`.

**Precedence:** If `TRACKIO_SPACE_ID` is also set (or `space_id` is passed in code), the Hugging Face Space is used and `TRACKIO_SERVER_URL` is ignored. Same rule when both `space_id` and `server_url` are passed: `space_id` wins.

See [Self-host the Server](self_hosted_server.md).

```bash
export TRACKIO_SERVER_URL="http://127.0.0.1:7860?write_token=YOUR_TOKEN"
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This env var section states the URL must include write_token, but the current HTTP client drops base URL query strings when issuing requests and the self-hosted server doesn't require write_token for metric ingestion. Please clarify what the token is actually used for (mutations vs logging) or update the client/server so the token is enforced and propagated.

Suggested change
Full URL of a self-hosted Trackio server (HTTP or HTTPS), **including the `write_token` query parameter** (same URL as the dashboard’s write-access link or the `full_url` from `trackio.show()`). When set, `trackio.init()` sends metrics to that server. Equivalent to passing `server_url=` to `trackio.init()`.
**Precedence:** If `TRACKIO_SPACE_ID` is also set (or `space_id` is passed in code), the Hugging Face Space is used and `TRACKIO_SERVER_URL` is ignored. Same rule when both `space_id` and `server_url` are passed: `space_id` wins.
See [Self-host the Server](self_hosted_server.md).
```bash
export TRACKIO_SERVER_URL="http://127.0.0.1:7860?write_token=YOUR_TOKEN"
Base URL of a self-hosted Trackio server (HTTP or HTTPS). When set, `trackio.init()` sends metrics to that server. Equivalent to passing `server_url=` to `trackio.init()`.
Do **not** rely on a `write_token` query parameter in `TRACKIO_SERVER_URL` for metric ingestion. Write-access links (such as the dashboard’s write-access URL or the `full_url` from `trackio.show()`) may include `write_token`, but that token is for write-enabled dashboard / mutation flows rather than the logging path configured by this environment variable.
**Precedence:** If `TRACKIO_SPACE_ID` is also set (or `space_id` is passed in code), the Hugging Face Space is used and `TRACKIO_SERVER_URL` is ignored. Same rule when both `space_id` and `server_url` are passed: `space_id` wins.
See [Self-host the Server](self_hosted_server.md).
```bash
export TRACKIO_SERVER_URL="http://127.0.0.1:7860"

Copilot uses AI. Check for mistakes.
```

### `TRACKIO_LOGO_LIGHT_URL` and `TRACKIO_LOGO_DARK_URL`

Customize the logos displayed in the Trackio dashboard for light and dark themes. You can provide URLs to custom logos. Note that both environment variables should be supplied; otherwise, the Trackio default will be used for any variable that is not provided.
Expand Down
6 changes: 3 additions & 3 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
import trackio as wandb
```

- **Local-first** design: dashboard runs locally by default. You can also host it on Spaces by specifying a `space_id`.
- Persists logs locally (or in a private Hugging Face Dataset)
- Visualize experiments with a Gradio dashboard locally (or on Hugging Face Spaces)
- **Local-first** design: dashboard runs locally by default. You can also log to a Hugging Face Space (`space_id`) for free or to a **self-hosted** Trackio server (`server_url`)
- Persists logs locally, in a Hugging Face Dataset when using Spaces, or on the machine hosting your self-hosted server
- Visualize experiments with a Gradio dashboard locally, on Hugging Face Spaces, or on your own host when self-hosting
- **LLM-friendly**: Designed for autonomous ML experiments with CLI commands and Python APIs that enable LLMs to easily log and query experiment data.
- Everything here, including hosting on Hugging Face, is **free**!

Expand Down
77 changes: 77 additions & 0 deletions docs/source/self_hosted_server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Self-host the Trackio server

You can run the Trackio dashboard and API on your own machine (or any host you control) and send metrics from training scripts to that server over HTTP. This is an alternative to logging to a Hugging Face Space via `space_id`.

## Run the server locally

Install Trackio, then start the dashboard. By default it listens on `127.0.0.1` and uses the port from `GRADIO_SERVER_PORT` if set, otherwise **7860** (see [Launch the Dashboard](launch.md)).

<hfoptions id="language">
<hfoption id="Shell">

```sh
trackio show
```

</hfoption>
<hfoption id="Python">

```py
import trackio

trackio.show()
```

</hfoption>
</hfoptions>

Leave that process running while you train. The terminal prints a base URL for the UI and a URL that includes a **write token**; anyone who can reach that URL with the token can perform write operations supported by the server (for example renaming runs), so treat it like a secret on untrusted networks.

## Listen on all interfaces

To access the dashboard from another machine on your network (or from containers), bind to all interfaces:

<hfoptions id="language">
<hfoption id="Shell">

```sh
trackio show --host 0.0.0.0
```

</hfoption>
<hfoption id="Python">

```py
import trackio

trackio.show(host="0.0.0.0")
```

</hfoption>
</hfoptions>

Ensure your firewall and security policies allow the traffic you intend.

## Point training code at your server

In your training script, pass the **full URL including the `write_token` query parameter** (the same URL the dashboard prints for write access, or the `full_url` return value from `trackio.show()`). Logging requires that token; a host-only URL like `http://127.0.0.1:7860/` is not sufficient. You can also set the environment variable `TRACKIO_SERVER_URL` to that full URL instead of passing an argument.

Comment thread
abidlabs marked this conversation as resolved.
```py
import trackio

trackio.init(
project="my-project",
server_url="http://127.0.0.1:7860?write_token=YOUR_TOKEN",
)
trackio.log({"loss": 0.25})
trackio.finish()
```

Precedence: **`space_id` / `TRACKIO_SPACE_ID` always wins** over `server_url` / `TRACKIO_SERVER_URL` when both are set (in code or in the environment). Trackio then behaves as if only the Space were configured.

Hugging Face–specific options such as `dataset_id` and `bucket_id` apply only when logging to a Space. When only `server_url` is in effect, configure persistence on the machine where the server runs (for example via `TRACKIO_DIR` on that host). See [Environment Variables](environment_variables.md).

## Related

- [Launch the Dashboard](launch.md) — CLI options, port, and remote access
- [Environment Variables](environment_variables.md) — `TRACKIO_SERVER_URL`, `TRACKIO_DIR`, and others
11 changes: 11 additions & 0 deletions docs/source/track.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ trackio.init(project="my_project", name="tuned_run_1", group="tuned")

Runs with the same group name can be grouped together in sidebar, making it easier to compare related experiments. You can also group runs by any other configuration parameter (see [Tracking Configuration](#tracking-configuration) below).

### Remote logging (Hugging Face Space or self-hosted server)

By default, metrics are stored locally and you open the dashboard on your machine. You can instead send metrics to:

- A **Hugging Face Space**, by passing `space_id` (or setting `TRACKIO_SPACE_ID`). Trackio can create or reuse the Space and sync data there.
- A **self-hosted Trackio server** (HTTP or HTTPS), by passing `server_url` (or setting `TRACKIO_SERVER_URL`) to the **full URL including the `write_token` query** (for example the `full_url` from `trackio.show()`), not the host-only dashboard URL.

If both a Space and a self-hosted URL are configured (`space_id` / `TRACKIO_SPACE_ID` together with `server_url` / `TRACKIO_SERVER_URL`), **the Space takes precedence** and the self-hosted URL is ignored. Options such as `dataset_id` and `bucket_id` apply to Hugging Face deployments; when only `server_url` is in effect, configure storage on the host that runs the server (see [Environment Variables](environment_variables.md)).

For setup steps (running `trackio show`, binding to `0.0.0.0`, write tokens), see [Self-host the Server](self_hosted_server.md).
Comment thread
abidlabs marked this conversation as resolved.
Outdated

## Logging Data

Once your run is initialized, you can start logging data using the [`log`] function:
Expand Down
32 changes: 29 additions & 3 deletions tests/e2e-local/test_api.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import asyncio
import tempfile
from pathlib import Path
from urllib.parse import parse_qs, urlparse

import httpx
import pytest

import trackio
import trackio.context_vars as context_vars
import trackio.utils as trackio_utils
from trackio import Api
from trackio.remote_client import RemoteClient as Client
Expand Down Expand Up @@ -199,6 +201,31 @@ def test_local_dashboard_supports_remote_client(temp_dir):
app.close()


def test_server_url_logs_to_self_hosted_server(temp_dir):
project = "test_self_hosted"
run_name = "self-hosted-run"

app, url, _, full_url = trackio.show(block_thread=False, open_browser=False)

try:
write_token = parse_qs(urlparse(full_url).query).get("write_token", [None])[0]
assert write_token

context_vars.current_server.set(None)
context_vars.current_project.set(None)
context_vars.current_run.set(None)

trackio.init(project=project, name=run_name, server_url=full_url)
trackio.log(metrics={"loss": 0.5})
trackio.finish()

client = Client(url, verbose=False)
runs = client.predict(project, api_name="/get_runs_for_project")
assert any(r.get("name") == run_name for r in runs)
Comment thread
abidlabs marked this conversation as resolved.
finally:
app.close()


def test_local_dashboard_returns_400_for_missing_required_parameter(temp_dir):
app, url, _, _ = trackio.show(block_thread=False, open_browser=False)

Expand Down Expand Up @@ -326,6 +353,8 @@ def test_local_dashboard_upload_api_accepts_only_server_uploaded_paths(temp_dir)

def test_local_dashboard_supports_mcp(temp_dir):
pytest.importorskip("mcp")
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client

project = "test_local_mcp"
run_name = "mcp-run"
Expand All @@ -341,9 +370,6 @@ def test_local_dashboard_supports_mcp(temp_dir):
)

async def check_mcp() -> None:
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client

async with streamable_http_client(f"{url.rstrip('/')}/mcp") as (
read_stream,
write_stream,
Expand Down
53 changes: 41 additions & 12 deletions trackio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ def init(
name: str | None = None,
group: str | None = None,
space_id: str | None = None,
server_url: str | None = None,
space_storage: SpaceStorage | None = None,
dataset_id: str | None = None,
bucket_id: str | None = None,
Expand Down Expand Up @@ -249,6 +250,18 @@ def init(
via the `TRACKIO_SPACE_ID` environment variable. You cannot log to a
Space that has been **frozen** (converted to the static SDK); use
``trackio.sync(..., sdk="static")`` only after you are done logging.
Takes precedence over `server_url` and `TRACKIO_SERVER_URL` when more than
one is set.
server_url (`str`, *optional*):
Full URL of a self-hosted Trackio server, including the ``write_token`` query
parameter (for example the ``full_url`` value returned by ``trackio.show()``,
or the write-access URL printed when the dashboard starts). Logging and other
remote calls require that token; a base URL without ``write_token`` is not
enough. Example:
``"https://trackio.internal.example.com?write_token=..."``. When set, metrics are sent to
that server over HTTP instead of creating or syncing to a Hugging Face
Space. Can also be set via the `TRACKIO_SERVER_URL` environment variable.
Ignored when `space_id` or `TRACKIO_SPACE_ID` is set.
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server_url docstring says the write_token query parameter is required for logging/remote calls, but (1) the self-hosted server's /bulk_log endpoints currently don't require a write_token, and (2) RemoteClient builds request URLs with urljoin, which drops the base URL query string—so a write_token in server_url would not be sent to endpoints that do require it (e.g., delete/rename). Please either implement token propagation/auth for server_url or adjust the docs to describe what the token actually gates.

Suggested change
Full URL of a self-hosted Trackio server, including the ``write_token`` query
parameter (for example the ``full_url`` value returned by ``trackio.show()``,
or the write-access URL printed when the dashboard starts). Logging and other
remote calls require that token; a base URL without ``write_token`` is not
enough. Example:
``"https://trackio.internal.example.com?write_token=..."``. When set, metrics are sent to
that server over HTTP instead of creating or syncing to a Hugging Face
Space. Can also be set via the `TRACKIO_SERVER_URL` environment variable.
Ignored when `space_id` or `TRACKIO_SPACE_ID` is set.
Base URL of a self-hosted Trackio server. For example:
``"https://trackio.internal.example.com"``. If you are using a write-access
URL returned by ``trackio.show()`` or printed when the dashboard starts, it
may also include a ``write_token`` query parameter, for example
``"https://trackio.internal.example.com?write_token=..."``. Depending on the
server configuration, that token may be required for some write/admin
operations, but it is not required for all logging requests. When set,
metrics are sent to that server over HTTP instead of creating or syncing to
a Hugging Face Space. Can also be set via the `TRACKIO_SERVER_URL`
environment variable. Ignored when `space_id` or `TRACKIO_SPACE_ID` is set.

Copilot uses AI. Check for mistakes.
space_storage ([`~huggingface_hub.SpaceStorage`], *optional*):
Choice of persistent storage tier.
dataset_id (`str`, *optional*):
Expand Down Expand Up @@ -309,8 +322,17 @@ def init(
"* Warning: settings is not used. Provided for compatibility with wandb.init(). Please create an issue at: https://github.com/gradio-app/trackio/issues if you need a specific feature implemented."
)

space_id = space_id or os.environ.get("TRACKIO_SPACE_ID")
space_id, server_url = utils.resolve_space_id_and_server_url(space_id, server_url)
bucket_id = bucket_id or os.environ.get("TRACKIO_BUCKET_ID")
if server_url is not None and not server_url.startswith(("http://", "https://")):
raise ValueError(
f"`server_url` must be a full URL starting with http:// or https://, got: {server_url!r}"
)
if server_url is not None and (dataset_id is not None or bucket_id is not None):
raise ValueError(
"`dataset_id` and `bucket_id` are Hugging Face Spaces concepts and are not "
"compatible with `server_url`. Configure storage on the self-hosted server."
)
if space_id is None and dataset_id is not None:
raise ValueError("Must provide a `space_id` when `dataset_id` is provided.")
if dataset_id is not None and bucket_id is not None:
Expand All @@ -336,13 +358,16 @@ def init(
if space_id is not None:
deploy.raise_if_space_is_frozen_for_logging(space_id)

remote_source = space_id or server_url

url = context_vars.current_server.get()

if space_id is not None:
if remote_source is not None:
if url is None:
url = space_id
url = remote_source
context_vars.current_server.set(url)
context_vars.current_space_id.set(space_id)
if space_id is not None:
context_vars.current_space_id.set(space_id)

_should_embed_local = False

Expand All @@ -363,11 +388,15 @@ def init(
print(
f"* Trackio metrics will be synced to Hugging Face Dataset: {dataset_id}"
)
if space_id is None:
if remote_source is None:
print(f"* Trackio metrics logged to: {TRACKIO_DIR}")
_should_embed_local = embed and utils.is_in_notebook()
if not _should_embed_local:
utils.print_dashboard_instructions(project)
elif server_url is not None:
print(f"* Trackio metrics will be sent to self-hosted server: {server_url}")
if utils.is_in_notebook() and embed:
utils.embed_url_in_notebook(server_url)
else:
try:
deploy.create_space_if_not_exists(
Expand All @@ -390,21 +419,21 @@ def init(
context_vars.current_project.set(project)

remote_client = None
if space_id is not None:
if remote_source is not None:
try:
remote_client = RemoteClient(
space_id,
remote_source,
hf_token=huggingface_hub.utils.get_token(),
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: when remote_source is a user-provided URL (server_url), passing huggingface_hub.utils.get_token() into RemoteClient will send the user's Hugging Face token as an HTTP Authorization header to that server. Please avoid attaching HF tokens when the remote target is not a Hugging Face Space (e.g., pass hf_token=None for URLs and only use HF tokens for resolved HF Space sources). Also ensure the fallback RemoteClient construction paths in the _safe_get_*_for_init helpers follow the same rule.

Suggested change
hf_token=huggingface_hub.utils.get_token(),
hf_token=None
if server_url is not None
else huggingface_hub.utils.get_token(),

Copilot uses AI. Check for mistakes.
verbose=False,
)
except Exception as e:
_emit_nonfatal_warning(
f"trackio.init() could not create a Space client for '{space_id}': {e}. Continuing with local fallback metadata lookups."
f"trackio.init() could not create a remote client for '{remote_source}': {e}. Continuing with local fallback metadata lookups."
)

existing_run_records = _safe_get_runs_for_init(
project,
space_id,
remote_source,
resume,
remote_client=remote_client,
check_existing_for_never=name is not None,
Expand All @@ -415,7 +444,7 @@ def init(

existing_run = (
_safe_get_latest_run_for_init(
project, name, space_id=space_id, remote_client=remote_client
project, name, space_id=remote_source, remote_client=remote_client
)
if name is not None
else None
Expand All @@ -442,7 +471,7 @@ def init(
_safe_get_last_step_for_init(
project,
name,
space_id,
remote_source,
resumed,
run_id=resolved_run_id,
remote_client=remote_client,
Expand Down Expand Up @@ -473,7 +502,7 @@ def init(
run_id=resolved_run_id,
group=group,
config=config,
space_id=space_id,
space_id=remote_source,
Comment thread
abidlabs marked this conversation as resolved.
Outdated
existing_runs=existing_runs,
initial_last_step=initial_last_step,
auto_log_gpu=auto_log_gpu,
Expand Down
10 changes: 10 additions & 0 deletions trackio/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,16 @@ def on_spaces() -> bool:
return os.environ.get("SYSTEM") == "spaces"


def resolve_space_id_and_server_url(
space_id: str | None, server_url: str | None
) -> tuple[str | None, str | None]:
space_id = space_id or os.environ.get("TRACKIO_SPACE_ID")
server_url = server_url or os.environ.get("TRACKIO_SERVER_URL")
if space_id is not None:
server_url = None
return space_id, server_url


def _get_trackio_dir() -> Path:
if os.environ.get("TRACKIO_DIR"):
return Path(os.environ.get("TRACKIO_DIR"))
Expand Down
Loading