GraphQL API, MCP server, graph sync engine, and usage database for the Hadron knowledge graph platform.
Serves both consumer and enterprise users. On sign-up, each user gets a personal workspace (auto-provisioned org, API key, default bundle) so they can connect their AI tool within minutes. Organizations, teams, and advanced graph management are available when needed.
Graphs are managed through the GraphQL API (via hadron-web) and optionally synced from GitHub. Nodes are stored in PostgreSQL and cloned locally for filesystem operations.
- Node.js 20+
- PostgreSQL 15+ with the pgvector extension
pnpm install
cp .env.example .env # edit DATABASE_URL, JWT_SECRET, HADRON_ENCRYPTION_KEYpnpm run db:push # push schema to database (first time)
pnpm run dev # http://localhost:4000| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
— | Prisma connection string |
DB_PROVIDER |
postgresql |
Leave unset. PostgreSQL is the only supported database (see below). |
JWT_SECRET |
— | HS256 signing secret. Production refuses to boot when it is missing or under 32 UTF-8 bytes (#421) |
HADRON_ENCRYPTION_KEY |
— | AES-256-GCM key for encrypting secrets at rest (32 bytes, hex) |
GRAPHS_PATH |
./graphs |
Root directory for cloned graph repos |
PORT |
4000 |
HTTP listen port |
BASE_URL |
http://localhost:4000 |
Public URL (used for OAuth callback) |
FRONTEND_URL |
http://localhost:5173 |
hadron-web URL (OAuth redirect, CORS) |
GITHUB_CLIENT_ID |
— | GitHub OAuth app client ID |
GITHUB_CLIENT_SECRET |
— | GitHub OAuth app client secret |
GOOGLE_CLIENT_ID |
— | Login-only Google OAuth web client ID |
GOOGLE_CLIENT_SECRET |
— | Login-only Google OAuth web client secret |
GITHUB_WEBHOOK_SECRET |
— | GitHub App webhook verification secret |
RESEND_API_KEY |
— | Resend API key for magic-link emails |
EMAIL_FROM |
Hadron <noreply@hadrongraph.io> |
Sender address for emails |
JWT_EXPIRY |
7d |
JWT token lifetime |
TEAM_ATTENTION_TOKEN_SECRET |
JWT_SECRET |
Optional separate HMAC secret for attention tokens and switchover proofs |
See .env.example for the full list with generation instructions.
Each node is a directory containing a .node/ subdirectory:
myorg/myrepo/
services/
.node/
node.yaml # name, description, tags, outgoing edges
topic.md # markdown content
node.yaml example:
name: services
description: "Service layer"
tags: [backend, api]
nodes:
- loc: models # relative loc of target node
rel: "depends on"The node's loc is derived from its path relative to the graph root, with /
replaced by . (e.g. services/auth → services.auth).
Two token types are accepted via Authorization: Bearer <token>:
- JWT (contains dots) — issued after GitHub OAuth or email magic-link login; carries
sub,roles, and provider-specific claims. - API key (hex string) — created via the GraphQL API with a bcrypt-hashed key.
Three identity providers are supported:
- GitHub OAuth —
GET /auth/githubstarts the flow; user is redirected back with a JWT. - Google OAuth —
GET /auth/googlestarts the login-only OIDC flow when configured. - Email magic link —
POST /auth/emailsends a sign-in link via Resend; clicking it verifies the email and issues a JWT.
Roles: OWNER > ADMIN > CONTRIBUTOR > READER.
Node queries and mutations enforce graph-level access control:
- Users see nodes from their organizations + cross-org subscriptions + public graphs
- Clients see only nodes from graphs in their subscribed bundles
- Writes require CONTRIBUTOR+ role, and for clients: a READ_WRITE bundle subscription
See hadron-concept/hadron-specs/Authorization/Graph Access Control.md for the full spec.
Hadron integrates with GitHub in two ways: OAuth for user authentication and a GitHub App for graph synchronization.
Users log in via the standard GitHub OAuth flow (/auth/github → GitHub → /auth/github/callback). On first login, a User record is created and linked to the user's GitHub login. A JWT is issued for subsequent API calls.
Set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET from a GitHub OAuth App.
Google login is enabled when GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are
set. Create a Web application OAuth client in a login-only Google Cloud
project and register ${BASE_URL}/auth/google/callback as an exact authorized
redirect URI (plus http://localhost:4000/auth/google/callback for local
development). The client requests only openid email profile; do not add Gmail
scopes or reuse the Gmail mailbox-integration project.
Graphs are stored as Git repositories on GitHub. A GitHub App provides authenticated access for cloning and pushing to these repos.
Credentials are stored per-organization in the database (githubAppId, encrypted private key). This allows multi-tenant setups where each org uses its own GitHub App. If no per-org credentials exist, the server falls back to the GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY environment variables.
Authentication flow:
- The server signs a short-lived JWT with the App's private key
- Exchanges it for a 1-hour installation access token via the GitHub API
- Uses the token to clone/push repos over authenticated HTTPS
Private keys are encrypted at rest using AES-256-GCM (see HADRON_ENCRYPTION_KEY). The server handles PKCS#1 → PKCS#8 key format normalization automatically.
The /github/webhook endpoint receives GitHub App events. Payloads are verified using HMAC-SHA256 with GITHUB_WEBHOOK_SECRET.
Installation events:
installation.created— When the App is installed on a GitHub org, the controller matches the GitHub org name to a Hadron organization (by domain) and stores the installation ID.installation.deleted— When the App is uninstalled, the installation ID is cleared.
Push events (auto-sync):
- When a push to the repository's default branch is received, the controller finds all graphs whose
sourceURL matches the repository and triggers a sync. If a graph has areadBranchset, only pushes to that branch trigger a sync.
On startup (and via POST /reload, which requires a platform ADMIN/OWNER bearer token since #556), the controller syncs all graphs that have a sourceUrl:
- Obtains an installation token for the graph's organization
- Clones (or pulls) the repo into
GRAPHS_PATH/<namespace> - Walks the filesystem for
.node/directories - Upserts nodes and edges into PostgreSQL
When a node is modified via the API, the change is written back to the local clone.
When a new user signs up (via GitHub OAuth or email magic link), the controller automatically provisions a personal workspace:
- Creates an
OrganizationwithisPersonal: true - Adds the user as OWNER
- Creates a default "My Graphs" bundle
- Creates a WORKSTATION client with an API key
- Subscribes the client to the default bundle (READ_WRITE)
- Stores the raw API key in a
PendingSetuprecord (shown once on the setup page)
New users are redirected to /app/setup where they download a config file for their AI tool.
Every call to upsertNode snapshots the current node state before applying changes. This creates
a linear version history per node without requiring git branches.
nodeVersions(nodeId, limit)— query version history (most recent first)restoreNodeVersion(versionId)— snapshots current state, then restores the node to a previous version
The publicGraphs query returns all graphs with visibility: PUBLIC. The addToMyGraphs mutation
lets an authenticated user add a public graph to their personal workspace (creates a subscription
and adds the graph to their default bundle).
| Endpoint | Auth | Description |
|---|---|---|
GET /health |
— | Health check |
POST /graphql |
Bearer | GraphQL API (Apollo) |
POST /reload |
Bearer (platform ADMIN/OWNER) | Re-sync all graphs from GitHub. Rate limited to 10/min per source address (#556) |
POST /sessions/:id/outcome |
Bearer (CONTRIBUTOR+ or App key) | Report a session outcome 0.0–1.0 (#556) |
GET /auth/github |
— | Start GitHub OAuth flow |
GET /auth/github/callback |
— | OAuth callback |
GET /auth/google |
— | Start Google OIDC login flow |
GET /auth/google/callback |
— | Google OIDC callback |
POST /auth/email |
— | Send magic-link email |
GET /auth/email/verify |
— | Verify magic link and issue JWT |
GET /auth/me |
Bearer | Current user info |
GET /setup/info |
Bearer | One-time setup info (API key, graphs) |
POST /github/webhook |
HMAC | GitHub App webhook receiver |
POST /mcp |
Bearer | MCP protocol endpoint |
Never run prisma db push or prisma migrate as part of the container entrypoint — if it fails, the container enters a restart loop. Instead, run migrations separately using docker compose run:
docker compose run --rm hadron-server npx prisma db pushThis spins up a fresh container with all env vars from your compose file, runs the command, and exits.
For schema changes that require dropping and recreating (e.g. enum changes), use --force-reset. This wipes all data:
docker compose run --rm hadron-server npx prisma db push --force-resetIf a failed migration or startup error puts the container in a restart loop:
docker compose stop hadron-server
docker compose run --rm hadron-server npx prisma db push --force-reset
docker compose start hadron-serverAfter --force-reset, the database is empty. Re-seed if needed:
docker compose run --rm hadron-server npx prisma db seedTests need a local PostgreSQL with pgvector that the holger role can create
databases on. No setup step is needed: each run clones a template that the
vitest globalSetup builds from an empty database for this checkout's schema
(hadron_test_s<fingerprint>, see src/test/schemaTemplate.ts). A schema
change gets a new template; nothing is pushed over an old one.
For serial mode (HADRON_TEST_PARALLEL=0, a single database and no CREATE
DATABASE), rebuild hadron_test from empty after every schema change:
pnpm run db:test-setupThen run:
pnpm test # run all tests
pnpm run test:watch # watch mode
pnpm run test:coverage # with coverage reportTests use Vitest with pool: 'forks' (one process per file), each worker on its own
cloned database.
| Script | Description |
|---|---|
pnpm run dev |
Start dev server with hot reload (tsx watch) |
pnpm run build |
Compile TypeScript |
pnpm start |
Run compiled server |
pnpm test |
Run all tests |
pnpm run test:watch |
Watch mode |
pnpm run test:coverage |
Coverage report |
pnpm run db:generate |
Regenerate Prisma client |
pnpm run db:push |
Push schema to database |
pnpm run db:migrate |
Run Prisma migrations (PostgreSQL) |
pnpm run db:studio |
Open Prisma Studio |
pnpm run db:test-setup |
Rebuild hadron_test from empty (serial mode only) |
SQLite is not supported. PostgreSQL (with pgvector) is the only database the server runs on. The SQLite schema and the db:use-* scripts that switched to it were removed in #426, because the schema had fallen far behind (13 models against today's 76) and running either script overwrote the live prisma/schema.prisma.
src/integrations/db/provider.ts is a vestige of that toggle: it still reads DB_PROVIDER at startup and, with the default (unset), passes values through unchanged.