Deployment Guide
Engraphy ships two supported deployment postures from the same artifact. Pick one.
| Profile | Placement | Transport | Install |
|---|---|---|---|
| Local / overlay | home server, reachable only over a tailnet/VPN | the overlay network is the encryption; TLS optional | systemd (Linux) or launchd (macOS) unit — deploy/units/ |
| Cloud | public VM/container host | TLS mandatory (reverse proxy in front) | Docker image + compose.yaml |
The full step-by-step for both lives in deploy/checklist.md (follow it in
order — each step depends on the one before). This guide is the operational
overview and the reference for auth, scopes, and day-2 tasks.
Environment variables
| Var | Used by | Meaning |
|---|---|---|
ENGRAPHY_DATABASE_URL |
server (app role) and admin CLI (superuser) | Postgres connection. The server uses the non-superuser engraphy_app role; admin/migrate use the superuser/owner role. |
ENGRAPHY_BIND_HOST |
server | interface to bind (default 127.0.0.1). |
ENGRAPHY_BIND_PORT |
server | port (default 8000). |
ENGRAPHY_INSECURE_TRANSPORT_OK |
server | set true only to bind a genuinely public interface without TLS on an overlay-only network. |
ENGRAPHY_LAST_BACKUP_STATUS_FILE |
server | path whose contents /healthz reports as last_backup_at — your backup job writes the completion timestamp here. |
ENGRAPHY_EMBEDDING_PROFILE |
server and admin CLI | which embedding backend runs (default onnx-fp32). See below. |
ENGRAPHY_EMBEDDING_THREADS |
server | ONNX Runtime intra-op threads (default 1). The embed runs beside the connection pool and the database, and extra threads contend with them: measured search p50 is 76.8ms at one thread and 156ms unpinned. Raise it only where the embedder has cores to itself. |
Embedding profiles
One model, one pinned revision, three ways to run it. All three emit the same
384-dim unit vectors into the same vector(384) column, so no profile implies a
schema change.
| Profile | Runs on | Vector space | Notes |
|---|---|---|---|
onnx-fp32 (default) |
ONNX Runtime, full graph | same as legacy-torch |
The shipped default. Reproduces the torch vectors to float noise. |
onnx-int8 |
ONNX Runtime, quantized graph | its own | Smaller and faster. Calibrate on the target host first, see below. |
legacy-torch |
sentence-transformers | same as onnx-fp32 |
Needs pip install '.[legacy-torch]'; see requirements-cpu.txt. |
Switching between onnx-fp32 and legacy-torch is a restart. Their vectors
are interchangeable and they share a stamp in nodes.embedding_model, so nothing
in the store needs to change.
Switching onto onnx-int8 needs a re-embed and a calibration step.
Quantization moves pairwise cosine, so int8 runs its own dedup bands (t_high
0.94, against 0.95 elsewhere) and its vectors are not interchangeable with the
others. Until every row is rewritten the write path compares new int8 vectors
against older ones, which is a comparison across two vector spaces:
engraphy-admin reembed --space <space> --dry-run # what would change
engraphy-admin reembed --space <space> # do it
It is resumable and idempotent, so a killed run resumes and a completed run finds nothing. The error direction while it is incomplete is the safe one, a near-duplicate opens a confirm round-trip rather than merging silently, but run it to completion rather than leaving a store half converted. Take a restore-tested backup first on a live space (design/04).
Calibrate dedup.t_low for int8 on the host that will run it. Quantized
arithmetic varies with the CPU, and at the confirm edge that variation is larger
than the margin int8 leaves: on two Linux x86-64 hosts the same fixtures gave
viable t_low windows of (0.8072, 0.8197] and (0.7809, 0.8017], which do not
intersect. Run scripts/baseline_dedup_fixtures_profile.py --profile onnx-int8
on the target host, read the window it reports, and set dedup.t_low for the
space:
engraphy-admin config set --space <space> --key dedup.t_low --value 0.80
The onnx-fp32 and legacy-torch profiles need none of this. They are
bit-reproducible and leave 0.040 of room at the same edge.
Running as a service
Local / overlay (systemd / launchd)
- Bind an overlay-only address (a tailnet
100.x.y.z, or127.0.0.1behind a local proxy) — never0.0.0.0or a bare public interface. - Provision the app role once:
psql "$SUPERUSER_URL" -v app_role_password=… -f deploy/provision-app-role.sql. engraphy-admin migrate --dump-dir /var/backups/engraphy(superuser URL).- Install the unit from
deploy/units/(engraphy.servicefor Linux,com.engraphy.server.plistfor macOS), put the env vars in a mode-0600/etc/engraphy/engraphy.env, andsystemctl enable --now engraphy(orlaunchctl load …).
The server refuses to bind a public interface in plaintext unless
ENGRAPHY_INSECURE_TRANSPORT_OK=true; loopback / RFC1918 / CGNAT / tailnet ranges are
exempt and need no opt-in.
Cloud (Docker + reverse proxy)
compose.yaml defines three services: postgres (pgvector/pgvector:pg16),
engraphy (the server), and a one-shot admin sidecar (carries dbmate +
pg_dump/pg_restore/psql, which the server image omits).
- Create a git-ignored
.envnext tocompose.yamlwithPOSTGRES_PASSWORDandENGRAPHY_APP_ROLE_PASSWORD. docker compose up -d postgres; wait for(healthy).- Migrate via the sidecar (it reaches postgres over the compose network, so
postgres needs no published port):
bash docker compose --profile admin run --rm admin engraphy-admin migrate --dump-dir /backups - Provision the app role via the sidecar:
bash docker compose --profile admin run --rm admin \ psql "$ENGRAPHY_DATABASE_URL" -v app_role_password="$ENGRAPHY_APP_ROLE_PASSWORD" \ -f deploy/provision-app-role.sql - Put a TLS-terminating reverse proxy (Caddy/nginx/Traefik or a load balancer) in
front of
127.0.0.1:8000— engraphy's own port is bound to loopback and never exposed directly. Caddy one-liner:your.domain { reverse_proxy 127.0.0.1:8000 }. docker compose up -d engraphy. The embedding model ships baked into the image, so the container seeds themodel-cachevolume from it and serves immediately. Watchdocker compose psgo(health: starting)to(healthy).Restartingmeans a real error; readdocker compose logs engraphy.
The transport-security refusal classifies the bind host — a container bound to
0.0.0.0is classified "private" and boots without the opt-in even though a published port makes it reachable. The real boundary iscompose.yaml's loopbackports:mapping + your firewall, not this check. Only 443 (your proxy) should be open to the internet; engraphy's and postgres's ports must not be.
Auth, principals, scopes
- Tokens are per
(space, principal, client_name), display-once bearer tokens (only a SHA-256 is stored).client_namebecomes thesource_clientprovenance on every write, so never share a token across devices. Mint withengraphy-admin token create …; revoke withengraphy-admin token revoke …(effective on the next request — no cache window). Aroleofreadwriteorreadonlygates write tools. - Principals are actors in a space. Each CLI-created principal gets a private,
ambient
personal-<id>scope in the same transaction. - Scopes are isolation containers with a
visibility: private— owner + explicit grants only.team-read— every principal in the space may read.team-write— every principal may read and write. Change withadmin_scope_visibility; grant a specific principal access withadmin_grant(read/write). All of this is enforced by Postgres RLS — the app role is notBYPASSRLS, so a bug in the application layer cannot leak across scopes.- Space administration is CLI-only for bootstrap.
space createand the first token must come fromengraphy-adminon the host (there is no network code path). Ongoing member/token/visibility/grant management is available either via the CLI or via the fouradmin_*MCP tools — unless a space setsspace_admin_tools: false(viaengraphy-admin config set), which removes those tools entirely for the paranoid "everything via the CLI" posture.
Day-2 operations (the engraphy-admin CLI)
Run against the superuser connection. All verbs also work as python -m
engraphy.admin.cli <verb> if engraphy-admin isn't on PATH.
| Command | What it does |
|---|---|
space create --id … --display-name … --principal … |
create a space + founding space_admin + personal scope + restore sentinel. |
principal add --space … --id … --display-name … [--role …] |
add a member (+ their personal scope). |
token create / token revoke |
mint / revoke a bearer token. |
config set --space … --key … --value … |
set a per-space config value (JSON), e.g. dedup.t_high, space_admin_tools. |
pack validate / pack apply / pack upgrade |
validate, apply, or migrate a pack's ontology. |
import <file.jsonl> --space … --scope … --principal … |
bulk-load through the dedup pipeline (idempotent; pending items → review-queue CSV). |
addenda promote --space … |
promote get-only addenda into searchable member nodes (Phase B recovery). |
surface rebuild --space … |
recompute the searchable attr surface + re-embed changed rows (run after a Phase C migration or a searchable-flag change). |
migrate |
pre-dump → dbmate up → restart → smoke test. |
verify-restore --against <dump> |
restore a dump into a throwaway scratch DB and assert schema version, row counts, constraints, and sentinel retrieval. Safe against production's own connection (never touches the live DB). |
doctor --space … |
read-only hygiene report (stale pendings, nonconforming attrs, registry drift, long chains, over-addenda nodes). |
Backups & restore-testing
Backups are the operator's job (Engraphy takes an unconditional pre-dump before
every migrate, but ongoing backups are external):
- Schedule
pg_dump --format=customon your RPO cadence (6 h is the documented personal-scale sweet spot). - Have the job write the completion timestamp to
ENGRAPHY_LAST_BACKUP_STATUS_FILE, so/healthzsurfaces staleness in-band. - Monthly, run
engraphy-admin verify-restore --against <one of the dumps>— "restore-tested, not just taken". It asserts the restore is usable, including retrieving the space's restore sentinel node.
Health & version
GET /healthz (unauthenticated) returns {status, version, schema_version, spaces,
embedding_model, last_backup_at?}. schema_version should equal the
highest-numbered file in engraphy/db/migrations/; the server refuses to start if
the DB is behind (run engraphy-admin migrate).
Client onboarding
Point an MCP client at https://your.domain/mcp (or the overlay address for the
local profile) with Authorization: Bearer <token>. Per-client configuration
(Claude Code, Claude Desktop, others) and plan prerequisites are in
deploy/clients.md.