From cf2c8fbcc44766c983cb8736de4600e35c3f5d2f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 19:53:46 +0000 Subject: [PATCH 01/11] venari: run self-hosted Convex beside Ghost Ghost does not replace Convex, and the reverse is also true. Ghost hands out disposable Postgres databases, so it is the test database. Convex is an app backend: server functions, auth, and live queries that push to subscribers. It is what create-epoch-app is built on. This runs the open-source backend and dashboard on venari, the same way Soft Serve and Ghost run there: a /srv// twin in this repo, loopback-only ports, and ssh -L from the laptop. Decisions: - SQLite in data/, not Postgres. Upstream's README recommends SQLite as the start. Putting it in the Ghost cluster would give Convex's data the lifecycle of a cluster that is disposable by design. A separate Postgres would cost RAM a 4 GB box does not have spare. The move later is export, set POSTGRES_URL, import. This differs from what I first suggested (the Ghost cluster, with its own role); I changed it after reading upstream's docs. - Both images are pinned to 0cf49cb, the revision :latest pointed at on 2026-09-22. Upstream does not guarantee that a mismatched backend and dashboard work together. - No .env and no agenix secret. The backend generates its instance secret into data/credentials/. Admin keys are signed with it, every generate call yields a different valid key, and each project keeps its own key in a gitignored .env.local, as upstream documents. - DISABLE_BEACON and NEXT_PUBLIC_LOAD_MONACO_INTERNALLY are set, so the images make no outbound calls. - The tunnel recipes put their forwards inline rather than use an ssh alias like venari-ghost, so nothing outside the repo has to exist first. Checked in a sandbox container, not on venari, against the pinned images: - A project deployed with a schema, a mutation, a query and a "use node" action. - A ConvexClient subscription saw 252 pushed updates, from 1 row to 501, across 500 concurrent mutations. - Peak memory was 150 MiB for the backend and 270 MiB for the dashboard. That sets the limits at 512m and 384m, so the box's container limits total about 2 GB. - After a restart the data was intact and the old admin key still worked; a bogus key was rejected (BadAdminKey). - The ports bind to 127.0.0.1 only. `docker compose config` validates, and personal.just parses (rust-just --list). Not checked: the ssh tunnel itself (the sandbox has no sshd), and anything on the box. Specific changes: - tools/venari/convex/: docker-compose.yml and .gitignore (twin of /srv/convex) - tools/venari/README.md: table row, a "Convex" section (storage, measurements, access, admin keys, the loopback-only limit, setup, upgrades), and Convex's data/ added to the backups gap - tools/just/personal.just: convex-tunnel, convex-tunnel-close, convex-admin-key - CLAUDE.md: venari's service list in the structure tree Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- CLAUDE.md | 2 +- tools/just/personal.just | 21 ++++++ tools/venari/README.md | 92 +++++++++++++++++++++++++- tools/venari/convex/.gitignore | 1 + tools/venari/convex/docker-compose.yml | 74 +++++++++++++++++++++ 5 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 tools/venari/convex/.gitignore create mode 100644 tools/venari/convex/docker-compose.yml diff --git a/CLAUDE.md b/CLAUDE.md index a30c899d..5bf4d0f4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -147,7 +147,7 @@ dotfiles/ │ ├── mise/ # Global mise config for the no-Nix, non-admin account │ # (bootstrap.sh links it to ~/.config/mise; no module) │ └── venari/ # the rented box: twins of its /srv compose files (Soft Serve, -│ # Ghost) and /etc drop-ins; nothing deploys them +│ # Ghost, Convex) and /etc drop-ins; nothing deploys them ├── secrets/ # agenix/ragenix age-encrypted secrets ├── docker/ # container notes (per-arch CLAUDE.md) + entrypoint ├── docs/solutions/ # documented solutions to past problems - bugs, practices, diff --git a/tools/just/personal.just b/tools/just/personal.just index 99bb673b..91cb4edd 100644 --- a/tools/just/personal.just +++ b/tools/just/personal.just @@ -36,6 +36,27 @@ ghost-tunnel: ghost-tunnel-close: @pkill -f 'ssh -fN venari-ghost' && echo "ghost tunnel closed" || echo "no ghost tunnel open" +# Convex on venari (tools/venari/README.md, "Convex"): backend, HTTP actions and +# dashboard listen on the box's loopback. The forwards are inline rather than an +# ssh alias like venari-ghost, so nothing outside this repo has to exist first; +# the laptop ports match the box's because CONVEX_CLOUD_ORIGIN says 127.0.0.1:3210. +# Safe to re-run: ExitOnForwardFailure kills a second tunnel that cannot bind. + +# open the Convex tunnel: 3210 -> API, 3211 -> HTTP actions, 6791 -> dashboard +[group('personal')] +convex-tunnel: + @pgrep -qf 'L 3210:127.0.0.1:3210' && echo "convex tunnel already open" || ssh -fN -o ExitOnForwardFailure=yes -L 3210:127.0.0.1:3210 -L 3211:127.0.0.1:3211 -L 6791:127.0.0.1:6791 venari + +# close the Convex tunnel +[group('personal')] +convex-tunnel-close: + @pkill -f 'L 3210:127.0.0.1:3210' && echo "convex tunnel closed" || echo "no convex tunnel open" + +# print a new Convex admin key (each call differs; all stay valid until data/ is lost) +[group('personal')] +convex-admin-key: + @ssh venari-root 'cd /srv/convex && docker compose exec -T backend ./generate_admin_key.sh' | tail -1 + # The box has two vCPUs and no Go, so the server binary is cross-compiled here # and only the static binary travels; the image on the box is Alpine plus that # file (tools/venari/ghost/server/Dockerfile). diff --git a/tools/venari/README.md b/tools/venari/README.md index 989bd786..d8ccb985 100644 --- a/tools/venari/README.md +++ b/tools/venari/README.md @@ -22,6 +22,7 @@ the box is the running copy. | `ssh/soft-tunnel.authorized_keys` | `/var/lib/soft-tunnel/.ssh/authorized_keys` (root-owned, 644) | | `ghost/docker-compose.yml` | `/srv/ghost/docker-compose.yml`, with `.env` (from `.env.example`), `certs/`, `data/` and `server/` beside it | | `ghost/server/Dockerfile` | `/srv/ghost/server/Dockerfile`, next to the `ghost-server` binary it packages | +| `convex/docker-compose.yml` | `/srv/convex/docker-compose.yml`, with `data/` beside it (no `.env`) | The dev-box side is `tools/mise/venari/` — the same arrangement felixia has, for the same reason: no Nix here either. @@ -193,6 +194,92 @@ invites, shares, API-key management (the key is configuration). `ghost logs` returns an empty page. Backups: the cluster rides the provider's box backups, like Soft Serve's `data/`; Ghost databases are disposable by design. +## Convex: a self-hosted app backend + +[Convex](https://www.convex.dev) is the backend create-epoch-app is built on: +a document database, server functions (queries, mutations, actions), and live +queries that push new results to subscribed clients. `/srv/convex/` runs the +open-source backend and dashboard from +[get-convex/convex-backend](https://github.com/get-convex/convex-backend) +(`self-hosted/`), pinned to one revision for both images. + +It is not a replacement for Ghost, and Ghost is not one for it. Ghost hands out +disposable Postgres databases, so it is the *test* database. Convex is where an +app's data lives. That difference drives the storage choice: + +- **SQLite in `data/`**, which is upstream's default and its recommended starting + point. Not Postgres in the Ghost cluster. Convex's data would then share a + lifecycle with databases that are disposable by design, and a separate + Postgres would cost RAM this box does not have spare. Moving later is + `npx convex export`, then set `POSTGRES_URL`, then `npx convex import` + (upstream `self-hosted/advanced/postgres_or_mysql.md`). +- **`data/` is the whole state**: the database, stored files and modules, and + `credentials/`. The backend generates the instance name and secret there on + first start, so there is no `.env`. + +Measured on 2026-09-22, in a container off the box, against the pinned +revision. A project with a schema, a mutation, a query, and a `"use node"` +action deployed with `npx convex deploy` (convex 1.46.0). A `ConvexClient` +subscription saw 252 pushed updates, from 1 row to 501, while 500 concurrent +mutations ran. Peak memory was 150 MiB for the backend and 270 MiB for the +dashboard (at startup), so the limits are 512m and 384m. With Soft Serve and +Ghost, the box's container limits then total about 2 GB of its 4 GB. The +images take about 1.5 GB of disk (798 MB + 668 MB). These are not load +figures for this box: that machine was not venari, and venari has 2 vCPUs. + +All three ports listen on the box's loopback. From the laptop: + +```sh +just convex-tunnel # 3210 -> API, 3211 -> HTTP actions, 6791 -> dashboard +just convex-admin-key # prints a new admin key (via venari-root) +open http://127.0.0.1:6791 # dashboard; paste the admin key +just convex-tunnel-close +``` + +In a Convex project, put the URL and key in `.env.local`, which is gitignored +by the project and never in this repo. `npx convex dev` and `deploy` then +target venari: + +```sh +CONVEX_SELF_HOSTED_URL='http://127.0.0.1:3210' +CONVEX_SELF_HOSTED_ADMIN_KEY='' +``` + +Every call to `generate_admin_key.sh` returns a different key. All of them +stay valid, because they are signed with the instance secret in `data/`. A +key issued before a restart still worked after it. So there is no single key +to keep in agenix. Revoking a key means rotating the instance secret, which +invalidates every key. + +`CONVEX_CLOUD_ORIGIN` and `NEXT_PUBLIC_DEPLOYMENT_URL` say +`http://127.0.0.1:3210`, which is the laptop's end of the tunnel. That means +this deployment serves one person through ssh. A frontend that other people +use needs a public HTTPS origin for the API. That means a port other than 22 +in the cloud firewall and a TLS reverse proxy. It is a separate decision, +not a config change. + +Both outbound calls the images make by default are off. `DISABLE_BEACON` +turns off the anonymous usage ping. `NEXT_PUBLIC_LOAD_MONACO_INTERNALLY` +serves the dashboard's editor from the image instead of from a CDN. + +Setup, as root on the box, after copying `convex/` here to `/srv/convex/`: + +```sh +cd /srv/convex && umask 077 && mkdir -p data +docker compose up -d --wait +curl -s http://127.0.0.1:3210/version; echo +docker compose logs backend | grep -m1 'Connected to SQLite' +``` + +Upgrading, from upstream `self-hosted/advanced/upgrading.md`: + +1. Run `npx convex export` from a project against this deployment, so there is + a copy to restore. +2. Bump **both** image tags to the same revision. Upstream does not guarantee + that a mismatched backend and dashboard work together. +3. Run `docker compose up -d --wait`, then watch the logs for the + `MigrationComplete` line. + ## Updates and reboots `unattended-upgrades` runs daily and may reboot the box at 11:00 UTC — see @@ -209,7 +296,10 @@ original: a `*.bak-` left in `/etc/apt/apt.conf.d/` makes apt print ## Not covered yet -- Backups. `data/` (repos plus db) should be backed up as a unit, and the +- Backups. Soft Serve's `data/` (repos plus db) should be backed up as a unit, and the planned home for that is restic to R2. `ledger-age` is itself a backup (ciphertext, with the plaintext still on felixia), so losing it is recoverable. The Moment repos are not. + Convex's `data/` needs the same treatment. It is the one directory on this + box that holds application data nothing else has a copy of. Until restic + lands, `npx convex export` from the laptop is the only backup. diff --git a/tools/venari/convex/.gitignore b/tools/venari/convex/.gitignore new file mode 100644 index 00000000..8fce6030 --- /dev/null +++ b/tools/venari/convex/.gitignore @@ -0,0 +1 @@ +data/ diff --git a/tools/venari/convex/docker-compose.yml b/tools/venari/convex/docker-compose.yml new file mode 100644 index 00000000..affe44e6 --- /dev/null +++ b/tools/venari/convex/docker-compose.yml @@ -0,0 +1,74 @@ +# Convex on venari: the self-hosted Convex backend and its dashboard, from +# get-convex/convex-backend's self-hosted/docker/docker-compose.yml. Both listen +# on the box's loopback only and are reached over ssh -L, like Soft Serve and +# Ghost: the cloud firewall admits 22 alone. +# +# On the box: /srv/convex/docker-compose.yml, next to data/ (never tracked). +# data/ is the whole state: the SQLite database, stored files and modules, and +# credentials/ - the instance name and secret the backend generates on first +# start. Admin keys are signed with that secret: `generate_admin_key.sh` gives a +# different key on every call, and every one stays valid as long as data/ does +# (checked 2026-09-22: a key issued before a restart still worked after it). +# There is no .env: nothing here is a secret that data/ does not already hold. +# +# Storage is SQLite, upstream's default and its recommended starting point. +# Not Postgres in the Ghost cluster: those databases are disposable by design, +# and Convex's are not. Moving later is `npx convex export`, set POSTGRES_URL, +# `npx convex import` (upstream: self-hosted/advanced/postgres_or_mysql.md). +# +# Upstream ships `:latest`. Both images are pinned to one revision here, because +# upstream says a backend and dashboard of different versions are not +# guaranteed to work together. Upgrading: self-hosted/advanced/upgrading.md - +# `npx convex export` first, then bump BOTH tags. +name: convex +services: + backend: + # 0cf49cb = get-convex/convex-backend main on 2026-09-21; the tag :latest + # pointed at on 2026-09-22. + image: ghcr.io/get-convex/convex-backend:0cf49cbf8c4b7e22e631ceda9b5111f6cccbcd49 + container_name: convex-backend + restart: unless-stopped + stop_grace_period: 10s + stop_signal: SIGINT + volumes: + - ./data:/convex/data + ports: + - "127.0.0.1:3210:3210" # client API, websocket sync + - "127.0.0.1:3211:3211" # HTTP actions + environment: + # What clients are told the backend is: the laptop's end of the tunnel + # (`just convex-tunnel` forwards the same port numbers). These appear in + # storage URLs and action callbacks, so they must be the address the + # client actually uses, not the container's. + CONVEX_CLOUD_ORIGIN: http://127.0.0.1:3210 + CONVEX_SITE_ORIGIN: http://127.0.0.1:3211 + # The anonymous usage beacon (a random ID plus the backend version). + DISABLE_BEACON: "true" + DISABLE_METRICS_ENDPOINT: "true" + # Upstream's compose default: keep old document versions for 2 days. + DOCUMENT_RETENTION_DELAY: "172800" + RUST_LOG: info + healthcheck: + test: curl -f http://localhost:3210/version + interval: 5s + start_period: 10s + mem_limit: 512m + + dashboard: + image: ghcr.io/get-convex/convex-dashboard:0cf49cbf8c4b7e22e631ceda9b5111f6cccbcd49 + container_name: convex-dashboard + restart: unless-stopped + stop_grace_period: 10s + stop_signal: SIGINT + ports: + - "127.0.0.1:6791:6791" + environment: + # Read by the dashboard in the *browser*, so it is the laptop's tunnel + # end too. + NEXT_PUBLIC_DEPLOYMENT_URL: http://127.0.0.1:3210 + # Otherwise the editor panes load monaco from a CDN. + NEXT_PUBLIC_LOAD_MONACO_INTERNALLY: "true" + depends_on: + backend: + condition: service_healthy + mem_limit: 384m From 0cdf2acf3ddd06d65e9c0e6adc8ae9e42f392df9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 20:05:34 +0000 Subject: [PATCH 02/11] venari: store Convex in Ghost's Postgres cluster The first cut used SQLite, arguing that Convex's data should not share a lifecycle with a cluster built for disposable databases. Alyssa chose Postgres in Ghost's cluster instead: one Postgres on a 4 GB box, not two. This commit makes that safe and records what it cost. Isolation, from reading the fork (internal/server/store.go) and then testing it: - ghost-server only acts on databases in its own ghost.databases table. Sizes, forks, pauses, DROP and pg_terminate_backend all join on it, so `ghost list` never shows convex_self_hosted and nothing Ghost does can drop it. The storage limit is reported, not enforced. - Postgres grants CONNECT on new databases to PUBLIC. tsdbadmin is the shared role Ghost hands to agents, so CONNECT is revoked; tsdbadmin now gets "User does not have CONNECT privilege". Convex has its own role, so `ghost password` does not touch it. - Both projects join venari-postgres, an external network owned by neither. `docker compose down` in /srv/ghost then removes only ghost_default. TLS turned out to be the surprise. The backend verifies Postgres certificates, and Ghost's is self-signed (CaUsedAsEndEntity). DO_NOT_REQUIRE_SSL alone still attempts TLS when the server offers it; sslmode=disable alone is refused. It takes both, and the hop is plaintext (pg_stat_ssl.ssl = f). I accepted that: container to container on a private bridge, never off the host, and readable only by root, which can read .env anyway. Re-issuing Ghost's certificate from a CA the backend trusts is the alternative, and it is untested. A cost that remains: state is now in two places, the database in the cluster and files, modules and credentials in data/. Only `npx convex export` captures both at one moment. The Ghost section no longer calls the whole cluster disposable. Checked in a sandbox with ghost-server built from the fork and Ghost set up per its README: - Deploy, mutation, query and "use node" action all work. A subscription saw 271 pushed updates across 500 mutations. - ghost create/fork/delete/list were unaffected. - Outages: Ghost `down` and back up, and Convex never restarted. On a cold start with Convex first, it crash-looped 7 times, then recovered by itself. - Peak memory: backend 132 MiB, dashboard 88 MiB, ghost-postgres 213 MiB of its 768m. - The README's Convex setup block ran verbatim, starting from Ghost on its current compose (no shared network), and ended in "Connected to Postgres". Specific changes: - tools/venari/ghost/docker-compose.yml: postgres joins the external venari-postgres network - tools/venari/convex/docker-compose.yml: POSTGRES_URL to ghost-postgres as role convex, with the plaintext settings and the reasons for them - tools/venari/convex/.env.example and .gitignore: the role password - tools/venari/README.md: Convex section rewritten for the shared cluster (isolation, network, TLS, split state, measurements, setup including the role and REVOKE); the Ghost setup creates the network; Ghost's and the box's backup notes now name Convex's data Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- tools/venari/README.md | 103 ++++++++++++++++++------- tools/venari/convex/.env.example | 4 + tools/venari/convex/.gitignore | 1 + tools/venari/convex/docker-compose.yml | 50 +++++++++--- tools/venari/ghost/docker-compose.yml | 11 +++ 5 files changed, 129 insertions(+), 40 deletions(-) create mode 100644 tools/venari/convex/.env.example diff --git a/tools/venari/README.md b/tools/venari/README.md index d8ccb985..520add1b 100644 --- a/tools/venari/README.md +++ b/tools/venari/README.md @@ -22,7 +22,7 @@ the box is the running copy. | `ssh/soft-tunnel.authorized_keys` | `/var/lib/soft-tunnel/.ssh/authorized_keys` (root-owned, 644) | | `ghost/docker-compose.yml` | `/srv/ghost/docker-compose.yml`, with `.env` (from `.env.example`), `certs/`, `data/` and `server/` beside it | | `ghost/server/Dockerfile` | `/srv/ghost/server/Dockerfile`, next to the `ghost-server` binary it packages | -| `convex/docker-compose.yml` | `/srv/convex/docker-compose.yml`, with `data/` beside it (no `.env`) | +| `convex/docker-compose.yml` | `/srv/convex/docker-compose.yml`, with `.env` (from `.env.example`) and `data/` beside it; its database is in Ghost's cluster | The dev-box side is `tools/mise/venari/` — the same arrangement felixia has, for the same reason: no Nix here either. @@ -182,6 +182,7 @@ cd /srv/ghost && umask 077 printf 'POSTGRES_PASSWORD=%s\nGHOST_API_KEY=gt_%s\n' "$(openssl rand -hex 24)" "$(openssl rand -hex 20)" > .env openssl req -new -x509 -days 3650 -nodes -subj /CN=venari-ghost -keyout certs/server.key -out certs/server.crt chown 70:70 certs/server.*; chmod 600 certs/server.key; chmod 644 certs/server.crt # uid 70 = the image's postgres +docker network inspect venari-postgres >/dev/null 2>&1 || docker network create venari-postgres # shared with Convex docker compose up -d # after `just ghost-deploy` has delivered server/ghost-server curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8787/v0/health ``` @@ -192,7 +193,8 @@ Then encrypt the box's `GHOST_API_KEY` into `secrets/personal/ghost-api-key.age` Not supported, and answered with 501: billing, spaces beyond the one, members, invites, shares, API-key management (the key is configuration). `ghost logs` returns an empty page. Backups: the cluster rides the provider's box backups, -like Soft Serve's `data/`; Ghost databases are disposable by design. +like Soft Serve's `data/`. Ghost databases are disposable by design, but the +cluster also holds Convex's database, which is not (see "Convex"). ## Convex: a self-hosted app backend @@ -205,27 +207,57 @@ open-source backend and dashboard from It is not a replacement for Ghost, and Ghost is not one for it. Ghost hands out disposable Postgres databases, so it is the *test* database. Convex is where an -app's data lives. That difference drives the storage choice: - -- **SQLite in `data/`**, which is upstream's default and its recommended starting - point. Not Postgres in the Ghost cluster. Convex's data would then share a - lifecycle with databases that are disposable by design, and a separate - Postgres would cost RAM this box does not have spare. Moving later is - `npx convex export`, then set `POSTGRES_URL`, then `npx convex import` - (upstream `self-hosted/advanced/postgres_or_mysql.md`). -- **`data/` is the whole state**: the database, stored files and modules, and - `credentials/`. The backend generates the instance name and secret there on - first start, so there is no `.env`. - -Measured on 2026-09-22, in a container off the box, against the pinned -revision. A project with a schema, a mutation, a query, and a `"use node"` -action deployed with `npx convex deploy` (convex 1.46.0). A `ConvexClient` -subscription saw 252 pushed updates, from 1 row to 501, while 500 concurrent -mutations ran. Peak memory was 150 MiB for the backend and 270 MiB for the -dashboard (at startup), so the limits are 512m and 384m. With Soft Serve and -Ghost, the box's container limits then total about 2 GB of its 4 GB. The -images take about 1.5 GB of disk (798 MB + 668 MB). These are not load -figures for this box: that machine was not venari, and venari has 2 vCPUs. +app's data lives. They do share one thing: the cluster. + +- **Postgres in Ghost's cluster**, so the box runs one Postgres, not two. + Upstream's default is SQLite. Convex gets its own database + (`convex_self_hosted`), owned by its own role (`convex`), with `CONNECT` + revoked from `PUBLIC`. `tsdbadmin`, the shared role Ghost gives to agents, + cannot open it ("User does not have CONNECT privilege"), and + `ghost password` does not touch it. `ghost-server` only acts on databases + listed in its own `ghost.databases` table. Sizes, forks, pauses, deletes and + connection kills all join on that table. So `ghost list` never shows + Convex's database, and nothing Ghost does can drop it. The storage limit is + reported, not enforced (`internal/server/config.go` in the fork). +- **One network owned by neither project.** `ghost-postgres` and + `convex-backend` both join `venari-postgres`, an external network created + once by hand. `docker compose down` in `/srv/ghost` then removes only + `ghost_default`, and Convex keeps its route to the cluster. +- **Plaintext on that network.** The backend verifies Postgres TLS, and + Ghost's certificate is self-signed, so a handshake fails with + `CaUsedAsEndEntity`. The fix needs both `DO_NOT_REQUIRE_SSL=1` and + `?sslmode=disable`; either one alone still fails. The hop is container to + container and never leaves the host. The alternative, not tested, is to + re-issue Ghost's certificate from a CA the backend trusts. +- **State is split across two places.** The documents are in + `convex_self_hosted` in the cluster. Files, function modules, search indexes + (`data/storage/`) and `data/credentials/` (the instance name, and the secret + that admin keys are signed with) are on disk. A backup needs both from the + same moment, which is what `npx convex export` produces. Two separate copies + of the cluster and `data/` do not. + +Measured on 2026-09-22 in a sandbox, not on venari. The setup there followed +this README: Ghost with its self-signed certificate, and Convex on the same +cluster, both at the pinned images. + +- **A workload.** A project with a schema, a mutation, a query and a + `"use node"` action deployed with `npx convex deploy` (convex 1.46.0). A + `ConvexClient` subscription saw 271 pushed updates, from 1 row to 501, while + 500 concurrent mutations ran. +- **Ghost unaffected.** `ghost create`, `fork`, `delete` and `list` behaved + as before. +- **Outages.** With `/srv/ghost` brought `down` and back up, Convex never + restarted and kept its data. On a cold start with Convex first, it + crash-looped (7 restarts) until the cluster was healthy, then recovered by + itself. `restart: unless-stopped` is what gets it through a reboot, because + `depends_on` cannot reach across compose projects. +- **Peak memory.** Convex backend 132 MiB and dashboard 88 MiB, against limits + of 512m and 384m. `ghost-postgres` peaked at 213 MiB of its 768m while + serving both. Across all containers, the box's limits total about 2 GB of + its 4 GB. +- **Disk.** The images take about 1.5 GB (798 MB + 668 MB). + +None of these are load figures for this box, which has 2 vCPUs. All three ports listen on the box's loopback. From the laptop: @@ -262,13 +294,25 @@ Both outbound calls the images make by default are off. `DISABLE_BEACON` turns off the anonymous usage ping. `NEXT_PUBLIC_LOAD_MONACO_INTERNALLY` serves the dashboard's editor from the image instead of from a CDN. -Setup, as root on the box, after copying `convex/` here to `/srv/convex/`: +Setup, as root on the box, after copying `convex/` here to `/srv/convex/`. +Ghost must already be running; its `docker-compose.yml` in this repo joins +`venari-postgres`, so it needs a `docker compose up -d` once the network +exists. ```sh +docker network inspect venari-postgres >/dev/null 2>&1 || docker network create venari-postgres +(cd /srv/ghost && docker compose up -d --wait) # re-create postgres on the new network + cd /srv/convex && umask 077 && mkdir -p data +printf 'CONVEX_POSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 24)" > .env +. ./.env +(cd /srv/ghost && docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U postgres -v pw="$CONVEX_POSTGRES_PASSWORD") <<'SQL' +CREATE ROLE convex LOGIN PASSWORD :'pw'; +CREATE DATABASE convex_self_hosted OWNER convex; +REVOKE CONNECT, TEMPORARY ON DATABASE convex_self_hosted FROM PUBLIC; +SQL docker compose up -d --wait -curl -s http://127.0.0.1:3210/version; echo -docker compose logs backend | grep -m1 'Connected to SQLite' +docker compose logs backend | grep -m1 'Connected to Postgres' ``` Upgrading, from upstream `self-hosted/advanced/upgrading.md`: @@ -300,6 +344,7 @@ original: a `*.bak-` left in `/etc/apt/apt.conf.d/` makes apt print planned home for that is restic to R2. `ledger-age` is itself a backup (ciphertext, with the plaintext still on felixia), so losing it is recoverable. The Moment repos are not. - Convex's `data/` needs the same treatment. It is the one directory on this - box that holds application data nothing else has a copy of. Until restic - lands, `npx convex export` from the laptop is the only backup. + Ghost's cluster is no longer all disposable: `convex_self_hosted` in it, + plus Convex's `data/`, is application data nothing else has a copy of. + Until restic lands, `npx convex export` from the laptop is the only + backup, and it is the only one that captures both halves at one moment. diff --git a/tools/venari/convex/.env.example b/tools/venari/convex/.env.example new file mode 100644 index 00000000..fbae1ea3 --- /dev/null +++ b/tools/venari/convex/.env.example @@ -0,0 +1,4 @@ +# Copy to .env on the box (mode 600). Generated there: +# CONVEX_POSTGRES_PASSWORD=$(openssl rand -hex 24) +# The same value goes into the role in Ghost's cluster (README, "Convex"). +CONVEX_POSTGRES_PASSWORD= diff --git a/tools/venari/convex/.gitignore b/tools/venari/convex/.gitignore index 8fce6030..24f9f8d0 100644 --- a/tools/venari/convex/.gitignore +++ b/tools/venari/convex/.gitignore @@ -1 +1,2 @@ +.env data/ diff --git a/tools/venari/convex/docker-compose.yml b/tools/venari/convex/docker-compose.yml index affe44e6..28ab4463 100644 --- a/tools/venari/convex/docker-compose.yml +++ b/tools/venari/convex/docker-compose.yml @@ -3,18 +3,26 @@ # on the box's loopback only and are reached over ssh -L, like Soft Serve and # Ghost: the cloud firewall admits 22 alone. # -# On the box: /srv/convex/docker-compose.yml, next to data/ (never tracked). -# data/ is the whole state: the SQLite database, stored files and modules, and -# credentials/ - the instance name and secret the backend generates on first -# start. Admin keys are signed with that secret: `generate_admin_key.sh` gives a -# different key on every call, and every one stays valid as long as data/ does -# (checked 2026-09-22: a key issued before a restart still worked after it). -# There is no .env: nothing here is a secret that data/ does not already hold. +# On the box: /srv/convex/docker-compose.yml, next to an untracked .env (see +# .env.example) and data/ (never tracked). # -# Storage is SQLite, upstream's default and its recommended starting point. -# Not Postgres in the Ghost cluster: those databases are disposable by design, -# and Convex's are not. Moving later is `npx convex export`, set POSTGRES_URL, -# `npx convex import` (upstream: self-hosted/advanced/postgres_or_mysql.md). +# The database is Postgres in Ghost's cluster (ghost-postgres), not upstream's +# default SQLite: one Postgres on the box, not two. Convex gets a database of +# its own (convex_self_hosted), owned by a role of its own (convex), with +# CONNECT revoked from PUBLIC so tsdbadmin - the shared role Ghost hands to +# agents - cannot even open it. ghost-server only ever touches databases listed +# in its ghost.databases table, so it neither sees nor sizes nor drops this one +# (checked against the fork's internal/server/store.go). The two projects meet +# on the venari-postgres network, which neither owns. +# +# State is split in two, and a backup needs both, taken together: +# - convex_self_hosted in the cluster: every document and index. +# - data/: stored files, pushed function modules, search indexes (storage/), +# and credentials/ - the instance name and the secret admin keys are signed +# with. The backend generates both on first start. +# Admin keys: `generate_admin_key.sh` gives a different key on every call, and +# every one stays valid as long as credentials/ does (checked 2026-09-22: a key +# issued before a restart still worked after it). # # Upstream ships `:latest`. Both images are pinned to one revision here, because # upstream says a backend and dashboard of different versions are not @@ -32,6 +40,9 @@ services: stop_signal: SIGINT volumes: - ./data:/convex/data + networks: + - default + - venari-postgres ports: - "127.0.0.1:3210:3210" # client API, websocket sync - "127.0.0.1:3211:3211" # HTTP actions @@ -42,6 +53,19 @@ services: # client actually uses, not the container's. CONVEX_CLOUD_ORIGIN: http://127.0.0.1:3210 CONVEX_SITE_ORIGIN: http://127.0.0.1:3211 + # No database name: the backend appends one derived from INSTANCE_NAME + # (default convex-self-hosted -> convex_self_hosted). + # + # Plaintext, on purpose, and both settings are needed (checked + # 2026-09-22). The backend verifies TLS certificates, and Ghost's is + # self-signed, so a TLS handshake fails with CaUsedAsEndEntity. + # DO_NOT_REQUIRE_SSL alone still attempts TLS whenever the server + # offers it, and fails the same way. `sslmode=disable` alone is refused + # without DO_NOT_REQUIRE_SSL. The hop is container to container on the + # venari-postgres bridge and never leaves the host. Anyone able to read + # that bridge is root, and root can read .env. + POSTGRES_URL: "postgresql://convex:${CONVEX_POSTGRES_PASSWORD:?set it in .env}@ghost-postgres:5432?sslmode=disable" + DO_NOT_REQUIRE_SSL: "1" # The anonymous usage beacon (a random ID plus the backend version). DISABLE_BEACON: "true" DISABLE_METRICS_ENDPOINT: "true" @@ -72,3 +96,7 @@ services: backend: condition: service_healthy mem_limit: 384m + +networks: + venari-postgres: + external: true diff --git a/tools/venari/ghost/docker-compose.yml b/tools/venari/ghost/docker-compose.yml index 1284f260..446a2260 100644 --- a/tools/venari/ghost/docker-compose.yml +++ b/tools/venari/ghost/docker-compose.yml @@ -32,6 +32,13 @@ services: - ./certs:/certs:ro ports: - "127.0.0.1:5432:5432" + # Also on venari-postgres, the network Convex reaches this cluster through + # (tools/venari/convex/docker-compose.yml). It is created once by hand and + # owned by neither project, so `docker compose down` here cannot take it + # away from Convex, and the reverse. + networks: + - default + - venari-postgres shm_size: 128m mem_limit: 768m healthcheck: @@ -62,3 +69,7 @@ services: ports: - "127.0.0.1:8787:8787" mem_limit: 128m + +networks: + venari-postgres: + external: true From 1abb0cc7e4e12c01cd8f832564b2279036182a9d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 08:02:20 +0000 Subject: [PATCH 03/11] venari: Ghost owns venari-postgres; no hand-made network Ghost's compose file declared venari-postgres as external, so Ghost depended on a network that exists only for Convex. Copy the updated twin to /srv/ghost on a box where nobody ran `docker network create`, and `just ghost-deploy` (`docker compose up -d server`) fails with "network venari-postgres declared as external, but could not be found", even with no Convex anywhere. Ghost now creates the network itself, with a fixed `name:` so Convex can refer to it. Convex keeps it external. That puts the dependency in the right direction: Ghost runs alone, and Convex needs Ghost up first, which it needs for its database anyway. What I was worried about, ownership tearing the network out from under Convex, does not happen (Docker Compose 2, 2026-10-16). `docker compose down` in the owning project while another project is attached prints "Network venari-postgres Resource is still in use", exits 0, and keeps the network; the attached container keeps running and still resolves the service by name after the owner comes back up. Only when both are down is the network removed. Checked with the real files: - (a) A fresh box, no network, ghost-deploy's `up -d server`: exit 0, network created. - (b) Ghost on main's compose file, then the README's Convex setup block run verbatim: Ghost and Convex both healthy, and "Connected to Postgres". Both manual `docker network create` steps are gone from the README. Found by /code-review (finding 7). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- tools/venari/README.md | 21 +++++++++++---------- tools/venari/convex/docker-compose.yml | 3 ++- tools/venari/ghost/docker-compose.yml | 9 +++++---- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/tools/venari/README.md b/tools/venari/README.md index 520add1b..0ec72fa1 100644 --- a/tools/venari/README.md +++ b/tools/venari/README.md @@ -182,7 +182,6 @@ cd /srv/ghost && umask 077 printf 'POSTGRES_PASSWORD=%s\nGHOST_API_KEY=gt_%s\n' "$(openssl rand -hex 24)" "$(openssl rand -hex 20)" > .env openssl req -new -x509 -days 3650 -nodes -subj /CN=venari-ghost -keyout certs/server.key -out certs/server.crt chown 70:70 certs/server.*; chmod 600 certs/server.key; chmod 644 certs/server.crt # uid 70 = the image's postgres -docker network inspect venari-postgres >/dev/null 2>&1 || docker network create venari-postgres # shared with Convex docker compose up -d # after `just ghost-deploy` has delivered server/ghost-server curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8787/v0/health ``` @@ -219,10 +218,13 @@ app's data lives. They do share one thing: the cluster. connection kills all join on that table. So `ghost list` never shows Convex's database, and nothing Ghost does can drop it. The storage limit is reported, not enforced (`internal/server/config.go` in the fork). -- **One network owned by neither project.** `ghost-postgres` and - `convex-backend` both join `venari-postgres`, an external network created - once by hand. `docker compose down` in `/srv/ghost` then removes only - `ghost_default`, and Convex keeps its route to the cluster. +- **One network, owned by Ghost.** `ghost-postgres` and `convex-backend` both + join `venari-postgres`. Ghost's compose file creates it; Convex's declares + it external. So Ghost runs with or without Convex, and Convex needs Ghost + up first, which it needs for the database anyway. `docker compose down` in + `/srv/ghost` while Convex is attached keeps the network ("Resource is still + in use", exit 0); Convex keeps its route, and the next `up` reattaches + Postgres (checked with Docker Compose 2 on 2026-10-16). - **Plaintext on that network.** The backend verifies Postgres TLS, and Ghost's certificate is self-signed, so a handshake fails with `CaUsedAsEndEntity`. The fix needs both `DO_NOT_REQUIRE_SSL=1` and @@ -295,13 +297,12 @@ turns off the anonymous usage ping. `NEXT_PUBLIC_LOAD_MONACO_INTERNALLY` serves the dashboard's editor from the image instead of from a CDN. Setup, as root on the box, after copying `convex/` here to `/srv/convex/`. -Ghost must already be running; its `docker-compose.yml` in this repo joins -`venari-postgres`, so it needs a `docker compose up -d` once the network -exists. +Ghost must be running from this repo's `ghost/docker-compose.yml`, which +creates `venari-postgres`. A box still on the older Ghost file needs one +`docker compose up` there first; it re-creates Postgres on the new network. ```sh -docker network inspect venari-postgres >/dev/null 2>&1 || docker network create venari-postgres -(cd /srv/ghost && docker compose up -d --wait) # re-create postgres on the new network +(cd /srv/ghost && docker compose up -d --wait) # creates venari-postgres if missing cd /srv/convex && umask 077 && mkdir -p data printf 'CONVEX_POSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 24)" > .env diff --git a/tools/venari/convex/docker-compose.yml b/tools/venari/convex/docker-compose.yml index 28ab4463..af783a4e 100644 --- a/tools/venari/convex/docker-compose.yml +++ b/tools/venari/convex/docker-compose.yml @@ -13,7 +13,8 @@ # agents - cannot even open it. ghost-server only ever touches databases listed # in its ghost.databases table, so it neither sees nor sizes nor drops this one # (checked against the fork's internal/server/store.go). The two projects meet -# on the venari-postgres network, which neither owns. +# on the venari-postgres network, which Ghost's project creates; Convex only +# joins it, so Ghost must be up first. # # State is split in two, and a backup needs both, taken together: # - convex_self_hosted in the cluster: every document and index. diff --git a/tools/venari/ghost/docker-compose.yml b/tools/venari/ghost/docker-compose.yml index 446a2260..3bdb006f 100644 --- a/tools/venari/ghost/docker-compose.yml +++ b/tools/venari/ghost/docker-compose.yml @@ -33,9 +33,10 @@ services: ports: - "127.0.0.1:5432:5432" # Also on venari-postgres, the network Convex reaches this cluster through - # (tools/venari/convex/docker-compose.yml). It is created once by hand and - # owned by neither project, so `docker compose down` here cannot take it - # away from Convex, and the reverse. + # (tools/venari/convex/docker-compose.yml). This project creates it, so + # Ghost needs no setup step for Convex's sake. `docker compose down` here + # while Convex is attached leaves it in place ("Resource is still in use", + # exit 0) and Convex keeps running; the next `up` reattaches postgres. networks: - default - venari-postgres @@ -72,4 +73,4 @@ services: networks: venari-postgres: - external: true + name: venari-postgres # fixed, so Convex can name it; no project prefix From 490449b0ada7eee5fcb80faf00a8eedb9de343dd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 08:04:34 +0000 Subject: [PATCH 04/11] venari: give Convex a fixed share of Ghost's connections Convex and every agent's Ghost databases share one cluster, and nothing divided its connections. The reviewer assumed Postgres' default of 100; the real limit is 25 (TimescaleDB tunes max_connections from the container's 768m), with 3 reserved for superusers. The Convex backend's pool defaults to 128 (POSTGRES_MAX_CONNECTIONS in crates/common/src/knobs.rs at the pinned 0cf49cb). Either side could exhaust the 22 shared slots ("sorry, too many clients already"), failing agents' connections or Convex's mutations. Two caps, one in each process: - POSTGRES_MAX_CONNECTIONS=8 in Convex's compose. The backend logs "Postgres connection pool max size 8". - CONNECTION LIMIT 10 on the convex role, in the README's setup SQL. Postgres enforces it whatever the knob says, with 2 slots of headroom for reconnects. Tested: with 10 convex sessions open, the next login gets FATAL: too many connections for role "convex". That leaves about 12 slots for agents. ghost-server itself connects as the superuser, so it can fall back on the reserved slots. Is 8 enough? Under the 500-mutation burst plus a live subscription, Convex peaked at 7 connections whether the pool was 128 or 8. The burst took 5.5 s at 128 and 6.1 s at 8 in this sandbox, which is within its noise. These are not load figures for venari. Found by /code-review (finding 4). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- tools/venari/README.md | 8 +++++++- tools/venari/convex/docker-compose.yml | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tools/venari/README.md b/tools/venari/README.md index 0ec72fa1..fc003389 100644 --- a/tools/venari/README.md +++ b/tools/venari/README.md @@ -231,6 +231,12 @@ app's data lives. They do share one thing: the cluster. `?sslmode=disable`; either one alone still fails. The hop is container to container and never leaves the host. The alternative, not tested, is to re-issue Ghost's certificate from a CA the backend trusts. +- **A connection budget.** The cluster allows 25 connections, 3 reserved for + superusers (TimescaleDB derives the limit from its memory). Convex's pool is + capped at 8 (`POSTGRES_MAX_CONNECTIONS`; its default of 128 could take every + slot), and the `convex` role has `CONNECTION LIMIT 10`, so Postgres enforces + the cap whatever the knob says. That leaves about 12 for agents' Ghost + databases. Under a 500-mutation burst Convex used 7. - **State is split across two places.** The documents are in `convex_self_hosted` in the cluster. Files, function modules, search indexes (`data/storage/`) and `data/credentials/` (the instance name, and the secret @@ -308,7 +314,7 @@ cd /srv/convex && umask 077 && mkdir -p data printf 'CONVEX_POSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 24)" > .env . ./.env (cd /srv/ghost && docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U postgres -v pw="$CONVEX_POSTGRES_PASSWORD") <<'SQL' -CREATE ROLE convex LOGIN PASSWORD :'pw'; +CREATE ROLE convex LOGIN CONNECTION LIMIT 10 PASSWORD :'pw'; CREATE DATABASE convex_self_hosted OWNER convex; REVOKE CONNECT, TEMPORARY ON DATABASE convex_self_hosted FROM PUBLIC; SQL diff --git a/tools/venari/convex/docker-compose.yml b/tools/venari/convex/docker-compose.yml index af783a4e..20cc1b19 100644 --- a/tools/venari/convex/docker-compose.yml +++ b/tools/venari/convex/docker-compose.yml @@ -67,6 +67,14 @@ services: # that bridge is root, and root can read .env. POSTGRES_URL: "postgresql://convex:${CONVEX_POSTGRES_PASSWORD:?set it in .env}@ghost-postgres:5432?sslmode=disable" DO_NOT_REQUIRE_SSL: "1" + # Connection budget. Ghost's cluster allows 25 connections in total + # (TimescaleDB tunes max_connections from its 768m limit), 3 of them + # reserved for superusers, and every agent's Ghost database draws on + # the same 22. The backend's pool defaults to 128 - enough to starve + # them, or be starved. 8 is above the 7 it used under a 500-mutation + # burst (2026-10-16); the convex role's CONNECTION LIMIT 10 (README + # setup) enforces the cap server-side even if this knob changed. + POSTGRES_MAX_CONNECTIONS: "8" # The anonymous usage beacon (a random ID plus the backend version). DISABLE_BEACON: "true" DISABLE_METRICS_ENDPOINT: "true" From db85018cdb7a048f3df0eb8b86b43f7acb9b7e99 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 08:06:49 +0000 Subject: [PATCH 05/11] venari: move the Convex tunnel's laptop ends to 13210/13211/16791 The tunnel bound laptop ports 3210/3211, the same ports a local `npx convex dev` deployment starts at (convex 1.46.0, localDeployment/utils.js: startPort 3210). Two failures follow: - Local backend running first: the tunnel dies on a bind error. Reproduced against a stand-in sshd ("cannot listen to port", exit 255). - Tunnel open first: the CLI quietly moves the local backend to the next free port. A project whose .env.local says 127.0.0.1:3210 then deploys to venari instead of the local backend, with no error. Because CONVEX_CLOUD_ORIGIN hardcoded 127.0.0.1:3210, the tunnel could not simply be moved. The laptop ends are now the box ports plus 10000 (13210 -> 3210, 13211 -> 3211, 16791 -> 6791), the convention Ghost already uses (15432 -> 5432). Changed together, because they must agree: - CONVEX_CLOUD_ORIGIN, CONVEX_SITE_ORIGIN and NEXT_PUBLIC_DEPLOYMENT_URL in compose - the forwards in personal.just - the README's tunnel line, dashboard URL and .env.local example The box side is unchanged: 3210/3211/6791 on loopback. Checked through a real ssh -L tunnel to a stand-in sshd, with the backend on 3210 and the laptop ends on 13210/13211/16791: - `npx convex deploy` over 13210 succeeds. - An HTTP action answers on 13211. - ctx.storage.getUrl returns http://127.0.0.1:13210/api/storage/..., and that URL serves the file through the tunnel, so the advertised origin is the one clients use. - A live subscription pushed updates across 500 mutations, and the dashboard loads on 16791. Found by /code-review (finding 2). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- tools/just/personal.just | 10 ++++++---- tools/venari/README.md | 15 ++++++++++----- tools/venari/convex/docker-compose.yml | 17 ++++++++++------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/tools/just/personal.just b/tools/just/personal.just index 91cb4edd..21e9ea93 100644 --- a/tools/just/personal.just +++ b/tools/just/personal.just @@ -39,18 +39,20 @@ ghost-tunnel-close: # Convex on venari (tools/venari/README.md, "Convex"): backend, HTTP actions and # dashboard listen on the box's loopback. The forwards are inline rather than an # ssh alias like venari-ghost, so nothing outside this repo has to exist first; -# the laptop ports match the box's because CONVEX_CLOUD_ORIGIN says 127.0.0.1:3210. +# laptop ports are the box's plus 10000 (13210 -> 3210, like Ghost's 15432 -> 5432), +# because 3210/3211 are where a local `npx convex dev` deployment starts, and they +# must match CONVEX_CLOUD_ORIGIN / CONVEX_SITE_ORIGIN in convex/docker-compose.yml. # Safe to re-run: ExitOnForwardFailure kills a second tunnel that cannot bind. -# open the Convex tunnel: 3210 -> API, 3211 -> HTTP actions, 6791 -> dashboard +# open the Convex tunnel: 13210 -> API, 13211 -> HTTP actions, 16791 -> dashboard [group('personal')] convex-tunnel: - @pgrep -qf 'L 3210:127.0.0.1:3210' && echo "convex tunnel already open" || ssh -fN -o ExitOnForwardFailure=yes -L 3210:127.0.0.1:3210 -L 3211:127.0.0.1:3211 -L 6791:127.0.0.1:6791 venari + @pgrep -qf 'L 13210:127.0.0.1:3210' && echo "convex tunnel already open" || ssh -fN -o ExitOnForwardFailure=yes -L 13210:127.0.0.1:3210 -L 13211:127.0.0.1:3211 -L 16791:127.0.0.1:6791 venari # close the Convex tunnel [group('personal')] convex-tunnel-close: - @pkill -f 'L 3210:127.0.0.1:3210' && echo "convex tunnel closed" || echo "no convex tunnel open" + @pkill -f 'L 13210:127.0.0.1:3210' && echo "convex tunnel closed" || echo "no convex tunnel open" # print a new Convex admin key (each call differs; all stay valid until data/ is lost) [group('personal')] diff --git a/tools/venari/README.md b/tools/venari/README.md index fc003389..bab6b0d0 100644 --- a/tools/venari/README.md +++ b/tools/venari/README.md @@ -267,12 +267,17 @@ cluster, both at the pinned images. None of these are load figures for this box, which has 2 vCPUs. -All three ports listen on the box's loopback. From the laptop: +All three ports listen on the box's loopback (3210 API, 3211 HTTP actions, +6791 dashboard). The tunnel's laptop ends are the same numbers plus 10000, +like Ghost's 15432, because 3210/3211 are where a local `npx convex dev` +deployment starts. With equal numbers, a project whose `.env.local` says +`127.0.0.1:3210` would deploy to whichever backend held the port. From the +laptop: ```sh -just convex-tunnel # 3210 -> API, 3211 -> HTTP actions, 6791 -> dashboard +just convex-tunnel # 13210 -> API, 13211 -> HTTP actions, 16791 -> dashboard just convex-admin-key # prints a new admin key (via venari-root) -open http://127.0.0.1:6791 # dashboard; paste the admin key +open http://127.0.0.1:16791 # dashboard; paste the admin key just convex-tunnel-close ``` @@ -281,7 +286,7 @@ by the project and never in this repo. `npx convex dev` and `deploy` then target venari: ```sh -CONVEX_SELF_HOSTED_URL='http://127.0.0.1:3210' +CONVEX_SELF_HOSTED_URL='http://127.0.0.1:13210' CONVEX_SELF_HOSTED_ADMIN_KEY='' ``` @@ -292,7 +297,7 @@ to keep in agenix. Revoking a key means rotating the instance secret, which invalidates every key. `CONVEX_CLOUD_ORIGIN` and `NEXT_PUBLIC_DEPLOYMENT_URL` say -`http://127.0.0.1:3210`, which is the laptop's end of the tunnel. That means +`http://127.0.0.1:13210`, which is the laptop's end of the tunnel. That means this deployment serves one person through ssh. A frontend that other people use needs a public HTTPS origin for the API. That means a port other than 22 in the cloud firewall and a TLS reverse proxy. It is a separate decision, diff --git a/tools/venari/convex/docker-compose.yml b/tools/venari/convex/docker-compose.yml index 20cc1b19..18d6b965 100644 --- a/tools/venari/convex/docker-compose.yml +++ b/tools/venari/convex/docker-compose.yml @@ -48,12 +48,15 @@ services: - "127.0.0.1:3210:3210" # client API, websocket sync - "127.0.0.1:3211:3211" # HTTP actions environment: - # What clients are told the backend is: the laptop's end of the tunnel - # (`just convex-tunnel` forwards the same port numbers). These appear in - # storage URLs and action callbacks, so they must be the address the - # client actually uses, not the container's. - CONVEX_CLOUD_ORIGIN: http://127.0.0.1:3210 - CONVEX_SITE_ORIGIN: http://127.0.0.1:3211 + # What clients are told the backend is: the laptop's end of the tunnel. + # These appear in storage URLs and action callbacks, so they must be the + # address the client actually uses, not the container's. The laptop + # ports are offset by 10000 (`just convex-tunnel`: 13210 -> 3210), like + # Ghost's 15432 -> 5432, because 3210/3211 are where a local + # `npx convex dev` deployment starts; sharing them would let a project + # deploy to whichever backend held the port. + CONVEX_CLOUD_ORIGIN: http://127.0.0.1:13210 + CONVEX_SITE_ORIGIN: http://127.0.0.1:13211 # No database name: the backend appends one derived from INSTANCE_NAME # (default convex-self-hosted -> convex_self_hosted). # @@ -98,7 +101,7 @@ services: environment: # Read by the dashboard in the *browser*, so it is the laptop's tunnel # end too. - NEXT_PUBLIC_DEPLOYMENT_URL: http://127.0.0.1:3210 + NEXT_PUBLIC_DEPLOYMENT_URL: http://127.0.0.1:13210 # Otherwise the editor panes load monaco from a CDN. NEXT_PUBLIC_LOAD_MONACO_INTERNALLY: "true" depends_on: From 29909e7ba20cb2426365e35a52236d853ac511a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 08:07:42 +0000 Subject: [PATCH 06/11] just: track the Convex tunnel with an ssh control socket personal.just reaches every profile, including the Linux devcontainer (common.nix -> global.justfile), and the tunnel recipes broke there: - `pgrep -qf`: procps-ng pgrep has no -q ("invalid option -- 'q'", exit 2, checked on 4.0.4), so "already open" was never detected and a second tunnel was always attempted. - `pkill -f ''`: the pattern is also in the argv of the recipe's own `sh -c`, so close killed its own shell. Reproduced: exit 144, and neither message printed. The recipes no longer look for processes at all. convex-tunnel starts ssh as a control master on ~/.ssh/venari-convex.sock; `ssh -O check` answers "open?" and `ssh -O exit` closes it. That works the same on macOS and Linux. Each recipe is now a bash script with `set -euo pipefail`, the convention justfile already uses. Checked against a stand-in sshd, on Linux: - close with nothing open -> "no convex tunnel open", exit 0 - open -> open, exit 0; curl through 13210 -> 200 - open again -> "already open", exit 0 - close -> "closed"; close again -> "no tunnel", exit 0 - master killed with -9, socket left behind -> open recovers (the stale socket is removed first) and the tunnel works - 13210 taken by another listener -> exit 255 with ssh's bind error, not a false "open" Not checked on macOS here; nothing in it is platform-specific beyond OpenSSH's control-socket options. ghost-tunnel on main uses the same pgrep -q / pkill -f pattern and has the same bugs. That recipe is outside this PR, so it is left alone. Found by /code-review (finding 3). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- tools/just/personal.just | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/tools/just/personal.just b/tools/just/personal.just index 21e9ea93..5cd237b1 100644 --- a/tools/just/personal.just +++ b/tools/just/personal.just @@ -42,17 +42,37 @@ ghost-tunnel-close: # laptop ports are the box's plus 10000 (13210 -> 3210, like Ghost's 15432 -> 5432), # because 3210/3211 are where a local `npx convex dev` deployment starts, and they # must match CONVEX_CLOUD_ORIGIN / CONVEX_SITE_ORIGIN in convex/docker-compose.yml. -# Safe to re-run: ExitOnForwardFailure kills a second tunnel that cannot bind. +# The tunnel is an ssh control master on a socket, so `ssh -O check` / `-O exit` +# find and close it on macOS and Linux alike - no pgrep/pkill, whose flags differ +# (procps pgrep has no -q) and whose -f pattern also matches the calling shell. # open the Convex tunnel: 13210 -> API, 13211 -> HTTP actions, 16791 -> dashboard [group('personal')] convex-tunnel: - @pgrep -qf 'L 13210:127.0.0.1:3210' && echo "convex tunnel already open" || ssh -fN -o ExitOnForwardFailure=yes -L 13210:127.0.0.1:3210 -L 13211:127.0.0.1:3211 -L 16791:127.0.0.1:6791 venari + #!/usr/bin/env bash + set -euo pipefail + sock="$HOME/.ssh/venari-convex.sock" + if ssh -o ControlPath="$sock" -O check venari 2>/dev/null; then + echo "convex tunnel already open" + exit 0 + fi + rm -f "$sock" # a master that died uncleanly leaves its socket behind + ssh -fN -o ControlMaster=yes -o ControlPath="$sock" -o ExitOnForwardFailure=yes \ + -L 13210:127.0.0.1:3210 -L 13211:127.0.0.1:3211 -L 16791:127.0.0.1:6791 venari + echo "convex tunnel open: API 13210, HTTP actions 13211, dashboard 16791" # close the Convex tunnel [group('personal')] convex-tunnel-close: - @pkill -f 'L 13210:127.0.0.1:3210' && echo "convex tunnel closed" || echo "no convex tunnel open" + #!/usr/bin/env bash + set -euo pipefail + sock="$HOME/.ssh/venari-convex.sock" + if ssh -o ControlPath="$sock" -O exit venari 2>/dev/null; then + echo "convex tunnel closed" + else + rm -f "$sock" + echo "no convex tunnel open" + fi # print a new Convex admin key (each call differs; all stay valid until data/ is lost) [group('personal')] From b9c616a65b8a70c5dd5900b9f23b28178ce908cc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 08:08:37 +0000 Subject: [PATCH 07/11] just: make convex-admin-key fail when it gets no key `ssh ... | tail -1` without pipefail exits with tail's status, so the recipe always exited 0. Reproduced against a stand-in sshd: with the backend stopped, and with ssh unreachable, the old recipe printed an empty line and exited 0. That empty line, or an error message's last line, is what would be pasted into .env.local as CONVEX_SELF_HOSTED_ADMIN_KEY. The recipe is now a bash script with `set -euo pipefail`, and it also refuses any output not shaped like a key (|; the "Admin key:" banner goes to stderr, so stdout is only the key). Checked: - working: prints the key, exit 0 - backend stopped: "service "backend" is not running", exit 1 - ssh unreachable: "Connection refused", exit 255 - the shape check alone accepts a real key and rejects an error line, an empty string and a bare "|" The two failure runs used a copy of the recipe with /srv/convex pointed at the test deployment. Found by /code-review (finding 5). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- tools/just/personal.just | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/just/personal.just b/tools/just/personal.just index 5cd237b1..1f43ff12 100644 --- a/tools/just/personal.just +++ b/tools/just/personal.just @@ -77,7 +77,17 @@ convex-tunnel-close: # print a new Convex admin key (each call differs; all stay valid until data/ is lost) [group('personal')] convex-admin-key: - @ssh venari-root 'cd /srv/convex && docker compose exec -T backend ./generate_admin_key.sh' | tail -1 + #!/usr/bin/env bash + set -euo pipefail + # Without pipefail a failed ssh or exec still exits 0 through `tail`, and the + # empty line or error text gets pasted into .env.local as the key. So the + # script also refuses anything not shaped like a key: |. + key=$(ssh venari-root 'cd /srv/convex && docker compose exec -T backend ./generate_admin_key.sh' | tail -n 1) + if [[ ! "$key" =~ ^[a-z0-9_-]+\|[0-9a-f]+$ ]]; then + echo "convex-admin-key: got no admin key from venari (output: '${key}')" >&2 + exit 1 + fi + printf '%s\n' "$key" # The box has two vCPUs and no Go, so the server binary is cross-compiled here # and only the static binary travels; the image on the box is Alpine plus that From 3337a4a8db3f1325c54d84685623770183776fce Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 08:08:55 +0000 Subject: [PATCH 08/11] venari README: one account of Convex backups Three passages disagreed. The Ghost section says the cluster rides the provider's box backups. The Convex section said two separate copies of the cluster and data/ cannot be consistent. The backups gap said `npx convex export` is "the only backup". A whole-disk provider snapshot is neither separate copies nor nothing: it takes both halves at one moment. So a reader could skip box backups for Convex, or trust them without knowing what they do and do not give. The account now, the same in both Convex passages: - A whole-disk snapshot captures the cluster and data/ together, and is crash-consistent like a power cut, if the provider snapshots the disk atomically. Nobody has checked that, and the text says so. - `npx convex export` is consistent by construction and is the only copy that leaves the box. - Copying the halves separately (pg_dump, then data/) is what is not consistent. The planned restic setup must export, or copy with the backend stopped. Found by /code-review (finding 6). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- tools/venari/README.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tools/venari/README.md b/tools/venari/README.md index bab6b0d0..18029cfa 100644 --- a/tools/venari/README.md +++ b/tools/venari/README.md @@ -241,8 +241,12 @@ app's data lives. They do share one thing: the cluster. `convex_self_hosted` in the cluster. Files, function modules, search indexes (`data/storage/`) and `data/credentials/` (the instance name, and the secret that admin keys are signed with) are on disk. A backup needs both from the - same moment, which is what `npx convex export` produces. Two separate copies - of the cluster and `data/` do not. + same moment. Two kinds of copy give that: a whole-disk snapshot of the box, + which takes both at once (crash-consistent, like a power cut, *if* the + provider snapshots the disk atomically, which nobody has checked), and + `npx convex export`, which is consistent by construction and is the only + copy that leaves the box. Copying the two halves separately (a `pg_dump` + of the database, then `data/`) does not. Measured on 2026-09-22 in a sandbox, not on venari. The setup there followed this README: Ghost with its self-signed certificate, and Convex on the same @@ -357,6 +361,10 @@ original: a `*.bak-` left in `/etc/apt/apt.conf.d/` makes apt print (ciphertext, with the plaintext still on felixia), so losing it is recoverable. The Moment repos are not. Ghost's cluster is no longer all disposable: `convex_self_hosted` in it, - plus Convex's `data/`, is application data nothing else has a copy of. - Until restic lands, `npx convex export` from the laptop is the only - backup, and it is the only one that captures both halves at one moment. + plus Convex's `data/`, is application data. On the box it rides the + provider's backups like everything else (see "Ghost"), which is one + copy, on the same provider, whose atomicity is unchecked. Until restic + lands, `npx convex export` from the laptop is the only off-box backup and + the only one consistent by construction. Restic for Convex must not + repeat the separate-copies mistake: export, or dump and copy with the + backend stopped. From 1e804a5605227cc2dde325677d8946b80400b954 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 08:09:38 +0000 Subject: [PATCH 09/11] venari: one home for Convex's reasoning; fix the recipe comment Two things the review grouped together: - personal.just said the forwards are inline "so nothing outside this repo has to exist first". That was false: the recipes ssh to the `venari` and `venari-root` aliases, which live only in ~/.ssh/config. On a machine without them they fail with "Could not resolve hostname venari". The comment now names both aliases and points to the README's "Access" section, which describes them. The true part stays: unlike Ghost, Convex needs no third alias. - The design reasoning was written out twice, in the compose header and in the README, and the two would drift on the next edit to either. The README's "Convex" section is now its only home; it already held every point. The compose file keeps where it lives and what it depends on, plus one line per setting saying what must stay true when editing (origins match the tunnel, plaintext needs both settings, pool below the role limit). 104 -> 75 lines. Comments only. `docker compose config` resolves to the same output before and after, and personal.just is unchanged apart from comment lines. Found by /code-review (finding 10). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- tools/just/personal.just | 10 ++-- tools/venari/convex/docker-compose.yml | 69 ++++++-------------------- 2 files changed, 20 insertions(+), 59 deletions(-) diff --git a/tools/just/personal.just b/tools/just/personal.just index 1f43ff12..ba808268 100644 --- a/tools/just/personal.just +++ b/tools/just/personal.just @@ -37,11 +37,11 @@ ghost-tunnel-close: @pkill -f 'ssh -fN venari-ghost' && echo "ghost tunnel closed" || echo "no ghost tunnel open" # Convex on venari (tools/venari/README.md, "Convex"): backend, HTTP actions and -# dashboard listen on the box's loopback. The forwards are inline rather than an -# ssh alias like venari-ghost, so nothing outside this repo has to exist first; -# laptop ports are the box's plus 10000 (13210 -> 3210, like Ghost's 15432 -> 5432), -# because 3210/3211 are where a local `npx convex dev` deployment starts, and they -# must match CONVEX_CLOUD_ORIGIN / CONVEX_SITE_ORIGIN in convex/docker-compose.yml. +# dashboard listen on the box's loopback. These recipes use the `venari` and +# `venari-root` aliases from ~/.ssh/config (README, "Access"; the box's address +# is never in this repo). The forwards are inline, so unlike Ghost they need no +# third alias. Laptop ports are the box's plus 10000 and must match +# CONVEX_CLOUD_ORIGIN / CONVEX_SITE_ORIGIN in convex/docker-compose.yml. # The tunnel is an ssh control master on a socket, so `ssh -O check` / `-O exit` # find and close it on macOS and Linux alike - no pgrep/pkill, whose flags differ # (procps pgrep has no -q) and whose -f pattern also matches the calling shell. diff --git a/tools/venari/convex/docker-compose.yml b/tools/venari/convex/docker-compose.yml index 18d6b965..f9ea4767 100644 --- a/tools/venari/convex/docker-compose.yml +++ b/tools/venari/convex/docker-compose.yml @@ -1,34 +1,14 @@ -# Convex on venari: the self-hosted Convex backend and its dashboard, from -# get-convex/convex-backend's self-hosted/docker/docker-compose.yml. Both listen -# on the box's loopback only and are reached over ssh -L, like Soft Serve and -# Ghost: the cloud firewall admits 22 alone. +# Convex on venari: the self-hosted backend and dashboard, from +# get-convex/convex-backend's self-hosted/docker/docker-compose.yml. # # On the box: /srv/convex/docker-compose.yml, next to an untracked .env (see -# .env.example) and data/ (never tracked). +# .env.example) and data/ (never tracked). The database is in Ghost's cluster, +# so Ghost must be up first. # -# The database is Postgres in Ghost's cluster (ghost-postgres), not upstream's -# default SQLite: one Postgres on the box, not two. Convex gets a database of -# its own (convex_self_hosted), owned by a role of its own (convex), with -# CONNECT revoked from PUBLIC so tsdbadmin - the shared role Ghost hands to -# agents - cannot even open it. ghost-server only ever touches databases listed -# in its ghost.databases table, so it neither sees nor sizes nor drops this one -# (checked against the fork's internal/server/store.go). The two projects meet -# on the venari-postgres network, which Ghost's project creates; Convex only -# joins it, so Ghost must be up first. -# -# State is split in two, and a backup needs both, taken together: -# - convex_self_hosted in the cluster: every document and index. -# - data/: stored files, pushed function modules, search indexes (storage/), -# and credentials/ - the instance name and the secret admin keys are signed -# with. The backend generates both on first start. -# Admin keys: `generate_admin_key.sh` gives a different key on every call, and -# every one stays valid as long as credentials/ does (checked 2026-09-22: a key -# issued before a restart still worked after it). -# -# Upstream ships `:latest`. Both images are pinned to one revision here, because -# upstream says a backend and dashboard of different versions are not -# guaranteed to work together. Upgrading: self-hosted/advanced/upgrading.md - -# `npx convex export` first, then bump BOTH tags. +# The reasons for every choice below - the shared cluster and its isolation, +# the network, plaintext, the connection budget, the port offset, the pinned +# revision, admin keys and backups - are in tools/venari/README.md, "Convex", +# and only there. The comments here say what must stay true when you edit. name: convex services: backend: @@ -48,35 +28,17 @@ services: - "127.0.0.1:3210:3210" # client API, websocket sync - "127.0.0.1:3211:3211" # HTTP actions environment: - # What clients are told the backend is: the laptop's end of the tunnel. - # These appear in storage URLs and action callbacks, so they must be the - # address the client actually uses, not the container's. The laptop - # ports are offset by 10000 (`just convex-tunnel`: 13210 -> 3210), like - # Ghost's 15432 -> 5432, because 3210/3211 are where a local - # `npx convex dev` deployment starts; sharing them would let a project - # deploy to whichever backend held the port. + # The laptop's end of the tunnel, not the container's address. Must match + # the forwards in `just convex-tunnel` (box port + 10000). CONVEX_CLOUD_ORIGIN: http://127.0.0.1:13210 CONVEX_SITE_ORIGIN: http://127.0.0.1:13211 # No database name: the backend appends one derived from INSTANCE_NAME - # (default convex-self-hosted -> convex_self_hosted). - # - # Plaintext, on purpose, and both settings are needed (checked - # 2026-09-22). The backend verifies TLS certificates, and Ghost's is - # self-signed, so a TLS handshake fails with CaUsedAsEndEntity. - # DO_NOT_REQUIRE_SSL alone still attempts TLS whenever the server - # offers it, and fails the same way. `sslmode=disable` alone is refused - # without DO_NOT_REQUIRE_SSL. The hop is container to container on the - # venari-postgres bridge and never leaves the host. Anyone able to read - # that bridge is root, and root can read .env. + # (default convex-self-hosted -> convex_self_hosted). Plaintext on + # purpose; it takes BOTH sslmode=disable and DO_NOT_REQUIRE_SSL. POSTGRES_URL: "postgresql://convex:${CONVEX_POSTGRES_PASSWORD:?set it in .env}@ghost-postgres:5432?sslmode=disable" DO_NOT_REQUIRE_SSL: "1" - # Connection budget. Ghost's cluster allows 25 connections in total - # (TimescaleDB tunes max_connections from its 768m limit), 3 of them - # reserved for superusers, and every agent's Ghost database draws on - # the same 22. The backend's pool defaults to 128 - enough to starve - # them, or be starved. 8 is above the 7 it used under a 500-mutation - # burst (2026-10-16); the convex role's CONNECTION LIMIT 10 (README - # setup) enforces the cap server-side even if this knob changed. + # Keep below the convex role's CONNECTION LIMIT (10, README setup). The + # cluster has 25 connections in total, shared with every Ghost database. POSTGRES_MAX_CONNECTIONS: "8" # The anonymous usage beacon (a random ID plus the backend version). DISABLE_BEACON: "true" @@ -99,8 +61,7 @@ services: ports: - "127.0.0.1:6791:6791" environment: - # Read by the dashboard in the *browser*, so it is the laptop's tunnel - # end too. + # Read in the browser, so it is the laptop's tunnel end too. NEXT_PUBLIC_DEPLOYMENT_URL: http://127.0.0.1:13210 # Otherwise the editor panes load monaco from a CDN. NEXT_PUBLIC_LOAD_MONACO_INTERNALLY: "true" From a4d95690d8147ab0afd49214e045f13c2ad2992e Mon Sep 17 00:00:00 2001 From: Alyssa Evans Date: Fri, 16 Oct 2026 01:19:05 -0700 Subject: [PATCH 10/11] just: bind convex-tunnel's forwards to 127.0.0.1 only On macOS, -L 13210:... binds both 127.0.0.1 and [::1]. With another process on 127.0.0.1:13210, ssh still bound [::1], ExitOnForwardFailure counted that as success, and the recipe reported the tunnel open while CONVEX_SELF_HOSTED_URL (127.0.0.1:13210) reached the other process. Binding 127.0.0.1 explicitly makes the conflict fail (exit 255, no socket left), which is what the recipe's comment promised. Co-Authored-By: Claude Opus 5.5 --- tools/just/personal.just | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/just/personal.just b/tools/just/personal.just index ba808268..1fa3535a 100644 --- a/tools/just/personal.just +++ b/tools/just/personal.just @@ -58,7 +58,7 @@ convex-tunnel: fi rm -f "$sock" # a master that died uncleanly leaves its socket behind ssh -fN -o ControlMaster=yes -o ControlPath="$sock" -o ExitOnForwardFailure=yes \ - -L 13210:127.0.0.1:3210 -L 13211:127.0.0.1:3211 -L 16791:127.0.0.1:6791 venari + -L 127.0.0.1:13210:127.0.0.1:3210 -L 127.0.0.1:13211:127.0.0.1:3211 -L 127.0.0.1:16791:127.0.0.1:6791 venari echo "convex tunnel open: API 13210, HTTP actions 13211, dashboard 16791" # close the Convex tunnel From 510d04fa6277b97397d477e48ac56525fb460f7d Mon Sep 17 00:00:00 2001 From: Alyssa Evans Date: Fri, 16 Oct 2026 01:34:39 -0700 Subject: [PATCH 11/11] venari: pull Convex images from ghcr.io/convex The pinned images no longer pull from ghcr.io/get-convex: the registry answers "denied" even for :latest, and the package API says Not Found. The org was renamed to convex (github.com/get-convex/convex-backend now redirects to convex/convex-backend), and the images moved with it. The same 0cf49cb revision tags exist at ghcr.io/convex/, so the pin itself is unchanged. Upstream's self-hosted/docker/docker-compose.yml still names the old path, so following it verbatim fails the same way. The compose comment records that, so nobody "fixes" this back to match upstream. Found running the README setup on venari for the first time. Co-Authored-By: Claude Opus 5.5 --- tools/venari/README.md | 2 +- tools/venari/convex/docker-compose.yml | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/tools/venari/README.md b/tools/venari/README.md index 18029cfa..ced513fc 100644 --- a/tools/venari/README.md +++ b/tools/venari/README.md @@ -201,7 +201,7 @@ cluster also holds Convex's database, which is not (see "Convex"). a document database, server functions (queries, mutations, actions), and live queries that push new results to subscribed clients. `/srv/convex/` runs the open-source backend and dashboard from -[get-convex/convex-backend](https://github.com/get-convex/convex-backend) +[convex/convex-backend](https://github.com/convex/convex-backend) (`self-hosted/`), pinned to one revision for both images. It is not a replacement for Ghost, and Ghost is not one for it. Ghost hands out diff --git a/tools/venari/convex/docker-compose.yml b/tools/venari/convex/docker-compose.yml index f9ea4767..b82ac04f 100644 --- a/tools/venari/convex/docker-compose.yml +++ b/tools/venari/convex/docker-compose.yml @@ -1,5 +1,5 @@ # Convex on venari: the self-hosted backend and dashboard, from -# get-convex/convex-backend's self-hosted/docker/docker-compose.yml. +# convex/convex-backend's self-hosted/docker/docker-compose.yml. # # On the box: /srv/convex/docker-compose.yml, next to an untracked .env (see # .env.example) and data/ (never tracked). The database is in Ghost's cluster, @@ -12,9 +12,11 @@ name: convex services: backend: - # 0cf49cb = get-convex/convex-backend main on 2026-09-21; the tag :latest + # 0cf49cb = convex/convex-backend main on 2026-09-21; the tag :latest # pointed at on 2026-09-22. - image: ghcr.io/get-convex/convex-backend:0cf49cbf8c4b7e22e631ceda9b5111f6cccbcd49 + # ghcr.io/convex/, not get-convex/: the old path went away with the org + # rename (denied, even for :latest), though upstream's compose still names it. + image: ghcr.io/convex/convex-backend:0cf49cbf8c4b7e22e631ceda9b5111f6cccbcd49 container_name: convex-backend restart: unless-stopped stop_grace_period: 10s @@ -53,7 +55,7 @@ services: mem_limit: 512m dashboard: - image: ghcr.io/get-convex/convex-dashboard:0cf49cbf8c4b7e22e631ceda9b5111f6cccbcd49 + image: ghcr.io/convex/convex-dashboard:0cf49cbf8c4b7e22e631ceda9b5111f6cccbcd49 container_name: convex-dashboard restart: unless-stopped stop_grace_period: 10s