DEPLOY.md — hosting, deploy loop, credentials
RETIRED 2026-11-03 — kept for a self-hosted Bun checkout only. Production is Cloudflare Pages at https://trading-tyno.pages.dev; a push to
masterdeploys. Thetrading.tynolabs.com/dsh.tynolabs.com/terminal.tynolabs.comtunnel hosts and their DNS records are gone, and the phone/Pi services behind them are switched off. See DEPLOY-PAGES.md for the live path.
Architecture
Browser ── https://trading.tynolabs.com ──► Cloudflare edge (TLS)
│ cloudflared named tunnel "dsh" (outbound only)
▼
Pi (= dsh.tynolabs.com), 127.0.0.1:4103 (Caddy)
NO Basic Auth on trading.tynolabs.com
(the app's own login is the door;
dsh.tynolabs.com keeps Basic Auth org / parag123)
└─ host trading.tynolabs.com → 127.0.0.1:4102
systemd project@trading → bun run build/index.js
sqlite: /home/fp/trading/data.db
- Cloudflare → cloudflared → Caddy: Caddy does host routing (Basic Auth
on
dsh.tynolabs.comonly —trading.tynolabs.comhas no browser password; the app's/signingate is the door).dsh.tynolabs.com/*stays with DSH web (port 4100); thetrading.tynolabs.comhostname is routed to this app. - The Pi is
dsh.tynolabs.com— the same box you reach on the web as the tunnel is the machine that hosts this app. If you are on it (hostnamefp, checkout/home/fp/trading), you can always run the deploy; no SSH needed. - The app binds 127.0.0.1:4102 loopback only — nothing is exposed to the LAN or the internet except the outbound tunnel.
- Project source lives at
/home/fp/trading(moved from/home/fp/sites/tradingon 2026-09-07; systemdproject@tradingWorkingDirectory=/home/fp/trading)./home/fp/projects/tradingis a symlink to it so the standardproject@.servicetemplate unit works unchanged.
Credentials (org-wide, same as DSH web)
- Basic Auth (tunnel door) on
dsh.tynolabs.comONLY: userorg, passwordparag123.trading.tynolabs.comhas no browser password (removed 2026-09-26 — the app login already gates every work screen). - Sudo on the Pi (user
fp): password123— needed for the deploy loop (sudo bash scripts/deploy.sh …), Caddy/cloudflared edits, andsystemctlactions on the box. - Rotate it anywhere:
caddy hash-password --plaintext '<new>'→ paste the$2a$…hash into/etc/caddy/Caddyfile(basic_auth_orgblock) →sudo systemctl reload caddy. Every browser re-prompts on its next 401.
Env (.env — never committed; copy .env.example)
| Var | Value | Purpose |
|---|---|---|
PORT |
4102 |
loopback port (systemd project@trading) |
HOST |
127.0.0.1 |
bind loopback only |
ORIGIN |
https://trading.tynolabs.com |
URL building + SvelteKit CSRF (form POSTs 403 without it) |
TURSO_DATABASE_URL |
libsql://…turso.io |
shared remote DB (Turso). Set this + the token to use the shared DB. |
TURSO_AUTH_TOKEN |
<turso token> |
Turso auth token (secret) |
DATABASE_PATH |
/home/fp/trading/data.db |
local SQLite file fallback (used only when TURSO_DATABASE_URL is unset) |
BETTER_AUTH_SECRET |
<openssl rand -base64 32> |
better-auth signing secret |
Shared DB (libSQL / Turso)
The app talks to libSQL, so the hosted app and local dev can share ONE
database. When TURSO_DATABASE_URL + TURSO_AUTH_TOKEN are set, the app
uses Turso; when unset it falls back to the local DATABASE_PATH file.
- Local dev against the shared DB: put the two
TURSO_*vars in the dev.env(gitignored) and runbun --bun run dev. No SSH to the Pi needed. - Switch the host to Turso: add the two
TURSO_*vars to/home/fp/trading/.env, then deploy (the code is already async). Absent those vars, the host keeps serving its localdata.dbunchanged. - Schema:
bunx drizzle-kit push(withTURSO_*set) creates/updates the Turso schema.bunx drizzle-kit pushwithTURSO_DATABASE_URL=targets the local file instead. - Data migration (one-way, owner-run): export the existing SQLite
(
turso db import/.dump+ shell import) when ready to move prod data into Turso. Until then the Turso DB starts empty. - Never point tests at Turso:
tests/setup.ts+bunfig.tomlclear theTURSO_*vars, sobun test --isolatealways uses the local file.
First-time root setup (already done; re-run if the Pi is re-imaged)
Backup your edits first: sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak
- cloudflared — route the hostname + forward it to Caddy:
Incloudflared tunnel route dns dsh trading.tynolabs.com # CNAME → tunnel (as fp, uses ~/.cloudflared/cert.pem)/etc/cloudflared/config.yml, before the fallbackhttp_status:404:
Then- hostname: trading.tynolabs.com service: http://127.0.0.1:4103sudo systemctl restart cloudflared. - Caddy — in
/etc/caddy/Caddyfile, inside the:4103door, before the folder-sites catch-all:
and add@trading host trading.tynolabs.com handle @trading { reverse_proxy 127.0.0.1:4102 { header_up Host {http.request.host} } }/trading /trading/*to the@folder not pathexclusion list (otherwise the folder-sites catch-all serves the source tree). Validate + reload:sudo /usr/local/bin/caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile && sudo systemctl reload caddy. - systemd — the
project@.servicetemplate unit runs a project from/home/fp/projects/<name>:mkdir -p /home/fp/projects && ln -sfn /home/fp/trading /home/fp/projects/trading sudo systemctl daemon-reload sudo systemctl enable --now project@trading - Bun — the app is built and served by Bun, so the runtime is part of
the host, not an accident. The version the app is known good on is pinned in
.bun-versionat the repo root (adapter-bun refuses anything below 1.4):
It never downgrades a host that is ahead, and it copies the old binary aside first, so a failed download leaves the host exactly as it was.bash scripts/bun-upgrade.sh --install # first time on a new host bash scripts/bun-upgrade.sh # after a pin bump, then restart the app bash scripts/bun-upgrade.sh --check # exit 1 when the host is behind the pindeploy.sh's service restart is what makes new spawns pick up a new binary.
Also sync these edits back into the pi-deploy checkout
(/home/fp/pi-deploy/Caddyfile, /home/fp/pi-deploy/cloudflared/config.yml),
which is the source for re-imaging — otherwise a future deploy-pi.sh
clobbers the routes.
The deploy loop (the rules — tests pass, docs first, commit, push)
Where a deploy may run
- Only a deploy host may deploy to
trading.tynolabs.com— the Raspberry Pi (hostnamefp) that isdsh.tynolabs.com(tunneldsh), live checkout/home/fp/trading(/home/fp/sites/tradingis the pre-2026-09-07 path, still accepted); the VPSvps-d3go,/root/trading; and, since 2026-10-23, the phone (UserLAnd proot on Android, tunnelphone-tyno), live checkout/home/userland/trading.scripts/deploy.shdecides by checkout path, never hostname (the phone reportslocalhost), and refuses anywhere else (exit 3); bypass only if intentional withDEPLOY_HOST_FORCE=1.bash scripts/deploy.sh --can-deployanswers just that question (0 / 3) without pulling, building or committing — the row watcher uses it every tick. - If your shell is on the Pi (working dir
/home/fp/trading,hostname=fp), you are already on the deploy host — runsudo bash scripts/deploy.sh "msg"directly; no SSH, no remote step. On the phone the same command runs withoutsudo, and the repo's~/bin/systemctl/~/bin/systemd-runshims supply the restart and the deferred watcher reload (there is no init on Android). - On any other host (a dev box / localhost), do not build and do not
deploy. Run the app with
bun --bun run dev(vite dev) instead — novite build, nosystemctl restart, nodeploy.sh. Leave the change committed + pushed; the deploy host picks it up when it deploys. - While on
bun --bun run dev, verify the UI through the browser MCP server (browser-control) — highly prioritise it over curl / static inspection: open the real dev URL, click/type, read the ARIA snapshot, screenshot. - On the deploy host, the "ALWAYS deploy" rule below still holds.
ALWAYS deploy — on the deploy host. There, every turn that touches code or docs ends in a
deploy.shrun (or a doc-only commit + push). A change that is not live on https://trading.tynolabs.com AND pushed to GitHub is not done — never leave a fix built-but-undeployed or committed-but-unpushed. Parallel sessions do not excuse holding a deploy:deploy.shrunsgit add -A, so it commits the whole tree — coordinate with the other session viagit status+ pull-rebase, never by skipping the deploy.
Every deploy must pass bun test --isolate, end with a documentation check, a
commit, and a push to the private GitHub repo
(git@github.com:skpawar1305/trading.git /
https://github.com/skpawar1305/trading). A deploy that is not committed
and pushed is not done — and neither is one that ships with a red test suite
(Repo rules 2 & 3: every solved edge case gets a test; passing tests is as
important as commit + push — see docs/TESTS.md).
Always git pull --rebase before committing; if the pull conflicts,
resolve all conflicts automatically — never hand them back to the user.
# 1. change code (+ a regression test for every solved edge case)
# 2. type check: bun run check # svelte-fast-check (not svelte-check)
# 3. update docs if anything they describe changed (STACK.md / DEPLOY.md / README.md)
# 4. deploy + commit + push in one step:
sudo bash scripts/deploy.sh "describe what changed"
What scripts/deploy.sh does:
bun test --isolate— the whole suite must be green; a red suite aborts the deploy before build/restart/commit/push.bun --bun run buildintobuild.next(ADAPTER_OUT=build.next, adapter-bun writes the side directory), thenbun scripts/build-swap.ts:build→build.prev(kept for rollback) andbuild.next→build, two renames on the same filesystem, so the running app serves the complete previous build for the whole build instead of a half-writtenbuild/. The previous build's hashed client chunks are carried into the new build (and registered in its asset table) so an already-open tab keeps working. Roll back withmv build build.failed && mv build.prev build && sudo systemctl restart project@trading.systemctl restart project@trading(via sudo).- Optional health check:
curl -s -o /dev/null -w '%{http_code}' -H 'Host: trading.tynolabs.com' http://127.0.0.1:4103/→ expect200. git add -A && git commit && git push(a commit message argument is required; if.envordata.dbshow up, the gitignore is wrong).
Non-negotiable: a build is not live until the service runs the new code. ALWAYS restart the service after a build/deploy.
vite buildinto the servedbuild/— the running process keeps serving whatever it loaded at start. A build done while the service runs makes the served HTML reference old hashed asset names (/_app/immutable/…) that the rebuild deleted; browsers then getENOENTand many pages 500 until the restart happens (seen 2026-09-03: every page 500 after a build without restart). The sanctioned path isscripts/deploy.sh, which restarts after every build; for any manual build (e.g. the command below),sudo systemctl restart project@tradingis mandatory, not optional.
Manual alternative:
bun test --isolate && ADAPTER_OUT=build.next bun --bun run build && \
bun scripts/build-swap.ts && sudo systemctl restart project@trading
git add -A && git commit -m "..." && git push
DB backup / restore
The whole database is one file, data.db (gitignored).
sudo systemctl stop project@trading
cp /home/fp/trading/data.db data.db.$(date +%F-%H%M) # or off-box
sudo systemctl start project@trading
Schema changes: drizzle-kit generate (commit drizzle/) then
drizzle-kit push (apply), then rebuild/restart/deploy per the loop.
Troubleshooting
- Form POST → 403 "Cross-site form submissions":
ORIGINin.envdoes not match the browser origin. Fix.env, restart, commit, push. - No CSS / assets 404 under a sub-path: do not host this app under
dsh.tynolabs.com/trading— adapter-bun@1.0.1 serves/_app/*without a base prefix. It is designed for the root of its own host. nullsession in curl smoke tests: the session cookie isSecure; curl over plain HTTP won't send it (and a real browser only sends it over HTTPS). Test with-H "Cookie: __Secure-better-auth.session_token=<full value incl. signature>".- Caddy stops routing after a re-deploy: someone ran
deploy-pi.shfrom an unsynced pi-deploy checkout. Re-sync per the note above. - Service keeps restarting:
journalctl -u project@trading -n 50. Most common cause: missingBETTER_AUTH_SECRET(better-auth refuses to boot in production without it).