Files
pezkuwi-telegram-miniapp/ops/README.md
T
Satoshi Qazi Muhammed f6d53f51b1 ci: deploy over pinned, single-purpose keys; root installs the gate
The deploy reached VPS3 as root with one key, handed to appleboy actions
referenced by tag, and on every run it reinstalled the edge-function
ownership gate and its registry as root. That gate guards every project's
functions in the shared volume, so any change merged here could rewrite the
rule that protects pwap's functions.

Now CI connects as miniapp-deploy with two keys, each pinned on the host
(authorized_keys restrict,command=) to a script in ops/host:

- MINIAPP_SITE_DEPLOY_KEY -> miniapp-site-receive: the site tar on stdin,
  into /var/www/telegram.pezkiwi.app, nothing else.
- MINIAPP_FUNCTIONS_DEPLOY_KEY -> miniapp-functions-ssh: `gate-sha`, or
  `deploy` with functions.tgz on stdin, run through sudo for exactly those
  arguments.

Root installs the gate and registry from ops/; the deploy checks the host's
copies against them and refuses to deploy functions while they differ.

The asset cleanup that ran after each deploy (/opt/cleanup-miniapp.sh, only
on the host) protected lazy chunks by mtime and so removed live ones when a
build was shipped more than two hours after it was made; it took seven pages
down for about two minutes on 2026-10-09 while this was being tested, and
they were restored from the same tar. The receive script now keeps every
file of the new build and removes other assets only after four hours. Its
tests cover that (red with the keep rule removed) and the refusals; CI runs
them with shellcheck, and apply-repo-settings.sh requires the job.

VPS3's host key is pinned. The commented-out VPS1 deploy is gone; that host
no longer exists. Every action is pinned to a commit SHA.
2026-10-09 06:09:22 +04:00

5.3 KiB

ops

Infrastructure that runs on the Supabase host, kept here rather than only on the server.

supabase-deploy-functions + functions-registry.json

The ownership gate for the shared edge-function volume on vps3.

Two projects deploy into one volume: this repo and pwap-web. Before the gate existed, pwap-web rsynced its whole tree in, so whoever deployed last silently overwrote any function name they happened to share. On 2026-06-28 that replaced this project's telegram-auth with pwap-web's login-widget handler, and sign-in — plus every wallet screen behind it — returned 401 for a month. Nothing failed loudly, because the name still resolved; it just resolved to the wrong project's code.

The gate is the only supported way to write into that volume. It refuses any directory the calling project does not own, and refuses names absent from the registry entirely, so a new collision cannot be introduced by accident.

supabase-deploy-functions --project <name> --src <dir> [--restart] [--dry-run]
  • Validates every incoming directory before writing anything — a refused deploy leaves the volume untouched rather than half-updated
  • Writes atomically per function, so a reader never sees a partial function
  • Serialises with flock: two projects deploy here and both restart the same runtime. Without it, one deploy can recreate the container while another is mid-restart, which is what turned a successful deploy into a failed job on 2026-07-30
  • Retries the restart, and if it still fails, checks whether the runtime is actually up before reporting failure — a racing restart should not be reported as a broken deploy
  • Logs every decision to /var/log/supabase-function-deploys.log

rsync --delete and wholesale copying into that volume are not acceptable.

The registry

functions-registry.json records which project owns which function name. A new function must be added here before it can be deployed — that is the point: the gate refuses unknown names so a collision is caught at deploy time rather than discovered a month later.

Current ownership: 21 names to this project, 12 to pwap-web, 2 to the platform (hello, main).

_cloud_hosted lists functions that live on the cloud Supabase project (vbhftvdayqfmcgmzdxfv), not on vps3: telegram-bot and ask. Both Telegram bots reach the cloud project by webhook, and news.pex.mom's assistant calls ask there. Stale copies of both sit in the vps3 volume from before that split and serve no traffic — deploying them here updates a dead copy while the live one keeps running whatever was last pushed by hand.

pwap-web depends on this file too. It is versioned here because this project owns the larger share and the gate was built for its incident, but a pwap-web change that adds a function needs a PR here first. That is deliberate friction: the registry is the record of who owns what, and it should not be edited on the server where nobody can see the change.

Deployment

The gate and its registry guard every project's functions in the shared volume, so no project's CI installs them; that would let one project rewrite the rule that protects the others. Root installs them from this directory:

install -o root -g root -m 755 ops/supabase-deploy-functions /usr/local/bin/
install -o root -g root -m 644 ops/functions-registry.json /opt/supabase-self-hosted/

They are still versioned here, not edited in place on the server: before 2026-07 the script existed only at /usr/local/bin — one copy, no history, no review. deploy.yml checks the host's copies against these files (gate-sha) and refuses to deploy functions while they differ.

host/: what the deploy keys can do

CI reaches vps3 as miniapp-deploy, with two keys. Neither opens a shell: each is pinned in authorized_keys to one script here (restrict,command=).

key (secret) forced command input
MINIAPP_SITE_DEPLOY_KEY miniapp-site-receive site tar on stdin → /var/www/telegram.pezkiwi.app; removes earlier builds' assets after four hours, never the new build's
MINIAPP_FUNCTIONS_DEPLOY_KEY miniapp-functions-ssh → sudo miniapp-functions-deploy gate-sha, or deploy with functions.tgz on stdin

Install (root, on vps3):

install -o root -g root -m 755 ops/host/miniapp-* /usr/local/sbin/
useradd --system --create-home --shell /bin/bash miniapp-deploy
chown -R miniapp-deploy:miniapp-deploy /var/www/telegram.pezkiwi.app
# /etc/sudoers.d/miniapp-functions-deploy
miniapp-deploy ALL=(root) NOPASSWD: /usr/local/sbin/miniapp-functions-deploy deploy, /usr/local/sbin/miniapp-functions-deploy gate-sha
# ~miniapp-deploy/.ssh/authorized_keys
restrict,command="/usr/local/sbin/miniapp-site-receive" ssh-ed25519 ... miniapp-ci-site-deploy
restrict,command="/usr/local/sbin/miniapp-functions-ssh" ssh-ed25519 ... miniapp-ci-functions-deploy

host/test-site-receive.sh runs the site script against a scratch web root, including what it must refuse; CI runs it with shellcheck.

apply-repo-settings.sh

This repo's branch protection, as code. See the header in that file; run --check to report drift without changing anything.

Note: every CI job is listed individually because this repo has no aggregate gate job, so a renamed job silently drops a requirement. An aggregate job (as pwap-web has with CI Gate ✅) would be sturdier.