Skip to content

Self-hosting

Run your own copy of Tuneline Studio on your own Supabase project and Vercel project, so that accounts, runs and API keys live in infrastructure you control.

You need a Supabase account, a Vercel account, Node.js 20 or newer, and a copy of the repository. The build runs on Vercel, so Python is only needed if you also want to develop locally (Contributing). Throughout this page, <your-project-ref> is your Supabase project's reference id and <your-domain> is the address Vercel gives your app (for example your-app.vercel.app).

Keep secrets out of the repository

The server key, CRON_SECRET and your Supabase personal access token never go into files you commit. Store them only in Supabase Vault and in Vercel's environment variables.

1. Create the Supabase project

  1. Create a project in the Supabase dashboard. Note its URL (https://<your-project-ref>.supabase.co) and, under Project Settings → API Keys, the public anon key (the legacy JWT) and the publishable key (sb_publishable_…). Both are public by design: every table is protected by row level security.
  2. Apply the migrations in supabase/migrations/, in file-name order (0001_core to 0008_safe_retention). Either paste each file into the SQL Editor and run it, or use the Supabase CLI from the repository folder:

    npx supabase link --project-ref <your-project-ref>
    npx supabase db push
    

    The migrations create the tables with row level security, the RPCs, the private datasets storage bucket, the Realtime publication, and the pg_cron jobs tuneline-tick (every 5 minutes) and tuneline-daily (03:41 UTC). They enable the pg_cron and pg_net extensions themselves.

  3. Create the server key in Vault. It is deliberately not in the migrations, so the value never lands in the repository. In the SQL Editor:

    select vault.create_secret(
      translate(encode(extensions.gen_random_bytes(36), 'base64'), '+/', '-_'),
      'tuneline_server_key',
      'Tuneline server key');
    select decrypted_secret from vault.decrypted_secrets where name = 'tuneline_server_key';
    

    Copy the value the second query shows: it becomes TUNELINE_SERVER_KEY on Vercel.

  4. Check the background jobs exist:

    select jobname, schedule, active from cron.job where jobname like 'tuneline%';
    

2. Your project's settings reach every part by themselves

You don't edit any code. The Vercel environment variables in step 3 (SUPABASE_URL, SUPABASE_ANON_KEY, and optionally SUPABASE_BROWSER_KEY) point every part of Tuneline at your Supabase project:

Part How it finds your project
The API Reads the environment variables directly.
The web app The build (vite.config.ts) uses SUPABASE_URL with SUPABASE_BROWSER_KEY, or SUPABASE_ANON_KEY, when SUPABASE_URL differs from the project built into web/src/lib/config.ts. VITE_SUPABASE_URL and VITE_SUPABASE_KEY still take precedence if you set them.
Cloud jobs and notebooks The API passes TUNELINE_SUPABASE_URL and TUNELINE_SUPABASE_KEY to every job.
Installers and background workers install.ps1 and install.sh read supabase_url and runner_supabase_key from your app's GET /api/config and pass them to the runner; the Windows background worker keeps them in its start-up script.
The pip run command Includes --supabase-url and --supabase-key whenever your project differs from the built-in one.
The deploy scripts Set TUNELINE_VERCEL_PROJECT and TUNELINE_VERCEL_SCOPE (your team slug) for the first link, or link the project yourself in step 4; once .vercel/project.json exists, the scripts skip linking.

Note

A runner started by hand without the installer (tuneline worker or tuneline run) uses the project built into the runner package. On such machines, set TUNELINE_SUPABASE_URL and TUNELINE_SUPABASE_KEY, or pass --supabase-url and --supabase-key (Runner CLI).

3. Create the Vercel project

Create a project in Vercel for the repository (import it from GitHub, or create it from the CLI in step 4). Settings:

  • Framework preset: FastAPI. Vercel finds the app object in app.py.
  • Install and build commands come from vercel.json; leave the dashboard defaults alone:
    • installCommand: python -m pip install --disable-pip-version-check -r requirements.txt -c constraints.txt. This is needed because Vercel's Python builder would otherwise install pyproject.toml (the runner package) instead of the API's dependencies.
    • buildCommand: builds the web app into tuneline/api/webui/ and copies it, without runner/, to public/, which Vercel's CDN serves before the function. The installers therefore always come from the API with your addresses filled in.
    • The function app.py gets maxDuration: 300 seconds, and a Vercel Cron calls GET /api/cron/reconcile daily at 04:13 UTC as a fallback for the database's own 5-minute job.

Environment variables (Settings → Environment Variables):

Variable Required Value
SUPABASE_URL yes https://<your-project-ref>.supabase.co
SUPABASE_ANON_KEY yes Your project's anon key (the API uses it for RPC calls).
TUNELINE_SERVER_KEY yes The value of the Vault secret tuneline_server_key from step 1.
CRON_SECRET yes, for the Vercel Cron fallback A random string of 32 or more characters.
TUNELINE_APP_URL optional https://<your-domain>. Needed for a custom domain; otherwise the API uses Vercel's production domain.
SUPABASE_BROWSER_KEY optional The key the browser uses, returned by /api/config and built into the web app (default: SUPABASE_ANON_KEY for your own project).
TUNELINE_CORS_ORIGINS optional Extra browser origins allowed to call the API, comma separated.
TUNELINE_WHEEL_URL optional Serve the runner wheel from somewhere else.
VITE_SUPABASE_URL, VITE_SUPABASE_KEY optional Override the web app's Supabase settings at build time (normally not needed: see step 2).

Set TUNELINE_SERVER_KEY and CRON_SECRET for Production only if other people can push branches to a connected GitHub repository, so code from a branch never runs with the server key. Don't use Vercel's per-deployment URLs as the app address: with Standard Deployment Protection they need a Vercel sign-in, so jobs could not download the runner from them.

4. Deploy

From the repository folder, link your Vercel project once, then deploy:

npx vercel@latest login
npx vercel@latest link          # pick your team and project; writes .vercel/project.json
# Windows: or double-click DEPLOY.cmd, which runs the same script
powershell -ExecutionPolicy Bypass -File .\scripts\deploy.ps1
# macOS / Linux / WSL
bash scripts/deploy.sh

The script checks your Vercel sign-in (and opens the browser sign-in if needed), skips linking because .vercel/project.json exists, and runs vercel deploy --prod. The build takes 2 to 4 minutes; the output is also saved in deploy-log.txt, and the full build log is under Deployments in the Vercel dashboard. Alternatively, connect the GitHub repository to the Vercel project: every push to main then deploys to production.

5. Check the deployment

curl -s https://<your-domain>/api/health

All three must be true:

{"ok": true, "version": "0.1.0", "runner_wheel_url": "https://<your-domain>/runner/tuneline-0.1.0-py3-none-any.whl",
 "server_key_configured": true, "background_checks": true}
Shows Fix
server_key_configured: false Add TUNELINE_SERVER_KEY in Vercel and redeploy (environment changes only apply to new deployments).
background_checks: false The database was not told the app's address. Check server_key_configured first (the value must equal the Vault secret); for a custom domain set TUNELINE_APP_URL to https://<your-domain> and redeploy. Only https:// addresses are accepted.

The first call to /api/health registers the app's address with the database. In the SQL Editor you can then see it, and within 5 minutes of an active cloud run, the database's calls to the API:

select value, updated_at from private.config where key = 'app_url';
select status, return_message, start_time from cron.job_run_details order by start_time desc limit 10;
select status_code, created from net._http_response order by created desc limit 5;

6. Configure sign-in

In the Supabase dashboard → Authentication:

  1. URL Configuration. Site URL: https://<your-domain>. Redirect URLs: add https://<your-domain>/**, and http://localhost:5173/** for local development. Without this, GitHub and Google send people back to localhost. On Windows, scripts/set-auth-urls.ps1 -AppUrl https://<your-domain> -ProjectRef <your-project-ref> does this through the Supabase Management API with a personal access token (revoke it afterwards if you don't need it).
  2. Email. The sign-in page sends no email: new accounts come from GitHub or Google, and existing accounts sign in with a password. SMTP (Authentication → Emails → SMTP Settings) is only needed for invitations or password resets you send from the dashboard; Supabase's built-in email only reaches members of your Supabase organization.
  3. Who can sign up. Migration 0009 refuses sign-ups from free email providers other than Gmail, Yahoo / Ymail and Outlook, and from throwaway inboxes; organization addresses are accepted. The rule runs in the database before a user is created, so it also covers accounts created through GitHub and Google: the address on that account decides. To accept a domain, run delete from private.blocked_email_domains where domain = 'example.com'; in the SQL editor; to refuse one, insert into private.blocked_email_domains (domain) values ('example.com');. 4. GitHub and Google sign-in. New accounts come only from these, so turn on at least one; each button appears on the sign-in page once its provider is on. GitHub: create an OAuth app at GitHub → Settings → Developer settings → OAuth Apps with the callback URL https://<your-project-ref>.supabase.co/auth/v1/callback, then paste its client id and secret under Sign In / Providers → GitHub. Google: in the Google Cloud console → Google Auth Platform, set up the consent screen (audience External, then Publish app) and create an OAuth client of type Web application with authorized JavaScript origin https://<your-domain> and the same callback URL as the authorized redirect URI, then paste its client id and secret under Sign In / Providers → Google.

Then sign up in your app, add a Hugging Face token with write access under Settings → API keys, and run the CPU smoke test template (Notebook or My computer) to check the whole path. docs/VERIFY_ON_FIRST_RUN.md lists what to check on the first real runs with each provider.

Rotate the server key and CRON_SECRET

Rotate both whenever they may have leaked, and before inviting other people if they passed through anyone else's hands. Launches fail between steps 1 and 3 (running jobs are not affected: they use run tokens), so do it when nobody is starting runs.

  1. New server key in Vault (SQL Editor):

    select vault.update_secret(
      (select id from vault.secrets where name = 'tuneline_server_key'),
      translate(encode(extensions.gen_random_bytes(36), 'base64'), '+/', '-_'));
    select decrypted_secret from vault.decrypted_secrets where name = 'tuneline_server_key';
    
  2. Vercel → Settings → Environment Variables: set TUNELINE_SERVER_KEY to the new value, and CRON_SECRET to a new random string of 32 or more characters.

  3. Redeploy (run the deploy script again, or Deployments → Redeploy).
  4. Check /api/health shows "server_key_configured": true and start a run.

If a user's provider key leaks, they delete it in Settings → API keys (it is removed from Vault) and revoke it at the provider. If a worker token leaks, remove that computer in Settings → Your computers and add it again.

Operating notes

  • Who can read keys. Provider keys are encrypted in Vault, and neither the browser nor the API logs ever show them. As the owner of the Supabase project you can still read Vault from the SQL editor: tell the people you invite, or have them use keys with the narrowest permissions their providers offer.
  • Expected Supabase advisor warnings. Advisors → Security lists runner_*, worker_* and server_* functions as callable by the public role, and set_secret, delete_secret, create_worker, cancel_run as callable by signed-in users. This is intended: each checks a run token, worker token, the server key or the caller's own rows first. Any other warning, such as a table without row level security, is not expected.
  • Log retention. Run logs older than 60 days are deleted daily. To keep them 7 to 3650 days:

    select public.server_set_config(
      (select decrypted_secret from vault.decrypted_secrets where name = 'tuneline_server_key'),
      'log_retention_days', '180');
    
  • Updating. Deploy again for the app. Add new migrations in file-name order (SQL editor or npx supabase db push). Runners in cloud jobs always install the version that matches the deployed app; users update runners on their computers by running the installer command again.