Imported from joelhooks/joelclaw (
skills/local-convex/SKILL.md). Install upstream withnpx skills add joelhooks/joelclaw --skill local-convex. Copyright stays with the author.
Local Convex
Operate Joel's local self-hosted Convex setup. This is not generic Convex advice. It is the local joelclaw path: Central Postgres for database state and Garage on three-body for S3-compatible object storage. The custom MinIO install is now a rollback/reference path. ASUSTOR MinIO CE is only a temporary smoke-test reference.
Canonical Sources
Read these before mutating the setup:
- Project note:
/Users/joel/.brain/projects/convex-self-hosting-local.svx - Garage installer:
/Users/joel/Documents/Codex/2026-06-17/we-re-setting-up-durable-self/outputs/install-three-body-convex-garage.sh - Legacy MinIO runbook:
/Users/joel/Documents/Codex/2026-06-17/we-re-setting-up-durable-self/outputs/three-body-convex-minio-runbook.md - Legacy MinIO installer:
/Users/joel/Documents/Codex/2026-06-17/we-re-setting-up-durable-self/outputs/install-three-body-convex-minio.sh
If those disagree, prefer the brain project note for current truth, then verify live state with non-secret health checks.
Current Local Topology
- NAS host:
three-body - Tailnet IP:
100.67.156.41 - Prefer the tailnet IP in machine config and smoke tests. Treat
three-bodyas a human label, not the Convex S3 endpoint. - NAS model:
AS6508T - ADM:
5.1.3.RI81 - Database state: Central Postgres, not NAS storage.
- Central Postgres runs as the
joelclawservice user:- binary:
/opt/homebrew/opt/postgresql@17/bin/postgres - data:
/Users/Shared/joelclaw/data/postgres - config:
/Users/Shared/joelclaw/etc/postgres/postgresql.conf - TCP:
127.0.0.1:5432 - socket:
/Users/Shared/joelclaw/run/.s.PGSQL.5432 - auth: local socket uses peer auth; TCP uses SCRAM password auth.
- binary:
- Canonical object storage: Garage on
three-body. - Garage S3 API:
http://100.67.156.41:39100 - Garage data:
/volume1/joelclaw/s3/garage/data - Garage metadata:
/volume1/joelclaw/s3/garage/meta - Garage snapshots:
/volume1/joelclaw/s3/garage/snapshots - Garage Compose path on NAS:
/volume1/joelclaw/s3/garage-compose/compose.yaml - Garage Convex S3 env on NAS:
/volume1/joelclaw/s3/garage-compose/convex-s3.env - Garage redacted Convex S3 env on NAS:
/volume1/joelclaw/s3/garage-compose/convex-s3.redacted.env
Custom MinIO is a rollback/reference path, not canonical:
- Custom MinIO API:
http://100.67.156.41:39000 - Custom MinIO console:
http://100.67.156.41:39001 - Custom MinIO data:
/volume1/joelclaw/s3/minio/data - Custom MinIO Compose path on NAS:
/volume1/joelclaw/s3/minio-compose/compose.yaml - Custom MinIO Convex S3 env on NAS:
/volume1/joelclaw/s3/minio-compose/convex-s3.env
ASUSTOR MinIO CE is not canonical:
- CE API:
http://100.67.156.41:29990 - CE console:
http://100.67.156.41:29991 - CE data:
/share/MinIOCE/data - CE login verified during setup:
minioadmin / minioadminPassWD9001 - CE exists only as a temporary smoke-test reference and should be uninstalled after the custom path is proven through Convex.
Convex Buckets
Garage should have these Convex buckets:
joelclaw-local-usw2-convex-exportsjoelclaw-local-usw2-convex-snapshot-importsjoelclaw-local-usw2-convex-modulesjoelclaw-local-usw2-convex-filesjoelclaw-local-usw2-convex-search
Convex Env Shape
Use the generated secret values from the NAS env file. Do not print them in chat.
AWS_REGION=usw2
S3_ENDPOINT_URL=http://100.67.156.41:39100
AWS_S3_FORCE_PATH_STYLE=true
AWS_ACCESS_KEY_ID=<from /volume1/joelclaw/s3/garage-compose/convex-s3.env>
AWS_SECRET_ACCESS_KEY=<from /volume1/joelclaw/s3/garage-compose/convex-s3.env>
S3_STORAGE_EXPORTS_BUCKET=joelclaw-local-usw2-convex-exports
S3_STORAGE_SNAPSHOT_IMPORTS_BUCKET=joelclaw-local-usw2-convex-snapshot-imports
S3_STORAGE_MODULES_BUCKET=joelclaw-local-usw2-convex-modules
S3_STORAGE_FILES_BUCKET=joelclaw-local-usw2-convex-files
S3_STORAGE_SEARCH_BUCKET=joelclaw-local-usw2-convex-search
Health Checks
Prefer checks that do not reveal secrets:
tailscale ping --timeout=5s --c 2 three-body
nc -vz -w 5 100.67.156.41 39100
curl -sS -m 5 -o /tmp/garage-root.out -w '%{http_code}\n' http://100.67.156.41:39100/
Garage does not allow anonymous S3 access; a 403 AccessDenied response from / proves the API is reachable.
For MinIO rollback/reference only:
curl -fsS -m 5 http://100.67.156.41:39000/minio/health/ready
nc -vz -w 5 100.67.156.41 39000
nc -vz -w 5 100.67.156.41 39001
For CE reference only:
curl -fsS -m 5 http://100.67.156.41:29990/minio/health/ready
NAS Access Rules
- Use one SSH session at a time.
three-bodyhas reset SSH handshakes after bursts of concurrent probes. - Back off after
kex_exchange_identificationorNot allowed at this time; do not hammer SSH. joel@three-bodycan inspect system state.joelis inadministrators, but Docker socket access is still denied because/var/run/docker.sockisroot:rootwith mode660.- Docker operations require
sudoor an ADM/admin path. - Do not probe
admin@three-bodyorroot@three-bodyrepeatedly; earlier bad auth contributed to lockout symptoms.
Central Postgres Rules
- Do not start a separate Postgres container for Convex. The target is Central Postgres.
- Do not move Postgres data to NAS.
- Bootstrap/admin work goes through the
joelclawservice user over the local socket:
sudo -u joelclaw psql -h /Users/Shared/joelclaw/run -p 5432 -d postgres
- Dockerized Convex reaches Central Postgres over TCP at
host.docker.internal:5432. - Convex's
POSTGRES_URLshould omit the database name. Convex derives the database name fromINSTANCE_NAME;INSTANCE_NAME=joelclaw-convexmaps to databasejoelclaw_convex. - For local Central Postgres, set
DO_NOT_REQUIRE_SSL=1.
Garage Operations
The Garage installer has already completed successfully. Re-run it only to reconcile the service after intentional edits:
ssh -t joel@three-body 'sudo sh /volume1/joelclaw/s3/install-three-body-convex-garage.sh'
The installer:
- runs
joelclaw-garageseparately from custom MinIO and ASUSTOR MinIO CE, - uses S3 API port
39100, - writes data under
/volume1/joelclaw/s3/garage/data, - writes metadata under
/volume1/joelclaw/s3/garage/meta, - writes metadata snapshots under
/volume1/joelclaw/s3/garage/snapshots, - uses a pinned amd64 Garage image digest,
- generates Garage admin/RPC tokens and Convex-specific S3 credentials on the NAS,
- creates the five Convex buckets,
- writes
/volume1/joelclaw/s3/garage-compose/convex-s3.env, - mirrors existing objects from custom MinIO into Garage,
- runs an upload/read/delete smoke test through the Convex-specific credentials.
Do not expose Garage to the public internet. LAN/tailnet only.
Local Convex Runner
The current projectless setup staged a local runner at:
/Users/joel/Documents/Codex/2026-06-17/we-re-setting-up-durable-self/work/local-convex
Important files:
prepare-env.shimports/volume1/joelclaw/s3/garage-compose/convex-s3.envfrom the NAS and writes a local.envwithout printing secrets.prepare-env.shpreserves the existingCONVEX_POSTGRES_PASSWORDandINSTANCE_SECRETwhen switching S3 endpoints; do not rotate database credentials just to change object storage.bootstrap-postgres.shcreates/updates theconvexPostgres role andjoelclaw_convexdatabase viasudo -u joelclaw.compose.yamlstartsghcr.io/get-convex/convex-backendandghcr.io/get-convex/convex-dashboard.generate-admin-env.shstores the generated self-hosted admin key inapp-admin.envwithout printing it.run.shperforms the bootstrap, starts the backend/dashboard, and writes the admin env.smoke-self-hosted-convex.shdeploysjoelclaw-api/convex, writes/reads/removes a temporary row, exports a snapshot, and verifies the export ZIP.
Selective Exposure
Convex is intentionally exposed by named interfaces and ports only. Do not change this to a wildcard 0.0.0.0 bind.
Current verified URLs:
# Local
http://127.0.0.1:3210
http://127.0.0.1:3211
http://127.0.0.1:6791
# LAN on Flagg
http://192.168.1.10:3210
http://192.168.1.10:3211
http://192.168.1.10:6791
# Tailnet on Flagg
http://100.99.76.47:3210
http://100.99.76.47:3211
http://100.99.76.47:6791
Implementation:
- Docker Compose keeps the Convex backend and dashboards bound to
127.0.0.1only. - LAN exposure is a system LaunchDaemon that runs the TCP forwarder as
joelbefore GUI login:- plist source:
infra/central/native/com.joelclaw.local-convex.lan-forwarder.plist.template - forwarder source:
infra/central/native/local-convex-lan-forwarder.mjs - installer:
infra/central/native/install-local-convex-lan-forwarder.sh - binds
192.168.1.10:{3210,3211,6791}and forwards to127.0.0.1:{3210,3211,6791}
- plist source:
- Tailnet exposure is Tailscale Serve TCP forwarding:
100.99.76.47:3210 -> 127.0.0.1:3210100.99.76.47:3211 -> 127.0.0.1:3211100.99.76.47:6791 -> 127.0.0.1:6792
- The tailnet dashboard uses a second local dashboard container on
127.0.0.1:6792so itsNEXT_PUBLIC_DEPLOYMENT_URLpoints athttp://100.99.76.47:3210. - The LAN/local dashboard on
127.0.0.1:6791is configured withNEXT_PUBLIC_DEPLOYMENT_URL=http://192.168.1.10:3210.
Known Docker Desktop gotcha:
- Docker could not bind directly to
192.168.1.10or100.99.76.47on this Mac; attempts failed withcannot assign requested address. - Keep Docker localhost-only and expose selected services through host-level forwarders.
Manage LAN forwarder:
cd /Users/joel/Code/joelhooks/joelclaw
sudo ./infra/central/native/install-local-convex-lan-forwarder.sh
sudo launchctl print system/com.joelclaw.local-convex.lan-forwarder
The old GUI LaunchAgent under the protected Documents/Codex work directory did not start after a reboot without GUI login. Do not restore that shape.
Manage tailnet forwards without disturbing unrelated Tailscale Serve config:
tailscale serve --bg --yes --tcp 3210 127.0.0.1:3210
tailscale serve --bg --yes --tcp 3211 127.0.0.1:3211
tailscale serve --bg --yes --tcp 6791 127.0.0.1:6792
tailscale serve status --json
# Disable only the Convex forwards if needed.
tailscale serve --tcp=3210 off
tailscale serve --tcp=3211 off
tailscale serve --tcp=6791 off
Do not run tailscale serve reset casually; there may be unrelated Serve routes on this node.
Exposure checks:
curl -fsS -m 5 http://127.0.0.1:3210/version
curl -fsS -m 5 http://192.168.1.10:3210/version
curl -fsS -m 5 http://100.99.76.47:3210/version
curl -fsSI -m 5 http://127.0.0.1:6791
curl -fsSI -m 5 http://192.168.1.10:6791
curl -fsSI -m 5 http://100.99.76.47:6791
lsof -nP -iTCP:3210 -iTCP:3211 -iTCP:6791 -iTCP:6792 -sTCP:LISTEN
The healthy self-hosted backend /version response is currently unknown.
Secret Handling
- Never paste Garage admin/RPC tokens, Convex Garage credentials, root MinIO credentials, or Convex MinIO credentials into chat.
- Never commit
.env,convex-s3.env, admin tokens, root credentials, access keys, or secret keys. - It is okay to read and report the redacted env file.
- If a command must use secrets, prefer running it on
three-bodywhere the env file already lives. - If copying env values to another host, use the existing secret-management path for that project, not ad hoc notes.
Operating Sequence
When asked to wire or debug local Convex:
- Read the brain project note.
- Verify Garage S3 API reachability on
39100. - Verify whether Convex env is pointing at Garage by IP, not MinIO CE and not the tailnet alias.
- Confirm Central Postgres is the DB target; do not move Postgres state to NAS.
- Run a Convex-level export/import or file upload smoke test.
- If exposing Convex beyond localhost, keep Docker bound to localhost and use the selective LAN/Tailscale forwarding model above.
- Capture any durable change back into the brain project note.
- Leave custom MinIO and MinIO CE alone until the Garage-backed Convex path is proven and Joel explicitly wants cleanup.
Gotchas
- The old CE smoke-test buckets do not prove the durable path. Only Garage on
39100counts for the current canonical path. - Do not expose Convex with
0.0.0.0just because Docker port binding is convenient. Use selected LAN/tailnet forwards. - Tailscale Serve has unrelated routes on this node; do not reset the whole Serve config when only changing Convex ports.
- The dashboard bakes in one public deployment URL. Keep separate LAN and tailnet dashboard instances if both access paths need to work cleanly.
- The NAS SSH service is sensitive to rapid probes. Parallel SSH commands can cause resets.
- Garage image availability can change; if changing image tags, verify current registry availability and pin a digest.
- Do not silently switch to local Flagg SSD object storage for Convex durability. The object storage target is NAS-backed Garage.