Saltar al contenido principal

Self-hosting Zeq

The entire open-source stack runs locally via Docker Compose. You get:

  • The full 156-endpoint API server
  • Postgres backend with fresh migrations
  • Docusaurus docs site (this site)
  • Every reference application on /apps/*
  • A working HulyaPulse loop at 1.287 Hz

What you don't get from a self-host: signatures issued against the hosted public key. Your self-hosted instance issues CKOs signed against its own locally-generated key. That's fine for development and private production; it doesn't attest across parties.

Clone

git clone https://github.com/hulyasmath/zeq-framework
cd zeq-framework

Boot the stack

The framework boots with exactly two environment variables — ZEQ_FIELD_KEY (the 32-byte master key that encrypts the ZSC vault) and DATABASE_URL. Every other secret (session secret, BYOK keys, Stripe, admin recovery, etc.) lives encrypted in the zsc_secrets table and is hydrated into process.env at boot by the launcher.

Do not create .env files. The legacy .env.local pattern is deprecated. See CLAUDE.md §9.5 and ZSC bootstrap.

First-time seed

# Pick the master key once. 64 hex chars = 32 bytes.
ZEQ_FIELD_KEY=$(openssl rand -hex 32)

# Pre-pin the fork's public origin (also seeds Postgres + ZSC).
DATABASE_URL=postgres://zeq:PASS@localhost:5432/zeq \
node infra/setup-zeq.mjs https://your-fork.example

setup-zeq.mjs is idempotent. Re-run it with a different URL to rename the fork; the runtime picks up the change within 60 seconds without a restart.

Launch

ZEQ_FIELD_KEY=<64-hex> \
DATABASE_URL=postgres://zeq:PASS@localhost:5432/zeq \
node infra/zeq-dev-launch.mjs

Or just ./infra/dev-launch.sh, which sources the two pointers from ~/.zeq-bootstrap (gitignored, host-local) and execs the launcher.

The launcher reads the vault, hydrates process.env, then spawns the bundled dist/server.mjs. Expected output: one Node process bound to localhost:3099 serving every framework surface (admin, portal, state explorer, SDK, all apps).

Verify

curl http://localhost:3099/api/health
# {"ok": true, "hulyapulse_hz": 1.287, "zeqond_s": 0.777, "version": "1.287.5"}

curl http://localhost:3099/api/transparency/snapshot
# Live network snapshot (Active N, supply, mint/day, foundation share).

Run the docs site locally

cd sdk-docs/source
npm install
npm run start
# Opens http://localhost:3000

Run an app locally

Apps are static bundles served directly by the API server. Visit e.g. http://localhost:3010/apps/aero-wind-tunnel/ after the stack is up.

Using your self-hosted stack from the SDK

const zeq = new ZeqClient({
apiKey: "zeq_local_...", // your locally-issued key
baseUrl: "http://localhost:3010", // your stack
});

Issue yourself a local API key:

curl -X POST http://localhost:3010/zeq-auth/register \
-H "content-type: application/json" \
-d '{"email":"you@example.com","password":"..."}'
# Returns an API key in the response.

Migrating to hosted

If you outgrow self-hosting, migration is simple: swap baseUrl back to https://api.zeq.dev and supply a hosted key. No code changes. CKOs from the hosted service will be verifiable against the hosted public key; CKOs your self-host issued are verifiable against your key. You can run both side-by-side.

Known limitations of self-host

  • No authoritative time. Your Zeqond is synced off your local clock, which drifts at whatever rate NTP allows.
  • No multi-tenant isolation. The open-source stack is single-tenant by design — one database, one key.
  • No HSM. Signing keys are stored in .env.local or a local keystore — not production-grade.
  • Limited scale. The stack is single-Postgres; load beyond a few RPS needs tuning.

If any of these matter, use the hosted service.


Forking a framework domain (the ZG bring-up runbook)

A fork is a per-domain Docker container running the same source as the canonical zeq.dev, against its own Postgres, its own ZSC vault, and its own admin roster. zeqstate.com was the first fork shipped under this pattern (2026-05-28); the steps below are the canonical checklist for any subsequent domain.

The invariant: Desktop = GitHub = VPS for source. The Docker image mounts apps/zeq-dev/public/ read-only from the VPS, so static changes propagate via git pull with no rebuild. API changes need a fresh dist/server.mjs shipped via SCP + docker cp.

ZG.0 — Pre-flight schema diff

Before booting a fresh container, sanity-check the per-domain DB schema against the latest migrations:

SELECT to_regclass('public.tally_transitions') IS NOT NULL,
to_regclass('public.domain_economy_config') IS NOT NULL,
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_name='network_snapshots'
AND column_name='econ_version');

All three should be t. If any is f, run the recovery runbook (see CLAUDE.md §9.7) before booting — tally_transitions missing crashes the API in a restart loop on the first compute tick.

ZG.1 — Mount the canonical static tree

Per-domain compose YAML at infra/per-domain/<domain>.yml mounts the canonical apps/zeq-dev/public/ (NOT a per-domain fork):

volumes:
- /opt/zeq-framework/apps/zeq-dev/public:/app/public:ro
- /opt/zeq-framework/sdk-docs/source/build:/app/public/sdk:ro

The second mount resolves the apps/zeq-dev/public/sdk symlink through the container — without it, /sdk/ returns 404.

ZG.2 — Bootstrap pointers in host shell, NOT a .env

Two pointers per fork. Store in /root/.zeq-bootstrap-<domain> (gitignored, host-local) or your shell's rc:

export <DOMAIN>_FIELD_KEY=$(openssl rand -hex 32)
export <DOMAIN>_DB_PASSWORD=$(openssl rand -hex 24)

Source them before docker compose up -d with the auto-export idiom:

set -a
source /root/.zeq-bootstrap-<domain>
set +a
docker compose -f infra/per-domain/<domain>.yml up -d

Without set -a + source + set +a, compose can't read the env vars and the container will exit with ZEQ_FIELD_KEY missing.

ZG.3 — Seed the vault + pin the genesis admin

docker exec <domain>-api-1 node infra/setup-zeq.mjs https://<domain>

This writes every secret into zsc_secrets (SESSION_SECRET, HITE_SECRET, ALLOWED_ORIGINS, etc.) and pins the first genesis admin into domain_genesis.admin_zid. The container hydrates them on next boot. Note setup-zeq.mjs is idempotent — safe to re-run.

System-default LLM credentials (Fireworks deepseek-v4-pro) live in user_llm_credentials, not zsc_secrets. BYOK keys are per-user with FK → users.id, while ZSC is framework-wide infra. Seed them separately if your fork needs LLM out of the box.

ZG.4 — nginx timeout for LLM-bearing routes

The default 60-second proxy_read_timeout cuts the build flow off mid-stream (typical BUILD-mode LLM completion is ~50 seconds, just inside the limit on a good day, well over on a bad one). On the VPS nginx block for the fork:

location / {
proxy_pass http://127.0.0.1:<port>;
proxy_read_timeout 600s; # required for /api/zeq/agent/* + /api/chat/*
proxy_send_timeout 600s;
# ... rest of headers
}

This was ZG.AUDIT Fix 4. Don't skip it — without it, the LLM-bearing routes throw 504 Gateway Timeout and the wizard hangs.

ZG.5 — Smoke test (the gate)

Every fork must pass the same end-to-end sweep before sign-off:

DOMAIN=your-fork.example

# Health + transparency + auth surfaces
for path in /api/health /api/transparency/snapshot /api/chain/recent \
/api/chain/state-machines /api/machines/top \
/ /s/<test-machine>/ /state/ /admin/ /portal/ /sdk/; do
echo -n "$path "
curl -sk -o /dev/null -w "%{http_code}\n" "https://$DOMAIN$path"
done

All should return 200 (except admin-protected paths returning 302 to sign-in). /api/transparency/snapshot returns 400 with INVALID_ZEQOND if called without ?zeqond=N — that's correct.

ZG.6 — User-surface E2E (the real gate)

The smoke test only proves routing. The real bar is the four-surface workbench driving Skill → Plan → Build → Deploy to a published machine page. Open the fork in Chrome, register a fresh admin, claim a machine, run all four surfaces, click BUILD & DEPLOY, then load /s/<machine>/p/<page>/ and verify the page renders the deployed HTML rather than the welcome canvas.

If a fork passes ZG.0 through ZG.6, it's production-ready.

ZG.7 — Cache invalidation note

After a fresh deploy, the user's browser may show one more canvas-cached render before the routing fix lands. Verify with curl or incognito to distinguish a cache stale from a routing bug — this misdiagnosed FIX 5 in the original zeqstate bring-up.