The self-hosted query runner for SemiLayer. Dials out to SemiLayer, executes database queries locally, and streams results back. Your database credentials never have to leave your network.
docker run --rm \
-e SEMILAYER_RUNNER_ID=<runner-id> \
-e SEMILAYER_RUNNER_TOKEN=rk_... \
ghcr.io/semilayer/runner:latestOne outbound WebSocket. No inbound ports. Queries come in over the socket; the runner opens the DB connection, runs the query, streams rows back.
┌────────────────────────┐ ┌────────────────────────┐
│ api.semilayer.com │ │ your network │
│ console.semilayer.com │ │ ┌──────────────┐ │
│ runner.semilayer.com │◄───── wss ─────┤ │ runner │ │
│ │ outbound │ │ (this repo) │ │
└────────────────────────┘ only │ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ your DB │ │
│ └──────────────┘ │
└────────────────────────┘
- Zero inbound. Your database never takes a connection from SemiLayer's IPs. The runner dials out over TLS 1.3; SemiLayer never opens a port toward you.
- Airgap-ready. In runner-local credentials mode, SemiLayer never holds your DB URL at all — only the runner knows how to connect. Rotate a password? You do it on your side; SemiLayer was never in the loop.
- Redundant by default. Run two (or twenty) with the same assignments. Dedup is handled by an atomic claim on the gateway — no double-execution, no leader election, no coordination.
- Drop-in swap. Same APIs, same generated Beam clients, same streaming responses. Flipping a source from direct to runner dispatch is one toggle in the Console. The query code on your app side doesn't change.
docker run --rm \
-e SEMILAYER_RUNNER_ID=<runner-id> \
-e SEMILAYER_RUNNER_TOKEN=rk_... \
ghcr.io/semilayer/runner:latestPinning a version is strongly recommended in production:
docker pull ghcr.io/semilayer/runner:0.1
# or a specific patch:
docker pull ghcr.io/semilayer/runner:0.1.4Published tags:
ghcr.io/semilayer/runner:latest— floating, latest published CLIghcr.io/semilayer/runner:<major>.<minor>— floating within a minor (picks up patches)ghcr.io/semilayer/runner:<major>.<minor>.<patch>— immutable
Same bits, if you'd rather not use Docker:
npm install -g @semilayer/runner-cli
semilayer-runner start \
--id <runner-id> \
--token rk_...apiVersion: apps/v1
kind: Deployment
metadata:
name: semilayer-runner
spec:
replicas: 2
selector: { matchLabels: { app: semilayer-runner } }
template:
metadata: { labels: { app: semilayer-runner } }
spec:
containers:
- name: runner
image: ghcr.io/semilayer/runner:0.1
env:
- { name: SEMILAYER_RUNNER_ID, valueFrom: { secretKeyRef: { name: semilayer, key: runner-id } } }
- { name: SEMILAYER_RUNNER_TOKEN, valueFrom: { secretKeyRef: { name: semilayer, key: runner-token } } }
readinessProbe:
httpGet: { path: /health, port: 8080 }
initialDelaySeconds: 5Runners maintain their own presence on the gateway via heartbeat — the /health shim is for the orchestrator.
- Mint a runner. Open Console → Runners → Add runner → name it → copy the
rk_token (shown once). - Create a source. Console → your env → Sources → Connect source. Pick your bridge. Under Credentials location:
- Managed — SemiLayer stores an encrypted connection URL. The runner reads the decrypted creds over the socket at job time.
- Runner-local — no config on the SemiLayer side. You pass the DB URL via
SEMILAYER_SOURCE_<NAME>_URLenv var on the runner container.
- Assign the runner to the source. Back on the runner's row, toggle the source under Assigned sources.
- Start the runner.
docker run(above). Within 5 seconds the runner appears as Online in the Console. - Query. Hit the normal
/v1/query/<lens>endpoint. Rows come back overapi.semilayer.com— but the actual DB connection happened inside your network.
Full walkthrough: semilayer.dev/runners/install.
All configuration is environment variables. Flags work too if you prefer them (see semilayer-runner start --help).
| Env var | Required | Description |
|---|---|---|
SEMILAYER_RUNNER_ID |
yes | From the Console on create. |
SEMILAYER_RUNNER_TOKEN |
yes | rk_..., shown once on create. |
SEMILAYER_GATEWAY_URL |
no | Defaults to wss://runner.semilayer.com/connect. Override for air-gapped or self-hosted. |
SEMILAYER_SOURCE_<NAME>_URL |
runner-local only | Per-source DB URL when credentialsLocation='runner-local'. Source name is upper-snake-cased — source main-db → SEMILAYER_SOURCE_MAIN_DB_URL. |
SEMILAYER_LOG_LEVEL |
no | debug / info / warn / error. Default info. |
SEMILAYER_HEALTH_PORT |
no | Listener port for /health. Default 8080. Set to 0 to disable. |
Managed-credentials mode — SemiLayer encrypts and stores your DB URL. The runner receives it over the authenticated socket at job time, opens a connection, executes the query, disconnects. SemiLayer holds ciphertext + the ability to decrypt; your DB still never takes an inbound connection from SemiLayer's IPs because the runner is what connects.
Runner-local credentials mode — SemiLayer knows a source exists by name + bridge, nothing more. The config column in the SemiLayer DB is literally {}. The runner reads the DB URL from its own environment. If you rotate your DB password, you update the env on the runner and restart — SemiLayer has nothing to rotate, nothing to forget, nothing to leak.
Full details: semilayer.dev/runners/airgap.
rk_tokens are shown once on create. SemiLayer stores only the SHA-256 hash — the plaintext is not recoverable from our side.- Tokens are scoped to a single runner row and can be revoked instantly from the Console; the next heartbeat (~25s) closes any open socket.
- All runner traffic rides TLS 1.3. The runner validates the gateway certificate before sending the token.
- The image runs as non-root (
USER node) and has no shell tooling beyond whatnode:22-slimships. - No SSRF surface, no inbound listeners beyond the optional
/healthport.
Responsible disclosure: SECURITY.md.
| Topic | Link |
|---|---|
| Overview | semilayer.dev/runners |
| Install | semilayer.dev/runners/install |
| Source assignments | semilayer.dev/runners/sources |
| Multiple runners / HA | semilayer.dev/runners/multiple-runners |
| Airgap mode | semilayer.dev/runners/airgap |
| Troubleshooting | semilayer.dev/runners/troubleshooting |
| Security posture | semilayer.dev/runners/security |
This repo is documentation + container artifact metadata for ghcr.io/semilayer/runner. The runner implementation ships as the @semilayer/runner-cli npm package; the Docker image is a node:22-slim base with that package installed globally.
Source for the runner implementation lives in the main SemiLayer repo. Issues for the runner specifically can be filed here; issues that span the platform belong in the main SemiLayer issue tracker.
MIT — see LICENSE.