Server Configuration

The Disc server provides an HTTP/JSON API for executing EdgeQL queries, WebSocket subscriptions, authentication, schema introspection, extension routing, health checks, and Prometheus metrics. It runs on Deno and connects to PostgreSQL (bundled or external) as its storage backend.


Starting the Server

Via CLI

disc serve    # Start the server
disc start    # Start server + bundled PostgreSQL
disc stop     # Graceful shutdown
disc status   # Show instance status

CLI Flags

Flag Description
--enable-access-policies Enable object-level access policies
--enable-auth Enable the auth subsystem
--host <host> Bind address (default: localhost)
--jwt-secret <key> Enable authentication with this signing key
--port <port> HTTP port (default: 5656)
--service-token <tok> Static bearer token for a trusted backend (≥ 32 bytes; bypasses access policies on /query and /transaction/*). Visible in ps — prefer DISC_SERVICE_TOKEN. See Service credential.
--tls-cert <path> Path to TLS certificate for HTTPS
--tls-key <path> Path to TLS private key

Programmatic

import { createServerFromEnv, DiscServer } from "disc/server/server.ts";

// Create from environment variables
const server = createServerFromEnv();
await server.start();

// Or with explicit configuration
const server = new DiscServer({
  databaseUrl: "postgresql://localhost:5432/disc",
  enableAuth: true,
  enableMetrics: true,
  host: "0.0.0.0",
  jwtSecret: "my-secret-key",
  port: 5656,
  protocol: "full",
  rateLimitRpm: 600
});

await server.start();

The createServerFromEnv() function reads all configuration from environment variables and optionally accepts a PostgresInstance (for bundled mode), a parsed Schema, and a list of extensions:

import { PostgresInstance } from "disc/postgres/instance.ts";
import { createServerFromEnv } from "disc/server/server.ts";

const pg = new PostgresInstance({ dataDir: "~/.disc/instances/myapp/data" });
await pg.start();

const server = createServerFromEnv(pg);
await server.start();

Configuration

Environment Variables

All server configuration can be set via environment variables. The createServerFromEnv() function reads these at startup.

Core

Variable Default Description
DATABASE_URL postgresql://localhost:5432/disc PostgreSQL connection string
DISC_HOST localhost Bind address
DISC_MAX_CONNECTIONS 100 Size of the server’s PostgreSQL connection pool (the pool the query handler and open transactions draw from)
DISC_MAX_REQUEST_BODY_BYTES 4194304 (4 MiB) Cap on the POST /query body. A disc.toml [server] max_request_body_bytes takes precedence over the env var. Larger bodies are 413.
DISC_PORT 5656 HTTP listen port
DISC_PROTOCOL full Protocol handler: full (the real EdgeQL compiler, access policies enforced) or simple (an explicit opt-in for development; enforces no policies)
DISC_REQUEST_TIMEOUT 30000 Request timeout in milliseconds

Authentication

Variable Default Description
DISC_ENABLE_ACCESS_POLICIES (none) Enable SDL access policy enforcement
DISC_ENABLE_AUTH (auto) Explicit auth toggle (true/false)
DISC_JWT_SECRET (none) JWT signing secret. Enables auth when set.
DISC_REQUIRE_AUTH false Gate /query, /schema*, /migrations, /stats, /metrics.
DISC_SERVICE_TOKEN (none) Static bearer token for a trusted backend. Honored on /query and /transaction/* only; the caller bypasses every access policy. At least 32 bytes or the server refuses to start. Env/CLI only — never disc.toml. See Service credential.
DISC_READ_ONLY false Reject INSERT/UPDATE/DELETE/CONFIGURE at AST level, including mutations nested in with, for and select (…).

CORS

Variable Default Description
DISC_CORS_ORIGINS (none) Comma-separated allowed origins
DISC_ENABLE_CORS true Enable CORS headers on all responses
DISC_ENABLE_WEBSOCKETS true Enable WebSocket upgrade
DISC_TRUST_PROXY false Honor X-Forwarded-{For,Proto} from a known proxy

TLS

Variable Default Description
DISC_TLS_CERT (none) Path to TLS certificate file
DISC_TLS_CERT_ENV (none) Name of an env var holding the PEM cert (used in lieu of a path; gh/geldata#4547)
DISC_TLS_KEY (none) Path to TLS private key file
DISC_TLS_KEY_ENV (none) Name of an env var holding the PEM key
DISC_TLS_REDIRECT false Redirect HTTP to HTTPS
DISC_TLS_REDIRECT_PORT 80 Port for the HTTP redirect listener

Rate Limiting

Variable Default Description
DISC_RATE_LIMIT_BURST (none) Burst size for the token bucket
DISC_RATE_LIMIT_RPM (none) Requests per minute per IP (disabled if 0)

Caching and Performance

Variable Default Description
DISC_CACHE_MAX_SIZE 1000 Max entries in parse and compilation caches
DISC_EXPLAIN_CACHE_TTL 300000 EXPLAIN plan cache TTL in milliseconds
DISC_SLOW_QUERY_MS 1000 Log queries slower than this threshold (ms)

Logging

Variable Default Description
DISC_LOG_FORMAT json Log format: json or text
DISC_LOG_LEVEL INFO Log level: DEBUG, INFO, WARN, ERROR

Observability

Variable Default Description
DISC_ENABLE_METRICS false Enable the /metrics Prometheus endpoint

Multi-Database

Variable Default Description
DISC_ENABLE_MULTI_DATABASE false Enable multi-database routing

Admin Features

Variable Default Description
DISC_ENABLE_DATA_WATCH true Enable the live-data subscription endpoint at /admin/data-watch. Set false to skip both the trigger bootstrap and the SSE endpoint.
DISC_ENABLE_REST true Enable the schema-derived REST surface at /api/<Type>. Set false to disable.

Shutdown

Variable Default Description
DISC_SHUTDOWN_DRAIN_TIMEOUT 30000 Max ms to wait for in-flight requests during graceful shutdown.

Binary Protocol

Variable Default Description
DISC_BINARY_PORT (none) Port for the Gel-compatible binary protocol
DISC_BINARY_PASSWORD (none) Password for SCRAM-SHA-256 auth on the binary listener; a connection that authenticated with it may run persistent configure
DISC_BINARY_TLS_CERT (none) TLS certificate for the binary listener (required by upstream Gel clients)
DISC_BINARY_TLS_CERT_ENV (none) Name of an env var holding the PEM cert (gh/geldata#4547)
DISC_BINARY_TLS_KEY (none) TLS private key for the binary listener
DISC_BINARY_TLS_KEY_ENV (none) Name of an env var holding the PEM key

The [server] knobs corsAllowCredentials, corsAllowedHeaders, corsAllowedMethods, corsExposeHeaders, and corsMaxAge remain disc.toml-only — they’re project-level defaults rather than per-deployment knobs. maxRequestBodyBytes can be set either way; the disc.toml value wins over DISC_MAX_REQUEST_BODY_BYTES. See Project Context Resolution for the full table. Secrets (JWT, bcrypt rounds, the service token) stay env/CLI-only and never live in disc.toml.

ServerConfig Reference

When constructing a DiscServer programmatically, you pass a DiscServerOptions object. All fields are optional.

interface DiscServerOptions {
  binaryPassword?: string;
  binaryPort?: number;
  cacheMaxSize?: number; // default: 1000
  corsAllowedHeaders?: string[]; // preflight Access-Control-Allow-Headers
  corsAllowedMethods?: string[]; // preflight Access-Control-Allow-Methods
  corsAllowCredentials?: boolean; // emit Access-Control-Allow-Credentials
  corsExposeHeaders?: string[]; // Access-Control-Expose-Headers
  corsMaxAge?: number; // default: 86400 (24 h)
  corsOrigins?: string[];
  databases?: Record<string, string>;
  databaseUrl?: string; // default: "postgresql://localhost:5432/disc"
  dryRun?: boolean; // skip write side-effects (test mode)
  enableAccessPolicies?: boolean;
  enableAuth?: boolean;
  enableCors?: boolean; // default: true
  enableExplain?: boolean; // EXPLAIN plan caching (full protocol only)
  enableMetrics?: boolean; // default: false
  enableMultiDatabase?: boolean;
  enableWebsockets?: boolean; // default: true
  extensions?: Extension[];
  host?: string; // default: "localhost"
  jwtSecret?: string;
  maxConnections?: number; // default: 100 — size of the PostgreSQL pool used by /query and transactions
  maxRequestBodyBytes?: number; // default: 4 MiB (env: DISC_MAX_REQUEST_BODY_BYTES; disc.toml wins)
  port?: number; // default: 5656
  postgresInstance?: PostgresInstance;
  protocol?: "simple" | "full"; // default: "full"; "simple" is an explicit opt-in (no policy enforcement)
  rateLimitBurst?: number;
  rateLimitRpm?: number;
  readOnly?: boolean; // reject INSERT/UPDATE/DELETE/CONFIGURE
  requestTimeout?: number; // default: 30000
  requireAuth?: boolean; // gate data-plane on Authorization header
  schema?: Schema;
  serviceToken?: string; // static bearer for a trusted backend (≥ 32 bytes); env: DISC_SERVICE_TOKEN
  shutdownDrainTimeout?: number; // default: 30000
  slowQueryThresholdMs?: number; // default: 1000
  tls?: {
    certFile: string;
    keyFile: string;
    redirect?: boolean;
    redirectPort?: number;
    reload?: boolean; // hot-reload on file change
    reloadDebounceMs?: number; // debounce window, default 500
  };
  trustProxy?: boolean; // honor X-Forwarded-* headers
}


Connection Resolution

Every Disc server (and CLI command that needs a database connection) resolves the PostgreSQL DSN in this order, taking the first one that yields a value:

  1. --backend-dsn CLI flag. Explicit external PostgreSQL. Always wins.
  2. DATABASE_URL environment variable. Standard 12-factor.
  3. disc.toml project context. Walks up from cwd looking for disc.toml (like git); when found and [database].managed = true, builds a Unix-socket DSN against the managed instance. When [database].backend_dsn = "..." is set, returns that.
  4. Hardcoded fallback: postgresql://localhost:5432/disc_dev.

The resolver lives in lib/project-context.ts (resolveProjectContext walks up; resolveDsn builds the connection string). For commands that auto-start the bundled PostgreSQL (serve, migrate, shell, start), postgres/ensure-running.ts (ensurePgRunning) takes the resolved context and discovers/starts/creates the instance as needed.

disc.toml keys vs env vars vs CLI flags

Setting CLI flag env var disc.toml
Database URL --backend-dsn DATABASE_URL [database] backend_dsn
Managed PG (auto) (auto) [database] managed = true
Instance name (auto from name) (auto) [database] instance_name
Server host --host/-H DISC_HOST [server] host
Server port --port DISC_PORT [server] port
JWT secret --jwt-secret DISC_JWT_SECRET (env/CLI only — secret)
TLS cert/key --tls-cert/key DISC_TLS_CERT[_ENV]/KEY[_ENV] (env/CLI only — secret-adjacent)
Require auth --require-auth DISC_REQUIRE_AUTH [server] require_auth
Read-only --read-only DISC_READ_ONLY [server] read_only
Trust proxy --trust-proxy DISC_TRUST_PROXY [server] trust_proxy
CORS knobs (none) DISC_CORS_ORIGINS [server] enable_cors, cors_origins, cors_allow_credentials
Drain timeout (none) DISC_SHUTDOWN_DRAIN_TIMEOUT (env/CLI only)
Rate limit (none) DISC_RATE_LIMIT_RPM [server] rate_limit_rpm
Binary port --binary-port DISC_BINARY_PORT (env/CLI only)

CLI flags always win over env vars, which always win over disc.toml. Secrets (JWT, TLS keys) deliberately have no disc.toml representation — they belong in env / a secrets manager, not in a tracked file. See CLI → Project Context Resolution for the full disc.toml reference.


HTTP API Endpoints

GET /

Returns server information and a listing of all available endpoints. Useful for service discovery.

Response (200):

{
  "endpoints": {
    "auth": {
      "login": "/auth/login",
      "logout": "/auth/logout",
      "refresh": "/auth/refresh",
      "register": "/auth/register",
      "reset": "/auth/reset",
      "reset_confirm": "/auth/reset/confirm",
      "password": "/auth/password",
      "profile": "/auth/profile",
      "verify": "/auth/verify"
    },
    "health": "/health",
    "healthLive": "/health/live",
    "healthReady": "/health/ready",
    "metrics": "/metrics",
    "query": "/query",
    "schema": {
      "describe": "/schema",
      "type": "/schema/types/:name",
      "types": "/schema/types"
    },
    "stats": "/stats",
    "websocket": "ws://upgrade"
  },
  "name": "Disc Database",
  "protocol": "HTTP/JSON",
  "version": "yyyy.mm.dd"
}

The auth, schema, metrics, and extensions sections only appear when those features are enabled.

POST /query

Execute an EdgeQL query. This is the primary endpoint for all data operations.

Request body:

{
  "query": "select User { email, name } filter .email = <str>$email",
  "variables": { "email": "ada@example.com" }
}
Field Type Required Description
operationName string No Operation name (for multi-query)
query string Yes EdgeQL query string
variables Record<string, unknown> No Query parameters, keyed by name

Variables bind by name, so the key order of variables does not matter. Every $name the query uses must be present, and nothing else may be: a missing variable and an unknown one are both a 400 VALIDATION_ERROR naming the variable (Missing variable: the query uses $b, … / Unknown variable: $typo was provided, …), and nothing reaches the database. An explicit null counts as a value.

bytes variables are base64 strings (or PostgreSQL \x… hex input); anything else is a 400 naming the variable. bytes values in responses are base64 too. See EdgeQL → Bytes.

The body is capped at maxRequestBodyBytes (4 MiB by default; DISC_MAX_REQUEST_BODY_BYTES or disc.toml [server] max_request_body_bytes). Because base64 inflates by 4/3, the default fits roughly 3 MiB of raw bytes per request. Larger bodies are 413.

Optional headers:

Header Description
Authorization: Bearer <token> A user JWT, or the service credential (DISC_SERVICE_TOKEN), which bypasses access policies.
X-Transaction-ID Run the query inside a transaction opened with POST /transaction/begin.
X-Disc-Apply-Access-Policies: false Per-request policy bypass; honored only when the JWT’s roles include admin or superuser.

Successful response (200):

{
  "data": [
    {
      "email": "ada@example.com",
      "name": "Ada"
    }
  ],
  "extensions": {
    "cacheHit": false,
    "compileMs": 3,
    "durationMs": 12,
    "executeMs": 8,
    "parseMs": 1,
    "queryHash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
  }
}

queryHash is the SHA-256 hex digest of the query text (64 characters); it is also the key of the parse, compilation and EXPLAIN caches.

Error response (400):

{
  "errors": [
    {
      "extensions": {
        "code": "COMPILATION_ERROR",
        "phase": "compilation"
      },
      "message": "Unknown type 'Userr'"
    }
  ]
}

Validation error (400):

{
  "errors": [
    {
      "extensions": { "code": "VALIDATION_ERROR" },
      "message": "Query is required and must be a string"
    }
  ]
}

Execution error (400): a statement that PostgreSQL rejects carries the SQLSTATE, plus constraint, table and detail when PostgreSQL sent them, so a client can tell a unique violation (23505) from a foreign-key violation (23503), a serialization failure (40001) or a deadlock (40P01) without matching on the message. Parse, compile and validation errors have no sqlState.

{
  "errors": [
    {
      "extensions": {
        "code": "EXECUTION_ERROR",
        "constraint": "uk_git_ref_program_id_name",
        "detail": "Key (program_id, name)=(…, refs/heads/main) already exists.",
        "durationMs": 4,
        "sqlState": "23505",
        "table": "git_ref"
      },
      "message": "duplicate key value violates unique constraint \"uk_git_ref_program_id_name\""
    }
  ]
}

Timeout (408):

{
  "errors": [
    {
      "extensions": { "code": "TIMEOUT" },
      "message": "Request timed out after 30000ms"
    }
  ]
}

The query endpoint validates requests before execution. Validation checks include:

Response shape by statement kind. Every statement answers with a set: a JSON array, [] when it is empty. A select answers with what it selects — objects for a type, a path ending in a link, or a shape, and the values themselves for anything else, as in Gel: select User.name is ["ann"], select count(User) is [2], select (1, 'a') is [[1, "a"]], select {1, 2} is [1, 2]. A type selected without a shape gives each object’s id and stored properties (Gel gives { "id" }). A bare insert, update or delete, and a for … union (insert …), answers with the set of objects it wrote, [] when it wrote none (an update or delete that matched nothing, an insert that hit its unless conflict target or whose else (update … filter …) kept the existing object). Each object is the stored row — id, stored properties, single links as target ids — where Gel gives only { "id" }; a for … union (insert …) over json_array_unpack/array_unpack/range_unpack gives { "id" } per object. A group answers with one { "key", "grouping", "elements" } object per group (see EdgeQL → GROUP BY). The kind is decided from the query, not from the generated SQL, so it is stable across cache hits. To choose the fields a mutation answers with, wrap it: select (update T filter … set { … }) { id }. See EdgeQL → Selecting over a mutation.

POST /transaction/begin, POST /transaction/commit, POST /transaction/rollback

The wire protocol behind client.transaction(). begin takes an optional JSON body { "isolationLevel": "read committed" | "repeatable read" | "serializable", "readOnly": boolean } and answers { "transactionId": "…" }. Queries join the transaction by sending that id in the X-Transaction-ID header on POST /query; commit and rollback take the same header and answer { "ok": true }.

Transactions are owned by the caller that opened them (the JWT’s user id, or "service" for the service credential); another caller’s query, commit or rollback against that id is 403. Transaction state lives in the server process, so behind a load balancer a client’s transaction requests must be pinned to one replica.

A failed statement poisons the transaction, as in PostgreSQL. Any statement that reaches PostgreSQL and fails — or times out, or crashes the handler — marks the transaction aborted; parse, compile and validation errors do not, because nothing was sent. A commit of an aborted transaction rolls it back, forgets the id and answers 409:

{
  "errors": [
    {
      "extensions": { "code": "TRANSACTION_ABORTED" },
      "message": "Transaction 7f3c… was aborted by a failed statement and has been rolled back"
    }
  ]
}

A COMMIT that PostgreSQL itself rejects (deferred constraint, serialization failure) answers 500 with the EXECUTION_ERROR envelope including sqlState. In both cases the transaction id is gone afterwards: a repeated commit is 404, never { "ok": true }. Unknown id (404), wrong owner (403) and a missing header (400) keep the plain { "error": "…" } body.

GET /health

Full health status including database connectivity, connection pool stats, server uptime, and extension health.

Response (200 when healthy, 503 when unhealthy):

{
  "database": {
    "connected": true,
    "latencyMs": 2
  },
  "extensions": {
    "full-text-search": { "healthy": true },
    "graphql": { "healthy": true }
  },
  "pool": {
    "active": 2,
    "idle": 8,
    "total": 10,
    "waiters": 0
  },
  "status": "healthy",
  "timestamp": "2026-03-20T12:00:00.000Z",
  "uptimeMs": 3600000
}

The status field is one of:

GET /health/live

Lightweight liveness probe. Returns 200 if the server process is running.

{ "status": "alive" }

Use this for Kubernetes liveness probes or load balancer health checks where you only need to know the process is alive.

GET /health/ready

Readiness probe. Returns 200 when the database is connected and the server can accept queries. Returns 503 when the database is unreachable.

{ "status": "healthy" }

Use this for Kubernetes readiness probes to control whether traffic is routed to this instance.

GET /stats

Detailed server statistics for monitoring and debugging.

Response (200):

{
  "cache": {
    "compilation": {
      "evictions": 0,
      "hitRate": 0.8,
      "hits": 1200,
      "misses": 300,
      "size": 300
    },
    "parse": {
      "evictions": 0,
      "hitRate": 0.73,
      "hits": 1100,
      "misses": 400,
      "size": 400
    }
  },
  "connections": {
    "active": 5,
    "http": 40,
    "total": 42,
    "websocket": 2
  },
  "memoryUsage": {
    "external": 1048576,
    "heapTotal": 67108864,
    "heapUsed": 52428800
  },
  "queries": {
    "avgDurationMs": 15.3,
    "bypassed": 12,
    "failed": 20,
    "successful": 1480,
    "total": 1500
  },
  "queryMetrics": {
    "avgCompileMs": 2.1,
    "avgExecuteMs": 12.7,
    "avgParseMs": 0.5,
    "cacheHitRate": 0.8,
    "totalQueries": 1500
  },
  "rateLimit": {
    "activeClients": 12,
    "rejectedCount": 3
  },
  "transactions": {
    "active": 1,
    "committed": 300,
    "rolledBack": 5
  },
  "uptimeMs": 3600000
}

queries.bypassed counts /query requests that ran with access policies bypassed — by the service credential or by an admin’s X-Disc-Apply-Access-Policies: false header. queries.total counts every HTTP request, including the /stats fetch itself. The service token itself never appears in /stats, /config, / or the logs.

GET /metrics

Prometheus-compatible metrics endpoint. Only available when DISC_ENABLE_METRICS=true (or enableMetrics: true in config). Returns text/plain; version=0.0.4 format.

Response (200):

# HELP disc_http_requests_total Total HTTP requests
# TYPE disc_http_requests_total counter
disc_http_requests_total 1500
# HELP disc_http_requests_successful_total Successful HTTP requests
# TYPE disc_http_requests_successful_total counter
disc_http_requests_successful_total 1480
# HELP disc_http_requests_failed_total Failed HTTP requests
# TYPE disc_http_requests_failed_total counter
disc_http_requests_failed_total 20
# HELP disc_query_cache_hits_total Query compilation cache hits
# TYPE disc_query_cache_hits_total counter
disc_query_cache_hits_total 1200
# HELP disc_pool_connections_active Active pool connections
# TYPE disc_pool_connections_active gauge
disc_pool_connections_active 2
# HELP disc_process_memory_heap_used_bytes Process heap memory used
# TYPE disc_process_memory_heap_used_bytes gauge
disc_process_memory_heap_used_bytes 52428800
# HELP disc_uptime_seconds Server uptime in seconds
# TYPE disc_uptime_seconds gauge
disc_uptime_seconds 3600

Available metric families:

Metric Type Description
disc_http_requests_failed_total counter Failed HTTP requests
disc_http_requests_successful_total counter Successful HTTP requests
disc_http_requests_total counter Total HTTP requests
disc_parse_cache_hits_total counter Parse cache hits
disc_parse_cache_misses_total counter Parse cache misses
disc_parse_cache_size gauge Current parse cache size
disc_pool_connections_active gauge Active pool connections
disc_pool_connections_idle gauge Idle pool connections
disc_pool_connections_total gauge Total pool connections
disc_pool_waiters gauge Pool connection waiters
disc_process_memory_external_bytes gauge Process external memory
disc_process_memory_heap_total_bytes gauge Process heap memory total
disc_process_memory_heap_used_bytes gauge Process heap memory used
disc_query_cache_evictions_total counter Compilation cache evictions
disc_query_cache_hits_total counter Compilation cache hits
disc_query_cache_misses_total counter Compilation cache misses
disc_query_cache_size gauge Current compilation cache size
disc_rate_limit_active_clients gauge Active rate limit client buckets
disc_rate_limit_rejected_total counter Rate-limited requests rejected
disc_uptime_seconds gauge Server uptime in seconds

Schema Introspection

Method Path Description
GET /schema Full schema description
GET /schema/types List all object types
GET /schema/types/:name Get details for a specific type

These endpoints are available when a schema is loaded into the server.

Auth Endpoints

Available when jwtSecret is configured and enableAuth is not false. For full details, see the Auth documentation.

Method Path Description
POST /auth/login Login with email/username
POST /auth/logout Logout and revoke session
POST /auth/password Update password
GET /auth/profile Get current user profile
POST /auth/refresh Refresh access token
POST /auth/register Register a new user
POST /auth/reset Request password reset
POST /auth/reset/confirm Confirm password reset with token
GET /auth/verify Verify email address

Extension Endpoints

Extensions register routes under /ext/<extension-name>/<path>. For details, see the Extensions documentation.

Admin UI

The admin UI is served at /ui when enabled. For details, see the Admin UI documentation.


WebSocket Protocol

The Disc server supports WebSocket connections for real-time queries and subscriptions. Connect by sending a WebSocket upgrade request to the server URL.

Connecting

const ws = new WebSocket("ws://localhost:5656");

WebSocket support must be enabled on the server (enableWebsockets: true, the default).

WebSocket queries carry no identity. The upgrade happens before the auth gate and the socket never reads Authorization, so with access policies on, queries sent over a WebSocket compile as an anonymous caller, and neither a user JWT nor the service credential is honored there. Use POST /query for anything that depends on who is asking.

Client-to-Server Messages

All messages are JSON objects with a type field.

Query:

Execute a one-off query and receive the result.

{
  "payload": {
    "query": "select User { email, name }",
    "variables": {}
  },
  "type": "query"
}

Subscribe:

Start a subscription. The server sends data messages whenever results change.

{
  "payload": {
    "id": "sub_12345",
    "query": "select User { email, name }",
    "variables": {}
  },
  "type": "subscribe"
}

Unsubscribe:

Stop receiving updates for a subscription.

{
  "payload": {
    "subscriptionId": "sub_12345"
  },
  "type": "unsubscribe"
}

Server-to-Client Messages

Query result:

{
  "payload": {
    "data": [{
      "email": "ada@example.com",
      "name": "Ada"
    }]
  },
  "type": "query_result"
}

Subscription data:

{
  "id": "sub_12345",
  "payload": [{
    "email": "ada@example.com",
    "name": "Ada"
  }],
  "type": "data"
}

Error:

{
  "payload": { "message": "Invalid message format" },
  "type": "error"
}

Subscription stopped:

{
  "payload": { "subscriptionId": "sub_12345" },
  "type": "subscription_stopped"
}

For a high-level client, use the SDK’s SubscriptionClient (see Client SDK).


Binary Protocol

Disc supports a Gel-compatible binary wire protocol for interoperability with existing Gel/EdgeDB client libraries. This runs on a separate port from the HTTP server.

Enabling

Set the binary port via config or environment variable:

DISC_BINARY_PORT=5657

Or programmatically:

const server = new DiscServer({
  binaryPassword: "secret",
  binaryPort: 5657
});

Authentication

The binary protocol uses SCRAM-SHA-256 authentication. Set a password via binaryPassword in the server options. If no password is set, authentication is not required.

SCRAM authenticates the connection, not a Disc user: with access policies on, every binary-protocol query compiles as an anonymous caller, so it cannot reach a type whose policy depends on global current_user, and the service credential is not honored on this listener. There is currently no way to carry a Disc identity over the binary protocol. The password is, however, the administrator credential for persistent configure (system, database, instance): a connection that authenticated with it may run them, and with no password set no binary connection may. Writes are rejected in read-only mode here too, nested ones included.

Protocol Details

The binary protocol shares the same schema as the HTTP handler. Queries submitted via binary protocol go through the same EdgeQL parser, compiler, and execution pipeline. This means existing Gel client libraries (Python, JavaScript, Go, etc.) can connect to a Disc server.

Results are described and encoded as Gel does:

Remaining differences from Gel:


Multi-Database

Disc can route queries to different PostgreSQL databases within the same cluster. Each named database gets its own connection pool and can have its own schema and migrations.

Enabling

DISC_ENABLE_MULTI_DATABASE=true

Or programmatically:

const server = new DiscServer({
  enableMultiDatabase: true
});

Targeting a Database

Clients specify the target database using one of these methods (in order of precedence):

  1. X-Database header -- set on the HTTP request.
  2. ?database= query parameter -- appended to the request URL.
  3. Default -- falls back to "disc".
# Via header
curl -H "X-Database: analytics" \
     -d '{"query": "select Event { name }"}' \
     http://localhost:5656/query

# Via query parameter
curl -d '{"query": "select Event { name }"}' \
     http://localhost:5656/query?database=analytics

Database Names

Database names must start with a lowercase letter and contain only lowercase letters, digits, and underscores. Disc prefixes all managed PostgreSQL databases with disc_ to avoid collisions with system databases.


Rate Limiting

The server uses a token-bucket rate limiter keyed by client IP address.

Configuration

DISC_RATE_LIMIT_RPM=600   # 600 requests per minute per IP
DISC_RATE_LIMIT_BURST=600 # burst size (defaults to RPM value)

Or programmatically:

const server = new DiscServer({
  rateLimitBurst: 100,
  rateLimitRpm: 600
});

Behavior

Rate limiting is applied before any request processing, including before incrementing request counters or creating connection/session objects.


TLS / HTTPS

Configuration

Provide certificate and key file paths to enable HTTPS:

DISC_TLS_CERT=/path/to/cert.pem
DISC_TLS_KEY=/path/to/key.pem

Or programmatically:

const server = new DiscServer({
  tls: {
    certFile: "/path/to/cert.pem",
    keyFile: "/path/to/key.pem"
  }
});

HTTP-to-HTTPS Redirect

Enable automatic redirect from HTTP to HTTPS:

DISC_TLS_CERT=/path/to/cert.pem
DISC_TLS_KEY=/path/to/key.pem
DISC_TLS_REDIRECT=true
DISC_TLS_REDIRECT_PORT=80

Or:

const server = new DiscServer({
  tls: {
    certFile: "/path/to/cert.pem",
    keyFile: "/path/to/key.pem",
    redirect: true,
    redirectPort: 80
  }
});

When redirect is enabled, the server starts a second HTTP listener on redirectPort that responds to all requests with a 301 Moved Permanently redirect to the HTTPS equivalent.


Graceful Shutdown

The server handles SIGTERM and SIGINT signals for graceful shutdown.

Shutdown Sequence

  1. Signal received -- the server sets a shutting-down flag.
  2. Reject new requests -- all new incoming requests receive 503 Service Unavailable with {"error": "Server is shutting down"}.
  3. Drain in-flight requests -- the server waits for all currently processing requests to complete, up to shutdownDrainTimeout milliseconds (default: 30000).
  4. Stop binary protocol server -- if running, the binary protocol server is shut down.
  5. Stop HTTP server -- the Deno HTTP server is shut down.
  6. Shut down extensions -- all registered extensions are shut down.
  7. Close database registry -- if multi-database is enabled, all database pools are drained.
  8. Close protocol handler -- the main connection pool is drained.
  9. Close auth database -- if authentication is enabled, the auth database connection is closed.

Configuration

# Set drain timeout (default: 30 seconds)

Programmatically:

const server = new DiscServer({
  shutdownDrainTimeout: 10000 // 10 seconds
});

The stop() method is idempotent -- calling it multiple times is safe and only the first call initiates shutdown.


Server Lifecycle

Startup Sequence

When server.start() is called, the following steps execute in order:

  1. Initialize protocol handler -- creates and warms up the PostgreSQL connection pool.
  2. Initialize database registry -- if multi-database is enabled, creates the default database pool.
  3. Initialize authentication -- if jwtSecret is set, creates the auth database, provider, middleware, and route handlers.
  4. Initialize extensions -- registered extensions are initialized with access to the schema and config. Extension functions and types are merged into the protocol handler’s schema.
  5. Start binary protocol server -- if binaryPort is configured, starts the binary protocol listener.
  6. Start HTTP server -- begins accepting HTTP and WebSocket connections.
  7. Register signal handlers -- SIGINT and SIGTERM are registered for graceful shutdown.
  8. Start cleanup intervals -- background tasks begin:

Protocol Handlers

The server supports two protocol handler implementations:

Handler Flag Description
EdgeQLProtocolHandler "full" Full EdgeQL parser, compiler, and SQL generation (the default)
SimpleEdgeQLProtocolHandler "simple" Simulated compilation for development and testing; enforces no access policies. Explicit opt-in only.

full is the default both for disc serve / createServerFromEnv() and for new DiscServer({}) with no protocol given. The simple handler is never selected implicitly — pass protocol: "simple" or set DISC_PROTOCOL=simple. The simple handler also does not decode base64 bytes variables and cannot run select (update …) { … }; treat it as a development aid, not a serving configuration.

The full handler provides:

Opt into the simple handler via:

DISC_PROTOCOL=simple

Or:

const server = new DiscServer({ protocol: "simple" });

CORS

CORS is enabled by default. To restrict allowed origins:

DISC_CORS_ORIGINS="https://app.example.com,https://admin.example.com"

When CORS is enabled, the server responds to OPTIONS preflight requests with appropriate Access-Control-Allow-* headers and a 204 No Content status. The Access-Control-Max-Age header is set to 86400 seconds (24 hours).

To disable CORS entirely:

DISC_ENABLE_CORS=false

Key Exports

From disc/server/server.ts:

Export Description
DiscServer Main server class
DiscServerOptions Constructor options interface
createDefaultConfig() Returns a default ServerConfig
createServerFromEnv() Creates a server from environment variables
HttpServer Low-level HTTP server
EdgeQLProtocolHandler Full EdgeQL protocol handler
SimpleEdgeQLProtocolHandler Simple (simulated) protocol handler
ConnectionManager HTTP/WebSocket connection tracking
SessionManager Session lifecycle management
TransactionManager Transaction state management
DatabaseRegistry Multi-database pool management
BinaryProtocolServer Gel-compatible binary protocol server

From disc/server/types.ts:

Export Description
ServerConfig Full server configuration interface
QueryRequest Query payload (query, variables, operationName)
QueryResponse Response envelope (data, errors, extensions)
QueryError Single query error
SessionContext Session state (id, database, variables)
Connection Connection state (id, type, session, remoteAddr)
Transaction Transaction state (id, sessionId, isolation)
ServerStats Server statistics
HealthStatus Health check result
AuthContext Per-request auth context (userId, roles, JWT claims)
QueryContext Full query context (session, auth, requestId)
ProtocolHandler Protocol handler interface

See Also