Architecture¶
How Tuneline Studio is built: a React app, a Supabase project, a FastAPI control plane on Vercel, and a runner that executes pipelines on the user's chosen compute.
docs/CONTRACTS.md in the repository is the source of truth for every interface described here (tables, RPCs, run spec, runner events, API routes). Read it before changing anything that crosses a boundary, and update it in the same change.
Overview¶
flowchart LR
subgraph Browser
SPA["React SPA<br/>(Vite, React Flow)"]
end
subgraph Supabase
Auth["Auth"]
DB[("Postgres<br/>+ row level security")]
Vault["Vault<br/>(user API keys,<br/>server key)"]
Storage["Storage<br/>(datasets bucket)"]
RT["Realtime"]
Cron["pg_cron + pg_net"]
end
subgraph Vercel
API["FastAPI control plane<br/>/api/*"]
CDN["Static web build<br/>(public/)"]
end
subgraph Compute["Where runs execute"]
Cloud["Modal · RunPod ·<br/>HF Jobs · Kaggle"]
NB["Notebook<br/>(Colab, Jupyter)"]
Worker["tuneline worker<br/>(your computer)"]
Runner["tuneline run"]
end
HF["Hugging Face Hub<br/>(user's account)"]
SPA -- "supabase-js: sign-in, own rows,<br/>uploads, live updates" --> Supabase
SPA -- "fetch /api/* with<br/>Bearer access token" --> API
CDN -. serves .-> SPA
API -- "server_* RPCs<br/>+ server key" --> DB
API -- "launch with the<br/>user's keys" --> Cloud
Cron -- "POST /api/cron/reconcile<br/>every 5 min" --> API
Cloud --> Runner
NB --> Runner
Worker -- "worker_claim" --> DB
Worker --> Runner
Runner -- "runner_* RPCs<br/>+ run token" --> DB
Runner -- "models, datasets,<br/>cache" --> HF
API -- "endpoints" --> Cloud
| Part | What it does | Where the code is |
|---|---|---|
| Web app | Single-page React app: canvas, run pages, datasets, registry, settings. Talks to Supabase directly for the signed-in user's own rows and to the API for everything that needs provider keys or the server. | web/ |
| Supabase | Auth (GitHub and Google sign-in; email and password for existing accounts), Postgres with row level security on every table, Vault for API keys, Storage for uploaded datasets, Realtime for live run pages, pg_cron for background checks. | supabase/migrations/ |
| Control plane | FastAPI app deployed as one Vercel Python function (app.py). Validates and compiles pipelines, launches jobs with the user's own keys, estimates, Hub search, deployments, playground, reconcile. Also serves the web build, the runner installers and the runner wheel. |
app.py, tuneline/api/ |
| Runner | The tuneline command. Fetches a run with its token, executes the blocks in order, reports events, reuses cached results and pushes artifacts to the user's Hugging Face account. |
tuneline/runner/ |
| Launchers and deployers | Start jobs on Modal, RunPod, Hugging Face Jobs, Kaggle, notebooks or a local worker; create vLLM endpoints on RunPod Serverless, Hugging Face Inference Endpoints and Modal. | tuneline/launchers/, tuneline/deployers/ |
Tuneline is multi-user: anyone can sign up, every table is per user under row level security, each user stores their own provider keys and pays for their own compute, and models go to their own Hugging Face account.
Trust boundaries¶
Three kinds of credentials reach the database, and each unlocks a narrow set of functions:
| Caller | Credential | Can do |
|---|---|---|
| Browser | Public key + the user's access token | Read and write the user's own rows under RLS; set_secret, delete_secret, create_worker, cancel_run. Never reads a key's value (only its last four characters). |
| API | Public key + the server key (TUNELINE_SERVER_KEY, checked against the Vault secret tuneline_server_key) |
server_* functions: read a user's keys, create and update runs, issue run tokens, manage deployments, set config. The API has no service-role key. |
| Runner | Public key + a run token (tlr_…) |
runner_* functions for that one run: fetch the spec and only the keys the pipeline needs, report events, look up the cache, read a golden set. |
| Worker | Public key + a worker token (tlw_…) |
worker_heartbeat, worker_claim for that user's local runs. |
Tokens are stored only as SHA-256 hashes. Run tokens expire after 14 days (at most 30), and 2 hours after the run ends; worker tokens last until the computer is removed. See docs/SECURITY.md.
Run lifecycle¶
sequenceDiagram
participant U as Browser
participant A as API (Vercel)
participant D as Supabase
participant P as Provider / worker
participant R as tuneline run
U->>A: POST /api/runs {graph, target, target_config}
A->>A: validate graph, check keys, estimate, compile spec
A->>D: server_create_run (+ run token for cloud targets)
A->>P: launch job with the user's provider keys
A-->>U: {run, launch, estimate, warnings}
P->>R: pip install runner wheel, tuneline run --token tlr_...
R->>D: runner_fetch (spec, needed keys, profile)
loop every step in spec.order
R->>D: runner_cache_lookup
R->>D: runner_report (step, log, metric, artifact events)
end
R->>D: runner_report (run: succeeded / failed / cancelled)
D-->>U: Realtime updates of runs, run_steps, run_logs, run_metrics, artifacts
D->>A: every 5 min: POST /api/cron/reconcile (server key)
A->>P: check job status, remove finished pods, refresh endpoints
- Create.
POST /api/runschecks the graph's shape and validates it, checks that the user saved every mandatory key (for the blocks and for the target), resolves uploaded datasets to 7-day signed URLs, resolves registry model versions and golden sets, estimates memory, time and cost, picks a GPU if none was chosen, and compiles the graph into a run spec (tuneline/spec.py: nodes with all params, the order, datasets,required_secrets, settings such asuse_cache,max_hours,app_url).server_create_runstores the run asqueued, with a run token for every target exceptlocal. - Launch. For Modal, RunPod, Hugging Face Jobs and Kaggle the API sets
launching, starts the job with the user's keys (read from Vault) and recordsprovider_job_id. Every job runs the same script: installtuneline[train,gguf]from<app>/runner/tuneline-<version>-py3-none-any.whl, thentuneline run --token "$TUNELINE_RUN_TOKEN". For Notebook the run staysqueueduntil the user downloads the notebook (GET /api/runs/{id}/notebook) or copies a command (GET /api/runs/{id}/command), each with a fresh token. For My computer the worker claims it:worker_claimpicks the oldest queuedlocalrun for that user (and computer), moves it tolaunchingand issues a token, and the worker starts a childtuneline runwith the token in its environment. - Execute. The runner calls
runner_fetch, exports the keys as environment variables (never logged), reportsrun: runningandstep: queuedfor every node, then walksspec.order. For each step it computes the cache key, reuses cached outputs when allowed (cached), otherwise runs the block (runningwith progress, logs and metrics), saves each output and reports oneartifactevent per output port andsucceededwith a smalloutputssummary. A failed upstream step makes later stepsskipped. - Events. Events are batched (flushed every 2 seconds or 200 events, at most 500 events per call) and sent with
runner_report, at least every 15 seconds with a heartbeat when idle. The database writes them torun_steps,run_logs,run_metrics,artifacts,model_versionsanddeployments; Realtime pushes them to the open run page. - Finish. The runner reports
run: succeeded | failed | cancelledand exits0or1. It stops by itself atmax_hoursor whenmax_cost_usdis reached at the estimated hourly price, and at the next safe point when a report answer sayscancel_requested. - Reconcile. Triggers on
runssetfinished_atand shorten token lifetimes when a run ends, and close steps leftqueued/runningin a failed or cancelled run. Thetuneline-tickcron job (every 5 minutes) applies the provider-free rules itself (a local or notebook run without heartbeat for 20 minutes fails; a cloud launch that never recorded a job id fails after 10 minutes) and, when there are active cloud runs or deployments, callsPOST /api/cron/reconcilewith the server key from Vault. The API then asks each provider for the job's status, removes finished RunPod pods and refreshes endpoints. Vercel Cron calls the same route once a day as a fallback.
Run statuses: queued → launching → running → succeeded | failed | cancelled. Step statuses: queued, running, succeeded, failed, skipped, cached, cancelled.
Data model¶
All tables live in schema public, have a user_id (or are keyed by the user) and allow owners only under row level security. Private helpers (private.run_tokens, private.config) are not reachable from the browser.
| Table | Holds | Written by |
|---|---|---|
profiles |
Display name, Hugging Face username, settings | Sign-up trigger; the user; the API (hf_username after a key test) |
user_secrets |
Name, Vault secret id and last-four hint of each API key (never the value) | set_secret / delete_secret |
pipelines |
Name, description, template, graph (the canvas) |
Browser |
datasets, dataset_versions |
Datasets and their versions: source (upload, Hub, URL), storage path, checksum, format, columns, stats, sample rows | Browser |
runs |
One run: status, target and target config, graph, compiled spec, estimate, provider job id and state, worker, cost, error, timestamps, token hash | API, runner |
run_steps |
Status, progress, message, cache key and outputs of each step | Runner |
run_logs, run_metrics |
Log lines; metric points (train/loss, eval/loss, report/<metric>, …) |
Runner |
artifacts |
Each output of each step: port, kind, URI, cache key, size, meta | Runner |
models, model_versions |
The model registry: models and their versions (adapter, merged or GGUF), metrics, model card, stage | Runner; browser (stage promotion) |
deployments |
Endpoints: provider, model URI, config, status, URL, provider state | API, runner |
eval_sets, eval_examples |
Golden sets and their examples | Browser (Playground) |
workers |
The user's computers: name, system info, last seen | create_worker; worker RPCs |
private.run_tokens |
Extra run tokens (hashes) with expiry | server_issue_run_token |
private.config |
app_url and log_retention_days |
server_set_config |
Storage bucket datasets is private, 50 MB per file, with paths <user_id>/<dataset_id>/<version>/<file_name>; each user can touch only their own folder. Realtime publishes runs, run_steps, run_logs, run_metrics, artifacts, workers and deployments. The daily cron job tuneline-daily deletes run logs older than log_retention_days (default 60), expired run tokens and old cron history.
Migrations, in order:
| File | Adds |
|---|---|
0001_core |
Tables, RLS policies, sign-up trigger, run numbering |
0002_secrets |
Vault-backed set_secret, delete_secret, server_get_secrets, the server-key check |
0003_runs_rpc |
Tokens, cancel_run, create_worker, worker_*, runner_* and server_* RPCs |
0004_storage_realtime |
The datasets bucket and its policies, the Realtime publication |
0005_cache_lookup |
runner_cache_lookup |
0006_run_tokens |
Several valid tokens per run; newest-first run listing for reconcile |
0007_housekeeping |
pg_cron and pg_net, private.config, status triggers, tuneline-tick and tuneline-daily |
0008_safe_retention |
The daily clean-up falls back to 60 days instead of failing on a bad log_retention_days |
Caching¶
Every step gets a content-addressed cache key (node_cache_key in tuneline/spec.py): a hash of the block type and version, the settings marked semantic (settings such as the Hub repository name or logging interval are left out, so renaming a repository does not retrain), the cache keys of its inputs ("<upstream key>:<port>"), and the content identity of its data (uploaded file checksum, downloaded file checksum, resolved Hub commit). Each output is stored with the cache key <node key>:<port>.
When settings.use_cache is on and the block is cacheable, the runner calls runner_cache_lookup with the keys of every output. If each one is found and loadable on this machine, the step is reported cached and its artifacts are reported again with meta.cached_from. Lookups only see the same user's artifacts.
Where outputs live so that other machines can reuse them:
- Datasets are saved as Parquet and, when the user has a Hugging Face token, uploaded to their private dataset repository
<hf_username>/tuneline-cacheatartifacts/<node key>/<port>.parquet(the node key, because cache keys contain a colon). Without a token they stay on the machine that made them. - Models (adapters, merged models) and GGUF files are pushed to the user's Hugging Face repositories when Push to Hugging Face is on; a cache hit checks the repository still exists.
- Evaluation reports are stored inline in the artifact record (
uri: "inline:"). Gates, publishing and deployments are never cached.
Repository map¶
| Path | What |
|---|---|
web/ |
React SPA: Vite, TypeScript, Tailwind v4, React Router, TanStack Query, @xyflow/react 12, Recharts, supabase-js. Builds into tuneline/api/webui/. |
app.py → tuneline/api/ |
FastAPI control plane (Vercel function): runs, launches, estimates, Hub search, deployments, playground, reconcile, static files. |
tuneline/registry.py, spec.py, templates.py |
Block registry (24 blocks), graph → run spec compiler and validator, templates. |
tuneline/config_defaults.py |
Public connection defaults baked into the runner and the API (Supabase URL and public keys). |
tuneline/runner/ |
The runner: CLI (run, worker, run-local, doctor), engine, event client, one module per block in nodes/. |
tuneline/launchers/, tuneline/deployers/ |
Modal, RunPod, Hugging Face Jobs, Kaggle, notebook and local launches; endpoints on RunPod Serverless, Hugging Face Inference Endpoints and Modal. |
tuneline/estimator.py, tuneline/catalog/ |
VRAM, time and cost estimator; model, GPU, provider and sample catalogs. |
supabase/migrations/ |
Schema, RLS, RPCs, storage, realtime, pg_cron housekeeping. |
web/public/runner/ |
install.sh / install.ps1, served by the API with the URLs filled in. |
tests/ |
pytest suites: test_api_* (with a fake Supabase) and test_runner_*. Web unit tests and Playwright tests live in web/. |
scripts/, DEPLOY.cmd |
Local setup, deploy and sample-data scripts. |
docs/ |
CONTRACTS.md, SETUP.md, SECURITY.md, VERIFY_ON_FIRST_RUN.md, DEVELOPMENT_NOTES.md, UI_GUIDE.md. |
documentation/ |
This documentation site (MkDocs Material) and its generator script. |
Where to go next¶
- Contributing: set up a development environment, run the tests, conventions.
- Self-hosting: run your own copy on your own Supabase and Vercel projects.
- REST API and Runner CLI: the interfaces in detail.