T
Sign in

DEPLOY.md — hosting, deploy loop, credentials

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 master deploys. The trading.tynolabs.com / dsh.tynolabs.com / terminal.tynolabs.com tunnel 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.com only — trading.tynolabs.com has no browser password; the app's /signin gate is the door). dsh.tynolabs.com/* stays with DSH web (port 4100); the trading.tynolabs.com hostname 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 (hostname fp, 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/trading on 2026-09-07; systemd project@trading WorkingDirectory=/home/fp/trading). /home/fp/projects/trading is a symlink to it so the standard project@.service template unit works unchanged.

Credentials (org-wide, same as DSH web)

  • Basic Auth (tunnel door) on dsh.tynolabs.com ONLY: user org, password parag123. trading.tynolabs.com has no browser password (removed 2026-09-26 — the app login already gates every work screen).
  • Sudo on the Pi (user fp): password 123 — needed for the deploy loop (sudo bash scripts/deploy.sh …), Caddy/cloudflared edits, and systemctl actions on the box.
  • Rotate it anywhere: caddy hash-password --plaintext '<new>' → paste the $2a$… hash into /etc/caddy/Caddyfile (basic_auth_org block) → 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 run bun --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 local data.db unchanged.
  • Schema: bunx drizzle-kit push (with TURSO_* set) creates/updates the Turso schema. bunx drizzle-kit push with TURSO_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.toml clear the TURSO_* vars, so bun test --isolate always 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

  1. cloudflared — route the hostname + forward it to Caddy:
    cloudflared tunnel route dns dsh trading.tynolabs.com   # CNAME → tunnel (as fp, uses ~/.cloudflared/cert.pem)
    
    In /etc/cloudflared/config.yml, before the fallback http_status:404:
      - hostname: trading.tynolabs.com
        service: http://127.0.0.1:4103
    
    Then sudo systemctl restart cloudflared.
  2. Caddy — in /etc/caddy/Caddyfile, inside the :4103 door, before the folder-sites catch-all:
        @trading host trading.tynolabs.com
        handle @trading {
            reverse_proxy 127.0.0.1:4102 {
                header_up Host {http.request.host}
            }
        }
    
    and add /trading /trading/* to the @folder not path exclusion 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.
  3. systemd — the project@.service template 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
    
  4. 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-version at the repo root (adapter-bun refuses anything below 1.4):
    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 pin
    
    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. deploy.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 (hostname fp) that is dsh.tynolabs.com (tunnel dsh), live checkout /home/fp/trading (/home/fp/sites/trading is the pre-2026-09-07 path, still accepted); the VPS vps-d3go, /root/trading; and, since 2026-10-23, the phone (UserLAnd proot on Android, tunnel phone-tyno), live checkout /home/userland/trading. scripts/deploy.sh decides by checkout path, never hostname (the phone reports localhost), and refuses anywhere else (exit 3); bypass only if intentional with DEPLOY_HOST_FORCE=1. bash scripts/deploy.sh --can-deploy answers 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 — run sudo bash scripts/deploy.sh "msg" directly; no SSH, no remote step. On the phone the same command runs without sudo, and the repo's ~/bin/systemctl / ~/bin/systemd-run shims 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 — no vite build, no systemctl restart, no deploy.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.sh run (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.sh runs git add -A, so it commits the whole tree — coordinate with the other session via git 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:

  1. bun test --isolate — the whole suite must be green; a red suite aborts the deploy before build/restart/commit/push.
  2. bun --bun run build into build.next (ADAPTER_OUT=build.next, adapter-bun writes the side directory), then bun scripts/build-swap.ts: build → build.prev (kept for rollback) and build.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-written build/. 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 with mv build build.failed && mv build.prev build && sudo systemctl restart project@trading.
  3. systemctl restart project@trading (via sudo).
  4. Optional health check: curl -s -o /dev/null -w '%{http_code}' -H 'Host: trading.tynolabs.com' http://127.0.0.1:4103/ → expect 200.
  5. git add -A && git commit && git push (a commit message argument is required; if .env or data.db show 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 build into the served build/ — 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 get ENOENT and many pages 500 until the restart happens (seen 2026-09-03: every page 500 after a build without restart). The sanctioned path is scripts/deploy.sh, which restarts after every build; for any manual build (e.g. the command below), sudo systemctl restart project@trading is 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": ORIGIN in .env does 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.
  • null session in curl smoke tests: the session cookie is Secure; 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.sh from an unsynced pi-deploy checkout. Re-sync per the note above.
  • Service keeps restarting: journalctl -u project@trading -n 50. Most common cause: missing BETTER_AUTH_SECRET (better-auth refuses to boot in production without it).