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.
disc serve # Start the server
disc start # Start server + bundled PostgreSQL
disc stop # Graceful shutdown
disc status # Show instance status
| 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 |
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();
All server configuration can be set via environment variables. The createServerFromEnv() function reads these at startup.
| 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 |
| 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 (…). |
| 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 |
| 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 |
| 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) |
| 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) |
| Variable | Default | Description |
|---|---|---|
DISC_LOG_FORMAT |
json |
Log format: json or text |
DISC_LOG_LEVEL |
INFO |
Log level: DEBUG, INFO, WARN, ERROR |
| Variable | Default | Description |
|---|---|---|
DISC_ENABLE_METRICS |
false |
Enable the /metrics Prometheus endpoint |
| Variable | Default | Description |
|---|---|---|
DISC_ENABLE_MULTI_DATABASE |
false |
Enable multi-database routing |
| 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. |
| Variable | Default | Description |
|---|---|---|
DISC_SHUTDOWN_DRAIN_TIMEOUT |
30000 |
Max ms to wait for in-flight requests during graceful shutdown. |
| 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]knobscorsAllowCredentials,corsAllowedHeaders,corsAllowedMethods,corsExposeHeaders, andcorsMaxAgeremaindisc.toml-only — they’re project-level defaults rather than per-deployment knobs.maxRequestBodyBytescan be set either way; thedisc.tomlvalue wins overDISC_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 indisc.toml.
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
}
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:
--backend-dsn CLI flag. Explicit external PostgreSQL. Always wins.DATABASE_URL environment variable. Standard 12-factor.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.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.
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 /queryExecute 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/rollbackThe 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 /healthFull 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:
"healthy" -- database connected, pool has capacity."degraded" -- database connected but pool has waiters queued."unhealthy" -- database unreachable or pool closed.GET /health/liveLightweight 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/readyReadiness 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 /statsDetailed 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 /metricsPrometheus-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 |
| 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.
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 |
Extensions register routes under /ext/<extension-name>/<path>. For details, see the Extensions documentation.
The admin UI is served at /ui when enabled. For details, see the Admin UI documentation.
The Disc server supports WebSocket connections for real-time queries and subscriptions. Connect by sending a WebSocket upgrade request to the server URL.
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. UsePOST /queryfor anything that depends on who is asking.
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"
}
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).
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.
Set the binary port via config or environment variable:
DISC_BINARY_PORT=5657
Or programmatically:
const server = new DiscServer({
binaryPassword: "secret",
binaryPort: 5657
});
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.
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:
{id} without a sub-shape), flagged as a link; a multi link, a multi property and a computed set are sets of their element type.id, which the clients hide: in a shape with no fields of its own (select User, a bare insert; a link property is a field, so members: { @role } has no id unless the client asks for ids), and in every object shape, nested ones too, when the client asks for implicit ids — the Python client does, the JS client doesn’t. __tid__ and __tname__ are added the same way when asked for; __tid__ is a stable id of the type, a version 5 UUID of its qualified name (default::User), the same across restarts and servers.@since, flagged as one), a splat ({ * }, the type’s stored properties, id first) and a type-intersection field ([is Circle].radius, empty on other objects) are described with their own types.group answers free objects {key, grouping, elements}: key an object of the key names (by (.a, .b) and grouping sets name each key by its property, a and b), grouping a set of str, elements a set of the element shape. A key of objects (using b := .best by b) is each object, in its {id}; under grouping sets, cube or rollup, a key outside a group’s set is empty.insert, update or delete answers the set of objects it wrote, each its {id}.execute) is described as the null type id, with no result, and a query without parameters describes its input as the null type id.querySingle, queryRequiredSingle and their JSON forms raise ResultCardinalityMismatchError (the query has cardinality MANY which does not match the expected cardinality ONE) for a query that returns more than one element. The result’s cardinality is reported as the query is parsed, as Gel reports it, and a query that may return several — select {1, 2}, a select of objects or an update not known to keep at most one — is refused before it runs, so nothing is written, however many elements it would return. A select keeps at most one with limit 1, as assert_single(…), or with a filter setting .id, an exclusive property, or every pointer of a type’s constraint exclusive on (…) to one value.queryJSON answers the whole result as one JSON array, querySingleJSON the one object (or null), and the JSON_ELEMENTS format one JSON value per element — each described as std::str.Remaining differences from Gel:
select assert_single((select User { name })) is described with its fields, but they decode as null.exclusive constraint on a link is not recognised for cardinality: a filter setting such a link to one object doesn’t make a select single.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.
DISC_ENABLE_MULTI_DATABASE=true
Or programmatically:
const server = new DiscServer({
enableMultiDatabase: true
});
Clients specify the target database using one of these methods (in order of precedence):
X-Database header -- set on the HTTP request.?database= query parameter -- appended to the request URL."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 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.
The server uses a token-bucket rate limiter keyed by client IP address.
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
});
rateLimitRpm / 60 tokens per second.rateLimitBurst tokens. This allows short bursts above the average rate.429 Too Many Requests and a Retry-After: 60 header.Rate limiting is applied before any request processing, including before incrementing request counters or creating connection/session objects.
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"
}
});
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.
The server handles SIGTERM and SIGINT signals for graceful shutdown.
503 Service Unavailable with {"error": "Server is shutting down"}.shutdownDrainTimeout milliseconds (default: 30000).# 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.
When server.start() is called, the following steps execute in order:
jwtSecret is set, creates the auth database, provider, middleware, and route handlers.binaryPort is configured, starts the binary protocol listener.SIGINT and SIGTERM are registered for graceful shutdown.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:
cacheMaxSize).EXPLAIN plan caching (when enableExplain is true).slowQueryThresholdMs).requestTimeout).enableAccessPolicies is true).Opt into the simple handler via:
DISC_PROTOCOL=simple
Or:
const server = new DiscServer({ protocol: "simple" });
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
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 |