Self-host the community engine
Run the open-source datahashi engine locally or in Docker. No external database.
Updated
This page runs the headless engine (hashi-engine). It reads YAML from a folder. It has no control plane, no database, and no tenants. The community binary is hashi. That binary adds a console and a SQLite store. Set ENGINE_MODEL_DIR on hashi to use this same folder.
flowchart TD
E["hashi-engine<br/>model dir $ENGINE_MODEL_DIR:<br/>model.yaml (entities, dimensions, metrics)<br/>warehouse.yaml (runner URL and key)<br/>policy.yaml (optional data roles and RLS)<br/>auth.yaml (optional JWT auth binding)"] -->|"parameterized SQL over HTTP"| QR["query-runner<br/>Postgres · MySQL · Snowflake · BigQuery · and five more"]
To change the model, edit the YAML files. Then restart the process.
Folder structure
Create a directory (for example ./model) with these files.
1. model.yaml (required)
Defines entities, dimensions, measures, relationships, and metrics:
source: local_warehouse
entities:
- name: Customer
table: public.customers
primary_key: id
dimensions:
- name: id
type: string
- name: region
type: string
measures:
- name: customer_count
expr: id
agg: count
metrics:
- name: total_customers
expr: customer_count
2. warehouse.yaml (required)
Tells the engine where Query Runner lives. Both values are environment references, not literals. Mode 1 sets RUNNER_URL to a loopback address. Mode 2 sets it to the runner service.
backend: postgresql
runner_url: "env:RUNNER_URL"
runner_key: "env:RUNNER_KEY"
3. policy.yaml (optional)
Data roles and row-level security. If you omit this file, one role named default grants every entity, every field, and every metric.
roles:
public:
grants:
- kind: metric
target: "*"
- kind: entity
target: "*"
fields: ["*"]
4. config.yaml (Query Runner)
Save this file next to model/. It sets listen, one policy, one key, and one warehouse. Allow every column the model reads.
listen: ":8443"
policies:
local:
default: deny
tables:
public.customers:
effect: allow
fields:
allow: [id, region]
keys:
- id: local
sha256: "95ff67bab710562b07e0e5c6ee95f46a46f856bf10a0773bf8bd6f37cc8fd67d"
policy: local
warehouses:
local_warehouse:
engine: postgresql
dsn_env: QUERY_RUNNER_WAREHOUSE_DSN
schemas: [public]
The hash above is for the key local-runner-key. When you change the key, make a new hash. Copy the first field into keys[].sha256.
printf '%s' 'local-runner-key' | sha256sum
Include mode replaces listen with the loopback address from RUNNER_URL.
These names must match:
- Model
sourceequals the runnerwarehouseskey. runner_sourceinwarehouse.yamlreplacessourcewhen you set it.backendinwarehouse.yamlequals the runner warehouseengine.- The sha256 of the runner key equals
keys[].sha256.
Get the images
Query Runner is published as ghcr.io/datahashi/query-runner. The community engine image is not published yet. Build it from source.
From the hashi repository root:
GITHUB_TOKEN=... docker build --secret id=github_token,env=GITHUB_TOKEN -t hashi:community .
The hashi build needs read access to datahashi/query-runner through GITHUB_TOKEN.
Mode 1 uses the query-runner binary inside hashi:community. Mode 2 runs the published ghcr.io/datahashi/query-runner:0.5.2 image.
Deploy
Run both modes from the directory that contains model/ and config.yaml. Replace QUERY_RUNNER_WAREHOUSE_DSN with your warehouse.
Mode 1: Single container (include-runner)
hashi-engine starts query-runner as a child on the loopback address in RUNNER_URL. Mount the runner config. Set QUERY_RUNNER_CONFIG. The warehouse password stays in the child environment.
docker run -d \
--name hashi-engine \
-p 8081:8081 \
-v "$(pwd)/model:/etc/hashi/model:ro" \
-v "$(pwd)/config.yaml:/etc/query-runner/config.yaml:ro" \
-e ENGINE_MODEL_DIR=/etc/hashi/model \
-e ENGINE_API_KEY=local-secret \
-e ENGINE_INCLUDE_RUNNER=true \
-e QUERY_RUNNER_CONFIG=/etc/query-runner/config.yaml \
-e QUERY_RUNNER_WAREHOUSE_DSN="postgres://user:pass@host:5432/analytics?sslmode=require" \
-e RUNNER_URL=http://127.0.0.1:8443 \
-e RUNNER_KEY=local-runner-key \
hashi:community \
/usr/local/bin/hashi-engine
Mode 2: Docker Compose (separate containers)
Separate the engine from Query Runner. The engine container then has no warehouse password. Set ENGINE_RUNNER_ALLOW_PRIVATE_EGRESS because the runner is on a private Docker network.
services:
query-runner:
image: ghcr.io/datahashi/query-runner:0.5.2
ports:
- "8443:8443"
environment:
QUERY_RUNNER_WAREHOUSE_DSN: "postgres://user:pass@host:5432/analytics?sslmode=require"
volumes:
- ./config.yaml:/etc/query-runner/config.yaml:ro
restart: unless-stopped
engine:
image: hashi:community
command: ["/usr/local/bin/hashi-engine"]
ports:
- "8081:8081"
environment:
ENGINE_MODEL_DIR: /etc/hashi/model
ENGINE_API_KEY: "prod-secret-key"
ENGINE_INCLUDE_RUNNER: "false"
ENGINE_ROLLUP_STORE_DIR: /var/lib/hashi/rollups
ENGINE_RUNNER_ALLOW_PRIVATE_EGRESS: "true"
RUNNER_URL: http://query-runner:8443
RUNNER_KEY: local-runner-key
volumes:
- ./model:/etc/hashi/model:ro
- ./rollups:/var/lib/hashi/rollups
depends_on:
- query-runner
restart: unless-stopped
Verify the deployment
REST query
Use the ENGINE_API_KEY from the mode you started. The command below uses the Mode 2 key. Mode 1 uses local-secret.
curl http://localhost:8081/v1/query \
-H "Authorization: Bearer prod-secret-key" \
-H "Content-Type: application/json" \
-d '{"metrics": ["total_customers"]}'
PostgreSQL wire (psql)
The compose above leaves SQL wire off. Set ENGINE_SQL_ADDR and the two TLS file paths. Mount ./tls on the engine service. SQL wire refuses plaintext. Put a PEM certificate and key in ./tls before you connect. dbname is any string. FROM names an entity.
PGPASSWORD=prod-secret-key psql "host=localhost port=5432 user=local dbname=analytics sslmode=require" -c \
'SELECT total_customers FROM "Customer";'
MCP endpoint
Point any MCP-compatible agent to:
http://localhost:8081/mcp
Next: REST API · Environment variables.