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¶
- 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. -
Apply the migrations in
supabase/migrations/, in file-name order (0001_coreto0008_safe_retention). Either paste each file into the SQL Editor and run it, or use the Supabase CLI from the repository folder:The migrations create the tables with row level security, the RPCs, the private
datasetsstorage bucket, the Realtime publication, and the pg_cron jobstuneline-tick(every 5 minutes) andtuneline-daily(03:41 UTC). They enable thepg_cronandpg_netextensions themselves. -
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_KEYon Vercel. -
Check the background jobs exist:
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
appobject inapp.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 installpyproject.toml(the runner package) instead of the API's dependencies.buildCommand: builds the web app intotuneline/api/webui/and copies it, withoutrunner/, topublic/, which Vercel's CDN serves before the function. The installers therefore always come from the API with your addresses filled in.- The function
app.pygetsmaxDuration: 300seconds, and a Vercel Cron callsGET /api/cron/reconciledaily 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
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¶
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:
- URL Configuration. Site URL:
https://<your-domain>. Redirect URLs: addhttps://<your-domain>/**, andhttp://localhost:5173/**for local development. Without this, GitHub and Google send people back tolocalhost. 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). - 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.
- 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 URLhttps://<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 originhttps://<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.
-
New server key in Vault (SQL Editor):
-
Vercel → Settings → Environment Variables: set
TUNELINE_SERVER_KEYto the new value, andCRON_SECRETto a new random string of 32 or more characters. - Redeploy (run the deploy script again, or Deployments → Redeploy).
- Check
/api/healthshows"server_key_configured": trueand 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_*andserver_*functions as callable by the public role, andset_secret,delete_secret,create_worker,cancel_runas 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:
-
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.