graphos

Open source · Apache-2.0 · version 0.13.1

pomentum/graphos

graphos

A multi-tenant property-graph service: PostgreSQL + Apache AGE behind a typed HTTP API.

Your services send JSON over HTTP. graphos owns the database, writes the SQL against AGE's tables, enforces tenant isolation, and gives you bulk loading, traversal, aggregation, graph algorithms, a read-only Cypher console and a web UI. AI agents read the same graphs over MCP. Consumers never get a connection string.

In 30 seconds

  • The problem: Connected data turns up in most systems. Once several services need it, each one ends up holding database credentials, its own Cypher, and its own answers to bulk loading, indexing and isolation.
  • The approach: One service owns the graph. Consumers call a typed REST API, and AI agents the same reads over MCP; graphos alone talks to PostgreSQL + Apache AGE on a private network.
  • What it is not: Not a query passthrough for applications (Cypher is offered read-only, under graphos's checks), not a new database engine, and not tied to any domain: graphos ships no labels of its own.
  • Licence: Apache 2.0, like the Apache AGE extension it is built on.

From many database clients to one graph service

Left: three services each embed a database driver, a connection string and their own Cypher, and all reach one shared graph database, so credentials and query text spread, there is no tenant boundary, a slow query affects everyone and each team re-solves bulk load and indexing. Right: the same services call graphos over typed HTTP; graphos alone reaches PostgreSQL with Apache AGE on a private network, giving each consumer its own account and short-lived tokens, a graph per tenant with bounded, timed queries, and built-in bulk load, indexing and community detection. Left: three services each embed a database driver, a connection string and their own Cypher, and all reach one shared graph database, so credentials and query text spread, there is no tenant boundary, a slow query affects everyone and each team re-solves bulk load and indexing. Right: the same services call graphos over typed HTTP; graphos alone reaches PostgreSQL with Apache AGE on a private network, giving each consumer its own account and short-lived tokens, a graph per tenant with bounded, timed queries, and built-in bulk load, indexing and community detection.
Fig 01Without graphos, every service holds a driver, a connection string and its own Cypher. With it, services call typed HTTP and only graphos reaches PostgreSQL + Apache AGE.

What graphos is made of

Storage
PostgreSQL 18 with Apache AGE 1.8; each tenant is its own AGE graph
API
Typed HTTP (FastAPI), JSON and NDJSON; the contract at /openapi.json
Access
Users and service accounts, 15-minute tokens, OpenID Connect sign-on, a level per tenant
Algorithms
Louvain (optional extra), PageRank, betweenness, components, degree
Clients
An async, typed Python client; an optional web UI; plain HTTP
Images
ghcr.io/pomentum/graphos-api and graphos-ui, amd64 and arm64
Licence
Apache-2.0
Latest
0.13.1
Agents
An MCP endpoint, /mcp: 22 read tools on the API's tokens and grants

Build, explore, run

Build

Tenants and schema

Each tenant is its own AGE graph with its own labels; no built-in labels or property names; optional property rules, warn or strict (usage).

Fast, idempotent writes

Batches, NDJSON bulk and snapshot import as COPY-staged upserts, 37–156× faster than per-row Cypher MERGE in our measurements (usage, AGE notes).

Load a CSV file

Send the file itself; graphos maps its columns to properties, merges rows that repeat an id, checks every row (a dry run writes nothing) and writes them as bulk loads do (usage).

Edit in place, safely

Patch or delete one node or edge, or delete up to 1,000 nodes in one transaction; If-Match turns a concurrent edit into a 412 instead of a lost update (usage).

Composable reads

Search (substring or ranked indexes), neighbors, multi-hop expand, shortest paths, scans and subgraphs (usage).

Rule-derived edges

Declare a property-match rule once; edges:resolve writes the edges in SQL (usage).

Reconcile by difference

Send the whole set of a node's or a type's edges; graphos adds, changes and removes only the difference, in one transaction (usage).

Copy, back up and restore

A tenant's schema and data as files, restored exactly or loaded into plain AGE (usage, without graphos).

Python client

Async and typed; signs in and renews its token by itself (below).

Explore

Graph analytics

A map of every label and the edges between them (about a second on ten million edges), aggregates, top-K by degree (usage).

Graph algorithms

Louvain communities (optional extra), PageRank, betweenness, components and degree, run beside the API and written onto the nodes (what each finds).

Cypher console

Read-only Cypher on one tenant, write clauses refused, with a timeout, a row cap and saved queries (usage).

AI agents (MCP)

/mcp serves graphos's reads to agents as tools and resources, with the caller's grants and a schema summary made for models; MCP clients sign people in themselves (agents guide).

Web UI (optional)

Explore, inspect and edit, work on many nodes at once, import a CSV file, query, extend the schema, follow changes live and export what is on screen (below).

Run

Safe by default

15-minute tokens for people and service accounts, single sign-on, a level per tenant, stored secrets sealed with a master key, a Host allowlist, statement timeouts, the database out of reach (installation, single sign-on).

Change feed

Every write recorded in its transaction; read after a cursor or follow live (usage).

Background jobs

Long operations answer 202 at once, then can be polled and cancelled (usage).

Operable

Prometheus metrics, JSON logs with traceparent, an audit log and per-user rate limiting (usage).

Bulk writes 37–156× faster than per-row Cypher MERGE

Every write goes through COPY-staged SQL upserts instead of Cypher, every label gets its indexes at registration, and the same graph reads faster on PostgreSQL 18 with AGE 1.8, which graphos now requires. Measured with the scripts in scripts/, by profiling queries and by timing requests against a running service: orders of magnitude, not guarantees.

Factor = before ÷ after; bars on a log scale. Writes: 13k nodes and 44k edges, 4 GB shared_buffers, one host. Lookups: a ~117k-row label. Reads: 251,246 nodes and 940,144 edges, the image's default settings, a laptop, 30 warm runs of 12 reads.
MeasuredBeforegraphosFactor (log scale)×
Bulk edge writesper-row Cypher MERGE → COPY-staged INSERT … ON CONFLICT31 /s1,153–4,840 /s37–156
Substring search on a nameno index → the trigram index graphos creates1.2 s1.3 ms~920
Lookup by community idno index → the index graphos creates2.4 s0.5 ms~4,800
12-read mix, 5 users, 360 requestsPostgreSQL 16 / AGE 1.6 → PostgreSQL 18 / AGE 1.857 s35 s1.6
Paths of up to 4 hops, median, 5 usersPostgreSQL 16 / AGE 1.6 → PostgreSQL 18 / AGE 1.81,233 ms529 ms2.3

The COPY figures come from scripts/bench_bulk_write.py; the per-row MERGE baseline was measured with an earlier graphos that still had a Cypher write path and can no longer be rerun. PostgreSQL 16 / AGE 1.6 was measured before graphos dropped it; a stack on it still moves to 18 by itself on docker compose up. The PostgreSQL 18 database was freshly restored and the 16 one carried about 3× dead space, which favours 18. Details: why every write is SQL, indexes created at registration, measured on PostgreSQL 18.

~1 sa map of every label and the edges between them, on ten million edges (sampled)
12 mina 9.5 GB database with five tenants moved from PostgreSQL 16 to 18 by docker compose up
+1.8 msto record a single write in the change feed, in the same transaction
0 of 840requests failed in the read mix, on either database

One service owns the graph

Consumers call graphos-api over the graphos-net network with a bearer token. graphos-api checks tenant scope and read/write access, validates typed contracts, builds SQL internally, times out bad queries and emits metrics and audit logs. Only graphos-api reaches PostgreSQL 18 with Apache AGE on an internal network; each tenant is its own AGE graph. Consumers call graphos-api over the graphos-net network with a bearer token. graphos-api checks tenant scope and read/write access, validates typed contracts, builds SQL internally, times out bad queries and emits metrics and audit logs. Only graphos-api reaches PostgreSQL 18 with Apache AGE on an internal network; each tenant is its own AGE graph.
Fig 02The boundary: consumers reach graphos-api only, with a bearer token; graphos-api alone reaches the database, on an internal network.
A call to recompute-communities, by a user with maintain on the tenant, streams edges out of PostgreSQL as integer ids with plain SQL, builds a weighted undirected igraph with its vertices in id order, runs Louvain in a separate process with a fixed seed, and matches each new cluster to its predecessor from the last run by Jaccard overlap of at least 0.5 so community ids stay stable. Results are written back with COPY and a single UPDATE, then the community id is denormalised onto every edge so scoped traversals use an index. Only one run per tenant happens at a time, a dry run previews the partition, and the response reports time per step and modularity. A call to recompute-communities, by a user with maintain on the tenant, streams edges out of PostgreSQL as integer ids with plain SQL, builds a weighted undirected igraph with its vertices in id order, runs Louvain in a separate process with a fixed seed, and matches each new cluster to its predecessor from the last run by Jaccard overlap of at least 0.5 so community ids stay stable. Results are written back with COPY and a single UPDATE, then the community id is denormalised onto every edge so scoped traversals use an index. Only one run per tenant happens at a time, a dry run previews the partition, and the response reports time per step and modularity.
Fig 03Communities, computed beside the data: Louvain in its own process, ids kept stable from run to run, written back with COPY (what each algorithm finds).

Explore, edit, query

The Explore view of the demo tenant, an invented software ecosystem: about 450 nodes coloured by the community Louvain found, six clusters around shared core libraries; the selected library's panel shows its community id and the graph metrics graphos computed.
UI 01Explore, on the demo tenant: coloured by community, a core library selected with the metrics graphos computed for it.
The schema diagram of the demo tenant: Library, Framework and Tool in a block under their parent label Package, the selected label's relationships, its properties in a panel (declared, or carried without a definition), and the Add menu for a new label, relationship type, search index or property.
UI 02The schema editor: labels with their kinds, relationship types, and adding to the schema in place.
The Query page of the demo tenant: a saved query's Cypher, coloured, its parameter as a field, the result as a table with each node shown with its label's icon, and the tenant's saved queries beside it.
UI 03The Query page (where the Cypher console is on): Cypher coloured and suggested as you type, a saved query's form, the result with each node's label icon.
A query's result on the canvas, laid out in layers from left to right: a package, the packages it depends on and theirs, each drawn in its label's colour with its icon, with a legend of the labels and their counts.
UI 04A query's result on the canvas, laid out in layers along its edges and drawn with the tenant's label styles.
The Explore canvas of the demo tenant with 31 nodes picked by a loop drawn with Shift-drag, shown in the accent colour; a bar at the foot offers Expand, Hide, Export and Delete for them, and a minimap in the corner frames the part of the graph on screen.
UI 05Many nodes at once: a loop drawn with Shift-drag (or Ctrl / ⌘ click) picks them; the bar expands, hides, exports or deletes them together. A minimap frames the part on screen.
The CSV import dialog: a semicolon-separated file of people mapped to the Person label, its columns as properties with their types, and graphos's check of every row before anything is written: one row refused for an empty id, a repeated id merged, a value that does not read as a number left out.
UI 06Importing a CSV file: the columns mapped to a label's properties, and graphos's check of every row before anything is written.

Run it on your machine

Docker with Compose v2 and about 4 GB of memory for Docker.

git clone https://github.com/pomentum/graphos.git && cd graphos

# 1. Create local secrets: random database passwords
bash scripts/init-secrets.sh          # or: pwsh scripts/init-secrets.ps1

# 2. Start graphos-db (Postgres 18 + AGE 1.8), graphos-api and the web UI (http://127.0.0.1:8081)
docker compose -f docker-compose.graphos.yml --profile ui up -d

# 3. Check it is up
curl -s http://127.0.0.1:8080/readyz

# 4. Sign in as graphos / graphos; the first password must be changed before anything else (uses jq)
TOKEN=$(curl -s -X POST http://127.0.0.1:8080/v1/auth/login -H "content-type: application/json" \
  -d '{"user": "graphos", "password": "graphos"}' | jq -r .access_token)
ADMIN_TOKEN=$(curl -s -X POST http://127.0.0.1:8080/v1/me/password -H "Authorization: Bearer $TOKEN" \
  -H "content-type: application/json" \
  -d '{"current": "graphos", "new": "choose-your-own-password"}' | jq -r .access_token)
  1. Sign in to the UI at http://127.0.0.1:8081 as graphos / graphos and choose a new password: the installation guide.
  2. Register a tenant, write nodes and edges, read them back: the usage guide, or the Python client.
  3. Load the demo tenant to explore: 08_demo_ecosystem.py.

Read further

Installation guide
Installing, the first sign-in, users, single sign-on, production settings, upgrading
Reading graphos's data without graphos
The table layout, reading it with SQL or Cypher, taking it out, loading an export into plain AGE
Upgrading the database
Moving from PostgreSQL 16 / AGE 1.6 (no longer supported) to PostgreSQL 18 / AGE 1.8 with docker compose up
Single sign-on
Registering graphos at an OpenID Connect provider, a Keycloak example, troubleshooting
Graph algorithms
What Louvain, PageRank, betweenness, components and degree find, and what they are for
Usage guide
Practical integration patterns
Connecting an agent (MCP)
Where /mcp is, getting a token, configuring a client, the tools and resources, security
Feature survey
Every capability, briefly
Concept
Architecture, endpoint catalogue, capacity planning
Apache AGE notes
AGE pitfalls, indexing, Postgres tuning, measured numbers
Web UI
Running the UI, its views, how its code and text are organised