Setup & Install
This guide brings Engraphy up locally, end-to-end: Postgres + pgvector, the schema, a space with a pack, a token, and the running MCP server — enough to write your first memory and search it back. For production postures (systemd/launchd or Docker + reverse proxy), see the Deployment guide.
Throughout, the code, CLI, and env vars are named
engram(the product is Engraphy). Prefer127.0.0.1overlocalhostin connection URLs — on IPv6-first systemslocalhostadds a ~10 s timeout per connection.
0. Prerequisites
- Postgres 16 with the
pgvectorextension. Thepgvector/pgvector:pg16image ships both; a native install needsCREATE EXTENSION vectorto succeed. - Python ≥ 3.12 and a virtualenv.
- dbmate on
PATH—engram-admin migrateshells out to it. - Postgres client tools (
psql, andpg_dump/pg_restoreif you'll take backups or runverify-restore). - On first server boot the embedding model
nomic-ai/nomic-embed-text-v1.5(~523 MB) downloads and is cached.
1. Start Postgres + pgvector
The quickest local database is the pgvector image with a published loopback port:
docker run -d --name engraphy-pg \
-e POSTGRES_PASSWORD=devpw -e POSTGRES_DB=engram \
-p 127.0.0.1:5432:5432 \
pgvector/pgvector:pg16
Your superuser URL (used for migrations and admin) is then:
postgres://postgres:devpw@127.0.0.1:5432/engram?sslmode=disable
2. Install the package
python -m venv .venv && source .venv/bin/activate # .venv\Scripts\activate on Windows
pip install . # or pip install .[dev] to also run the test suite
This installs the engram-admin console script into the venv. It is on PATH
only while the venv is activated; if you ever get command not found, either
re-activate or use the always-works form from the repo root:
python -m engram.admin.cli <verb> ...
3. Provision the application role
Engraphy runs the server as a non-superuser role (engram_app) so that
Row-Level Security actually constrains it — the app role is deliberately not
BYPASSRLS and cannot run migrations. Create it once, against the superuser
connection, using the shipped script:
psql "postgres://postgres:devpw@127.0.0.1:5432/engram?sslmode=disable" \
-v app_role_password="devapppw" \
-f deploy/provision-app-role.sql
Your app-role URL (used by the running server) is then:
postgres://engram_app:devapppw@127.0.0.1:5432/engram?sslmode=disable
4. Run the migrations
engram-admin migrate wraps the safe sequence — an unconditional pre-migration
pg_dump, then dbmate up, then an optional restart + smoke test. On a brand-new
DB the pre-dump is nearly empty and there is nothing to restart yet, which is fine.
Run it against the superuser URL:
ENGRAM_DATABASE_URL="postgres://postgres:devpw@127.0.0.1:5432/engram?sslmode=disable" \
engram-admin migrate --dump-dir ./backups
Under the hood this runs dbmate --migrations-dir engram/db/migrations
--no-dump-schema --url <conninfo> up. The migrations ship as package data, so the
--migrations-dir default resolves even from an installed (non-repo) location.
5. Create a space, apply a pack, mint a token
Space/principal/token administration is local-CLI only (there is deliberately no network code path for it). Run these against the superuser URL:
export ENGRAM_DATABASE_URL="postgres://postgres:devpw@127.0.0.1:5432/engram?sslmode=disable"
# A space + its founding space_admin principal + that principal's personal scope:
engram-admin space create --id demo --display-name "Demo space" --principal devon
# Give the space an ontology (the shipped starter pack, or your own — see doc 3):
engram-admin pack apply packs/starter/pack.yaml --space demo
# A display-once bearer token for a client/device:
engram-admin token create --space demo --principal devon --client-name laptop --role readwrite
The token prints once — copy it now; it is stored only as a SHA-256 and cannot
be retrieved again (token revoke + token create to replace a lost one).
space createalso mints the restore sentinel node and prints its id — that is expected; you never interact with it.
6. Run the server
The server reads the app-role URL and a bind address from the environment:
ENGRAM_DATABASE_URL="postgres://engram_app:devapppw@127.0.0.1:5432/engram?sslmode=disable" \
ENGRAM_BIND_HOST=127.0.0.1 \
ENGRAM_BIND_PORT=8000 \
python -m engram.server.app
Boot order is: load config → schema-version gate (refuses to start if the DB is
behind the migrations shipped with the code — run engram-admin migrate) → warm
the embedding model → serve. It exposes three routes:
POST /mcp— the MCP endpoint (Streamable HTTP), bearer-authenticated.POST /inbox— low-friction capture, same bearer auth.GET /healthz— unauthenticated liveness/version.
On a public interface the process refuses to bind plaintext unless
ENGRAM_INSECURE_TRANSPORT_OK=true; loopback and private ranges are exempt. See
Deployment for TLS.
7. Verify
curl -s http://127.0.0.1:8000/healthz
Expect something like:
{"status":"ok","version":"0.1.0","schema_version":"0020",
"spaces":1,"embedding_model":"nomic-ai/nomic-embed-text-v1.5"}
schema_version should match the highest-numbered file in
engram/db/migrations/. If the server refused to start with a
SchemaVersionMismatch, you skipped or partially ran step 4.
8. Connect a client & write your first memory
Point an MCP client (Claude Code, Claude Desktop, or any MCP-capable client) at
http://127.0.0.1:8000/mcp with an Authorization: Bearer <token> header. See
deploy/clients.md for per-client config. Then, over the tool surface, in order:
scope_list— proves the token resolves; on a fresh install shows exactly one writable scope,personal-devon.write{"scope":"personal-devon","type":"note","title":"Coffee machine", "body":"Descale the office coffee machine monthly.","attrs":{}}→ returnsoutcome: "inserted"and a node id.search{"scope":"personal-devon","query":"how to keep the espresso maker working"}→ recall is semantic, so a paraphrase finds it.writethe same note again (identical text) → comes backoutcome: "merged"(deduplicated into the existing node — the envelope'scanonical.idis the original) instead of creating a second one. Write a re-worded version of the same fact instead and you'll getoutcome: "merged_linked"— kept as its own node, joined by asame_topicedge. Either way, a blind duplicate is never created — that's the dedup guarantee.
If step 2 fails with ENGRAM_SCOPE_UNKNOWN, the scope doesn't exist or isn't
writable — re-run scope_list, and create more with scope_create.
Next: Build your own pack to define your own node/edge ontology, or the Tool reference for every call in detail.