Server configuration reference
The laminardb binary reads a TOML configuration file, constructs streaming pipelines, and serves a REST API with Prometheus metrics.
Server and checkpoint defaults are checked against
server config
and engine config.
This page describes the v0.33.0 release. Last updated: 19 October 2026.
Install
v0.33.0 is the current release. Release assets are named
laminardb-server-<target>-v0.33.0.tar.gz (.zip for
x86_64-pc-windows-msvc) for the targets x86_64-unknown-linux-gnu,
aarch64-unknown-linux-gnu, x86_64-unknown-linux-musl,
x86_64-apple-darwin, aarch64-apple-darwin and
x86_64-pc-windows-msvc, with a SHA256SUMS file. The commands
below link to the v0.33.0 tag so that they keep working after later releases.
BASE=https://github.com/laminardb/laminardb/releases/download/v0.33.0
curl -fLO "$BASE/SHA256SUMS"
# Linux x86_64
ASSET=laminardb-server-x86_64-unknown-linux-gnu-v0.33.0.tar.gz
curl -fLO "$BASE/$ASSET"
grep " $ASSET\$" SHA256SUMS | sha256sum -c -
tar xzf "$ASSET"
# macOS Apple silicon (instead of the Linux lines above)
ASSET=laminardb-server-aarch64-apple-darwin-v0.33.0.tar.gz
curl -fLO "$BASE/$ASSET"
grep " $ASSET\$" SHA256SUMS | shasum -a 256 -c -
tar xzf "$ASSET"
# Run the binary (the archive contains the laminardb executable)
./laminardb --config laminardb.toml
# Or build from a source checkout (laminar-server is not published to crates.io)
git clone https://github.com/laminardb/laminardb.git
cd laminardb && git checkout v0.33.0
cargo build --release -p laminar-server
./target/release/laminardb --config laminardb.toml
# Or Docker. The image's built-in config binds 0.0.0.0:8080 and reads
# console_token from LAMINAR_CONSOLE_TOKEN; the server exits without it.
export LAMINAR_CONSOLE_TOKEN="$(openssl rand -hex 32)"
docker run -d -p 8080:8080 -e LAMINAR_CONSOLE_TOKEN \
-v laminardb-data:/var/lib/laminardb \
laminardb/laminardb-server:0.33.0
The image runs laminardb --config /etc/laminardb/laminardb.toml. To add pipelines,
mount your own file there (for example
-v "$PWD/laminardb.toml:/etc/laminardb/laminardb.toml:ro"); keep a
console_token, because a container needs a non-loopback bind. Its built-in
checkpoint directory is file:///var/lib/laminardb/checkpoints. The image sets
LAMINAR_DISABLE_FILE_WATCH=1, so call POST /api/v1/reload after editing a
mounted config. The :latest and :main tags are rolling builds of the main
branch, not releases.
CLI Options
-c, --config <CONFIG> [default: laminardb.toml]
--log-level <LOG_LEVEL> [default: info]
--admin-bind <ADMIN_BIND>
--pgwire-bind <PGWIRE_BIND> Postgres wire bind address (e.g. `127.0.0.1:5433`). Wildcard binds rejected
| Flag | Default | Description |
|---|---|---|
| -c, --config <CONFIG> | laminardb.toml | TOML configuration file path |
| --log-level <LOG_LEVEL> | info | trace, debug, info, warn, error |
| --admin-bind <ADMIN_BIND> | Overrides [server].bind from the config file | |
| --pgwire-bind <PGWIRE_BIND> | Postgres wire bind address (e.g. 127.0.0.1:5433); overrides [server].pgwire_bind, and an empty value disables the listener. A non-loopback address (including 0.0.0.0) is refused unless pgwire_users, pgwire_tls_cert/pgwire_tls_key and pgwire_allow_remote = true are configured. |
[server]
The [server] section configures standalone server behaviors, control plane security, and pgwire network parameters.
| Field | Type | Default | Description |
|---|---|---|---|
| mode | String | "single" | Run mode. "single" (standalone) or "cluster" (multi-node deployment). |
| key_groups | u16 | 256 | Stable hash partitions for keyed aggregates and interval joins. A single-node runtime owns all of them; a cluster distributes them across nodes. |
| bind | String | "127.0.0.1:8080" | HTTP API and Console UI control-plane bind address. |
| delivery | String | "at_least_once" | Pipeline-wide contract: best_effort, at_least_once, or exactly_once. Connector protocols are derived from this value; sinks do not have an independent delivery setting. |
| incremental_emit | bool | true | Query execution policy: emit dirty-only changelog updates for keyed non-windowed running aggregates. |
| datafusion_memory_limit_bytes | usize | 268435456 (256 MiB) | Shared participating DataFusion reservations per DB/node. Positive value required; direct Arrow allocations and process RSS are outside this limit. |
| source_queue_max_bytes | usize | 67108864 (64 MiB) | Shared connector-to-coordinator Arrow queue, including parked input. Each source's waiting batch must fit; value must be positive and at most the engine's MAX_SOURCE_QUEUE_BYTES. |
| pipeline_max_input_buf_batches | usize | 256 | Batch count per graph input port. Set to 0 to disable the count limit. |
| pipeline_max_input_buf_bytes | usize | unset | Optional retained Arrow-byte limit per graph input port; must be positive when set. Fan-out is charged at each port. |
| pipeline_max_managed_state_bytes | usize | 268435456 (256 MiB) | Pipeline-wide charged-byte budget for managed operator state (keyed aggregates, windows, joins). Must be positive when set. |
| temporal_join_idle_history_retention | Duration | unset | Right-side version history kept while a temporal join input is idle. Required when a pipeline contains a temporal join; at least 1ms. |
| source_idle_timeout | Duration | unset | Marks inactive watermarked sources and input channels idle after this duration. Unset disables idle detection; at least 1ms when set. |
| event_time_max_future_skew | Duration | "5m" | Event timestamps further ahead of the wall clock than this do not advance source watermarks. 0 disables the guard. |
| drain_timeout | Duration | checkpoint.timeout + 30s | Cluster graceful-shutdown allowance for the leader to checkpoint and reassign this node's key groups. Must be at least checkpoint.timeout. |
| reference_table_max_rows | usize | 1000000 | Positive live-row limit for each local reference table. |
| reference_table_max_bytes | usize | 268435456 (256 MiB) | Positive retained-memory charge limit for each local reference table. Cluster reference-table plans are restricted. |
| materialized_view_max_rows | usize | 1000000 | Positive live-row limit for each local materialized view. |
| materialized_view_max_bytes | usize | 268435456 (256 MiB) | Positive retained-memory charge limit for each local materialized view. Cluster materialized views are rejected. |
| console_token | String | none | Bearer token for the /api/v1/* and /ws/{name} routes; at least 8 characters. Required when bind is not a loopback address; omitting it on a loopback bind leaves those routes open for local development. |
| diagnostic_read_token | String | none | Cluster only. Read-only bearer token for the local diagnostic routes. Requires a loopback bind and a console_token; both tokens must then be different, unpadded base64url encodings of exactly 32 bytes (43 characters). |
| console_cors_allowed_origins | Array<String> | none | CORS origins allowed to call the control-plane REST/WS endpoints. When unset, the CORS policy is permissive (development only). |
| pgwire_bind | String | none | Postgres wire protocol listener bind address. Disabled if omitted. |
| pgwire_allow_remote | bool | false | Must be true to bind pgwire on a non-loopback address. A remote bind also requires pgwire_users (trust auth never leaves loopback) and pgwire_tls_cert + pgwire_tls_key. |
| pgwire_users | Map | {} | Usernames mapped to a plain-text password (at least 12 characters) or a pre-hashed md5 + 32 lowercase hex value. Non-empty enables MD5 auth; empty means trust auth on loopback only. |
| pgwire_tls_cert | String | none | Path to PEM certificate file for pgwire TLS; set together with pgwire_tls_key. The listener watches the cert, key and client-CA files and reloads them on change. |
| pgwire_tls_key | String | none | Path to PEM private key (PKCS#8/RSA) for pgwire TLS. |
| pgwire_tls_client_ca | String | none | Path to PEM CA bundle; every client must present a certificate chained to it (mTLS). Requires pgwire_tls_cert + pgwire_tls_key. |
| pgwire_max_connections | usize | 256 | Concurrent pgwire session cap; extra connections are closed immediately. Must be greater than 0. |
| pgwire_max_auth_failures_per_min | u32 | 10 | Failed authentications per IP per minute before temporary ban. 0 disables. |
| pgwire_tls_min_version | String | "1.2" | Minimum TLS protocol version ("1.2" or "1.3"). |
Top-Level Config Keys:
| Field | Type | Default | Description |
|---|---|---|---|
| node_id | String | none | Unique node identity. Required when mode = "cluster": startup fails without it (there is no hostname fallback). Changing it requires a restart. |
| sql | String | none | Raw SQL DDL executed before the pipeline starts, as an alternative to the structured sections. Changing it requires a restart. |
Other top-level sections: [supervision] (see Supervision),
[[process_function]] (supervised Python workers; see the
server README),
and [ai] / [models.<name>] (see AI functions).
Unknown keys in [server], [checkpoint], [discovery],
[[source]], [[sink]] and at the top level are rejected at startup.
[server]
mode = "single"
bind = "0.0.0.0:8080"
console_token = "${LAMINAR_CONSOLE_TOKEN}" # required for a non-loopback bind
pgwire_bind = "127.0.0.1:5433"
Cluster Config
When mode = "cluster", the server requires a unique top-level node_id, a [discovery] section with seeds, and a [checkpoint] url on shared object storage (s3://, gs:// or az://). Cluster coordination uses that checkpoint store; there is no separate [coordination] section or embedded Raft service.
Cluster mode rejects best_effort delivery, local paths and file:// checkpoints (peers cannot recover them), and a checkpoint.interval below 100ms. At-least-once requires replayable input and a durable sink. Cluster exactly-once supports Kafka input with either an S3/S3A append-mode Delta Lake sink or a REST-catalog Iceberg append sink backed by S3/S3A storage.
[discovery]
| Field | Type | Default | Description |
|---|---|---|---|
| strategy | String | required | Discovery mode. Either "gossip" (Chitchat p2p membership) or "static". |
| seeds | Array<String> | [] | Gossip address of every node, including this one (e.g. ["10.0.0.1:7946", "10.0.0.2:7946"]). Required for both strategies; expected membership is derived from it. |
| gossip_port | u16 | 7946 | Gossip communications port. |
| advertise_host | String | none | Host or IP this node advertises to peers. If unset: the host part of [server].bind; for a wildcard bind (0.0.0.0, ::) the machine hostname; 127.0.0.1 only if the hostname is empty. |
| failure_domain | String | none | Locality gossiped to peers for placement: a flat label ("rack17") or coarsest-first ;-separated tiers ("region=...;zone=...;rack=..."). |
| placement_isolation_tier | usize | 0 | Locality tier the placement and blast-radius metrics group by (0 = coarsest). |
| cluster_tls_cert | Path | none | PEM certificate for control-plane (barrier/shuffle) mTLS. |
| cluster_tls_key | Path | none | PEM private key paired with cluster_tls_cert. |
| cluster_tls_client_ca | Path | none | CA that signed every peer certificate; verifies both directions. |
| cluster_tls_server_name | String | none | DNS SAN present in every node certificate, checked on connect (peers dial by IP). |
The four cluster_tls_* keys must be set together and each path must exist. They do not protect the HTTP API; terminate HTTP TLS at a trusted proxy.
node_id = "node-1" # unique per node
[server]
mode = "cluster"
bind = "0.0.0.0:8080"
console_token = "${LAMINAR_CONSOLE_TOKEN}" # required for a non-loopback bind
delivery = "at_least_once"
key_groups = 256
[checkpoint]
url = "s3://my-bucket/laminardb/cluster-checkpoints" # shared by every node
[discovery]
strategy = "gossip"
gossip_port = 7946
advertise_host = "10.0.0.1"
seeds = ["10.0.0.1:7946", "10.0.0.2:7946"]
[checkpoint]
| Field | Type | Default | Description |
|---|---|---|---|
| url | String | OS temp/laminardb | Checkpoint storage URL: absolute file://, s3:///s3a://, gs:///gcs://, or az:///abfs[s]:///wasb[s]://. R2 and MinIO use s3:// with an endpoint option. Single-node at_least_once and exactly_once need a local file:// directory; an object-store URL there is rejected with [LDB-0014]. Cluster mode needs an object-store URL. Use an explicit durable location in production. On Windows, keep a file:// directory's path short (for example file:///C:/laminardb/checkpoints): checkpoint files sit several levels below it, and a path over the 260-character Windows limit stops the pipeline at its first checkpoint. |
| interval | Duration | "10s" | Checkpoint interval; must be greater than zero. Supports humantime such as "10s", "1m", or "500ms". Cluster mode has a 100ms minimum. |
| timeout | Duration | "120s" | One absolute end-to-end deadline spanning admission, alignment, capture, durable publication, and completion delivery. |
| storage | Map | {} | Object-store options and credentials, for example aws_region, aws_access_key_id, aws_secret_access_key. Credentials can also come from the provider's standard environment. |
| max_node_data_bytes | u64 | 536870912 (512 MiB) | Maximum participant checkpoint data object and in-flight captured-state admission. Positive value required; separate from live-state limits. |
[checkpoint]
url = "file:///var/lib/laminardb/checkpoints"
interval = "30s"
[checkpoint]
url = "s3://my-bucket/laminardb/checkpoints"
interval = "30s"
[checkpoint.storage]
aws_region = "us-east-1"
aws_access_key_id = "${AWS_ACCESS_KEY_ID}"
aws_secret_access_key = "${AWS_SECRET_ACCESS_KEY}"
A single-node server with replay-capable delivery and an s3://, gs://
or az:// checkpoint fails with [LDB-0014]: a shared cloud checkpoint
namespace is not admitted for one node until its writer lease is term-fenced. A
file:// checkpoint directory is locked by one process at a time.
Memory Limits & Production Tuning
The limits above are independent admission budgets. DataFusion reservations are shared per DB/node; source queue bytes are shared per DB/node; graph input limits apply to each port; reference-table and materialized-view limits apply to each local table or view. They do not sum to a process RSS cap. Connector decoding, direct Arrow allocations, managed state, query results, checkpoint encoding, restore staging and allocator overhead need separate headroom.
Managed operator working state has a default 256 MiB charged-byte budget. Set
[server].pipeline_max_managed_state_bytes to change it (embedded users set the same field
through LaminarConfig or the builder). Its charge is a lower bound, not total resident
memory. Local reference tables and materialized views are unavailable in cluster plans that require
distributed ownership.
Size a deployment
- Run representative peak input, bursts and a fault/restart cycle with the intended SQL and connectors. Measure peak process or container RSS, source lag and checkpoint duration.
- Observe
/metrics:laminardb_cycles_backpressured_total,laminardb_input_buf_bytes,laminardb_managed_state_accounted_bytes,laminardb_checkpoint_size_bytes,laminardb_checkpoints_failed_totalandlaminardb_checkpoint_duration_seconds. Use OS or container RSS (on Linux,laminardb_process_resident_memory_bytes) for the actual memory envelope. - Set container memory requests and limits above the measured peak with room for Arrow and connector scratch, simultaneous old/new state on recovery, and checkpoint capture. The Helm chart defaults to
resources: {}; supply workload-specific values. - If admission saturates, examine batch size and downstream throughput before raising queue or graph-port limits. A result too large for a port after execution faults the pipeline. Size
checkpoint.max_node_data_bytesabove the largest expected participant artifact and verify recovery at that setting.
checkpoint.interval defaults to 10s and trades replay work against checkpoint
traffic. checkpoint.timeout defaults to 120s; keep it above observed attempt
duration. Changes to [server] and [checkpoint] require a restart (see Hot Reload). The main I/O
runtime uses TOKIO_WORKER_THREADS (logical CPU count by default), while streaming compute
uses one dedicated thread. On the default non-MSVC build, jemalloc can be tuned with
MALLOC_CONF at process start; see the
allocator guidance.
Console UI & Control API
The Console UI is a web client that lets you inspect your streaming SQL nodes, view lineage dependency graphs, and author queries. You can access the hosted console at laminardb.github.io/laminardb-console-ui.
The console connects to the control-plane APIs exposed by the server. When server.console_token is configured, every /api/v1/* route and /ws/{name} requires it. /health, /ready and /metrics are always public so probes and scrapers work without a token; restrict network access to them.
Security & Gating
- HTTP Authentication: Send the configured token in the header:
Authorization: Bearer <token>. Duplicate or comma-joined bearer values are refused. - WebSocket Authentication: Browsers cannot set headers on the upgrade request, so
/ws/{name}also accepts?token=<token>. The query parameter is accepted only on/ws/routes. - CORS Gating: Restrict client origins using the
server.console_cors_allowed_originsarray in TOML. Without it the CORS policy is permissive. - TLS: The HTTP listener has no TLS settings; terminate TLS at a trusted proxy.
Lineage Dependency Graph
GET /api/v1/graph returns the pipeline lineage (sources → streams → sinks). node_type is "Source", "Stream" or "Sink"; sql is the node's SQL or null.
{
"nodes": [
{ "name": "trades", "node_type": "Source", "sql": null },
{ "name": "vwap", "node_type": "Stream", "sql": "SELECT ..." },
{ "name": "trade_archive", "node_type": "Sink", "sql": null }
],
"edges": [
{ "from": "trades", "to": "vwap" },
{ "from": "vwap", "to": "trade_archive" }
]
}
WebSocket Subscription Protocol
Connect to GET /ws/{name}, where name is a stream; add ?as_of_epoch=N to start from a committed epoch instead of the tail. Each text frame is a JSON object with a type. subscription_id is the stream name, and all counters are sent as decimal strings. Data frames carry at most 1 MiB, so a large batch is split across frames using row_offset/row_count:
{
"type": "data",
"subscription_id": "vwap",
"data": [ { "symbol": "AAPL", "vwap": 178.5 } ],
"sequence": "42",
"log_sequence": "17",
"row_offset": "0",
"row_count": "1"
}
Other frame types: progress (epoch, checkpoint_id, log_sequence, through_log_sequence, sequence),
gap (the subscriber lagged; code = subscription_lagged, skipped_messages) and
error (code, message). In cluster mode data frames also carry stream_generation, partition, partition_sequence and committed_epoch.
The server sends a WebSocket ping every 15 seconds and closes the connection after two unanswered pings. The server accepts at most 10,000 WebSocket connections.
Cluster Management Endpoints
GET /api/v1/cluster/nodes— Cluster members:id,name,rpc_address,state(Joining,Active,Suspected,Draining,Left),metadataandlast_heartbeat_ms.GET /api/v1/cluster/vnodes— The durable key-group (vnode) assignment snapshot.GET /api/v1/cluster/leader— The current leader's node record (if known) and whether this node is the leader.GET /api/v1/cluster/checkpoints— Latest checkpoint status; also available in single-node mode.GET /api/v1/cluster/topologyand/api/v1/cluster/topology/*— Durable catalog version, topology validation, migration submission, preparation, status and legacy-catalog adoption (see the REST table below).
/api/v1/cluster, /nodes, /vnodes and /leader return 404 when the server is not in cluster mode.
REST API
The server exposes an HTTP API on the configured bind address. All responses are JSON except /metrics (Prometheus text format). The first three routes are public; the rest require the console token when one is configured. Until startup completes, and in cluster mode during coordinated recovery, routes other than the three probes return 503.
Probes and metrics (public)
200 unless the pipeline is Stopped (then 503); includes version and pipeline state
200 only while the pipeline is Running
Catalog, SQL and operations
success, checkpoint_id, epoch, duration_ms
POST /api/v1/sql takes {"sql": "..."} and returns at most 1000 rows, stopping after a 5 second collection budget; a capped result sets "truncated": true. Use a WebSocket or pgwire subscription for unbounded results.
Pipeline control
pipeline_state, plus last_error after a fault
These routes act on the whole server, not on one named pipeline. In cluster mode, start and stop run locally first and are then forwarded to every peer; add ?local=true to act on this node only.
Cluster
The two local-* diagnostic routes accept the console token or the read-only
server.diagnostic_read_token, allow one request at a time (at most 8 starts per
second), and are outside the console CORS policy. See the
server README
for the topology migration workflow.
Hot Reload
Edit the TOML config file while the server is running. A file watcher detects changes
(500ms debounce, content-hash comparison), diffs the old and new configs, and applies
incremental DDL. Or trigger manually via POST /api/v1/reload; a second reload while one
is running returns 409.
What reloads without restart: [[source]], [[lookup]], [[pipeline]] and [[sink]] entries (add, remove, or change).
What requires restart: [server], [checkpoint], [discovery], [supervision], [[process_function]], [ai], [models], the top-level sql string and node_id. The reload response carries a warning for each of these that changed.
Reload does not touch certificates. The pgwire listener watches its own pgwire_tls_cert, pgwire_tls_key and pgwire_tls_client_ca files and swaps them in when they change; a failed reload keeps the previous certificate.
Reload order: remove changed/deleted sinks → pipelines → lookups → sources,
then recreate sources → lookups → pipelines → sinks.
Disable file watcher with LAMINAR_DISABLE_FILE_WATCH=1.
Prometheus Metrics
GET /metrics returns Prometheus text format (version 0.0.4) and needs no token. Every metric
name carries the laminardb_ prefix and the constant labels instance (hostname)
and pipeline (the first [[pipeline]] name, or default). A representative set:
| Metric | Type | Description |
|---|---|---|
| laminardb_events_ingested_total | counter | Events ingested from sources |
| laminardb_events_emitted_total | counter | Events emitted to streams |
| laminardb_events_dropped_total | counter | Events dropped |
| laminardb_events_null_timestamp_total | counter | Source rows dropped because the event-time column was null |
| laminardb_window_late_dropped_total | counter | Window assignments dropped past allowed lateness |
| laminardb_cycles_total | counter | Normal processing cycles completed |
| laminardb_cycles_backpressured_total | counter | Cycles skipped by backpressure |
| laminardb_batches_total | counter | Batches processed |
| laminardb_cycle_duration_seconds | histogram | Normal processing-cycle duration |
| laminardb_operator_process_duration_seconds | histogram | Per-operator processing time (labels: operator, mode) |
| laminardb_pipeline_watermark | gauge | Global pipeline watermark |
| laminardb_source_watermark_ms | gauge | Per-source watermark in epoch ms (label: source) |
| laminardb_source_idle | gauge | 1 if the source is idle and excluded from the watermark minimum (label: source) |
| laminardb_stream_watermark_ms | gauge | Per-stream watermark in epoch ms (label: stream) |
| laminardb_input_buf_bytes | gauge | Per-stream input buffer bytes (label: stream) |
| laminardb_managed_state_accounted_bytes | gauge | Operator-reported retained-state charge, a lower bound (labels: operator, phase) |
| laminardb_mv_updates_total | counter | Materialized view updates |
| laminardb_mv_bytes_stored | gauge | Approximate materialized view bytes stored |
| laminardb_checkpoints_completed_total | counter | Completed checkpoints |
| laminardb_checkpoints_failed_total | counter | Failed checkpoints |
| laminardb_checkpoint_epoch | gauge | Current checkpoint epoch |
| laminardb_checkpoint_size_bytes | gauge | Last checkpoint size |
| laminardb_checkpoint_duration_seconds | histogram | Checkpoint cycle duration |
| laminardb_sink_write_failures_total | counter | Sink write errors |
| laminardb_sink_write_timeouts_total | counter | Sink write timeouts |
| laminardb_sink_precommit_duration_seconds | histogram | Sink pre-commit latency |
| laminardb_lookup_cache_hits_total | counter | On-demand lookup cache hits (label: table) |
| laminardb_lookup_cache_misses_total | counter | On-demand lookup cache misses (label: table) |
| laminardb_lookup_source_errors_total | counter | On-demand lookup source fetch errors (label: table) |
| laminardb_pipeline_faults_total | counter | Compute-thread crashes that faulted the pipeline |
| laminardb_pipeline_restarts_total | counter | Automatic recover-from-checkpoint restarts after a fault |
| laminardb_reload_total | counter | Config reload count |
| laminardb_uptime_seconds | gauge | Server uptime in seconds |
| laminardb_ws_connections | gauge | Active WebSocket connections |
On Linux the standard process collector is registered under the same prefix
(for example laminardb_process_resident_memory_bytes). Cluster nodes add
laminardb_cluster_subscription_* and placement metrics, and connectors register their own
metrics when they start (for example laminardb_kafka_source_records_polled_total).
[[source]]
Each [[source]] becomes CREATE SOURCE name (columns, PRIMARY KEY, WATERMARK) FROM CONNECTOR (properties) [FORMAT fmt]. Unknown fields are rejected.
| Field | Type | Default | Description |
|---|---|---|---|
| name | String | required | Unique source name. Referenced in SQL and sinks. |
| connector | String | required | "kafka", "postgres-cdc", "mongodb-cdc", "nats", "websocket", "files", "delta-lake", "iceberg", "otel", "generator" |
| format | String | none | Emitted as the FORMAT clause. When omitted there is no FORMAT clause and the connector's own default applies (Kafka, NATS and WebSocket use JSON). A format key under properties is rejected. |
| properties | Table | {} | Connector options; see the connector sections below. Values must be flat (strings, numbers, booleans, or arrays, which are joined with commas); quote dotted keys such as "bootstrap.servers". |
| schema | Array | [] | Column definitions: {name, type, nullable}; nullable defaults to true. Can be omitted for connectors with a fixed or resolvable schema. |
| primary_key | Array<String> | [] | Emitted as PRIMARY KEY (...), for keyed sources such as CDC document mode or the right side of a temporal join. |
| watermark | Section | none | Event-time watermark: {column, max_out_of_orderness} (default "5s"). The generated DDL uses whole seconds, so sub-second values round down. The column must be a TIMESTAMP. |
[[source]]
name = "trades"
connector = "kafka"
format = "json"
[source.properties]
"bootstrap.servers" = "localhost:9092"
topic = "market-trades"
"group.id" = "laminar-analytics"
"startup.mode" = "earliest" # required under at_least_once / exactly_once
[[source.schema]]
name = "symbol"
type = "VARCHAR"
nullable = false
[[source.schema]]
name = "price"
type = "DOUBLE"
[[source.schema]]
name = "ts"
type = "TIMESTAMP"
[source.watermark]
column = "ts"
max_out_of_orderness = "5s"
[[pipeline]]
Each [[pipeline]] becomes CREATE STREAM name AS sql. It has two fields; other keys are ignored.
| Field | Type | Default | Description |
|---|---|---|---|
| name | String | required | Unique pipeline (stream) name |
| sql | String | required | The SELECT query. Multi-line supported with triple-quoted strings. |
[[sink]]
Each [[sink]] becomes CREATE SINK name FROM pipeline INTO CONNECTOR (properties) [FORMAT fmt]. Unknown fields are rejected.
| Field | Type | Default | Description |
|---|---|---|---|
| name | String | required | Unique sink name |
| pipeline | String | required | The [[pipeline]] (or process-function output) this sink reads from. Must exist. |
| connector | String | required | "kafka", "postgres-sink", "mongodb-sink", "nats", "delta-lake", "iceberg", "websocket", "files" |
| format | String | none | Emitted as the FORMAT clause; required by the files sink, rejected by the WebSocket sink. A format key under properties is rejected. |
| properties | Table | {} | Connector-specific properties (flat values, as for sources) |
Delivery is configured once at [server].delivery. A per-sink delivery field is rejected, and a delivery.guarantee property is owned by the runtime and cannot be set, so a pipeline cannot silently mix checkpoint semantics.
[[lookup]]
Each [[lookup]] becomes CREATE LOOKUP TABLE name (columns, PRIMARY KEY (...)) WITH ('connector' = ..., 'strategy' = ..., properties).
| Field | Type | Default | Description |
|---|---|---|---|
| name | String | required | Unique lookup table name. Used in SQL JOINs. |
| connector | String | required | "postgres", "delta-lake", "iceberg" (replicated or on-demand), "mongodb" (on-demand only), "static" (replicated only) |
| strategy | String | set explicitly | "replicated" (load the table and keep it in memory) or "on-demand" (fetch keys from the connector through a bounded cache). Always set it: in v0.33.0 a [[lookup]] without strategy fails at startup. On-demand is rejected under exactly_once. |
| schema | Array | required | At least one [[lookup.schema]] column {name, type, nullable} |
| primary_key | Array<String> | required | Exactly one column, declared NOT NULL (nullable = false) |
| properties | Table | {} | Connector options, plus pushdown: "auto" (default), "enabled" or "disabled" |
| cache.size_bytes | u64 | 104857600 | [lookup.cache]; on-demand only. A non-default value is passed as cache.memory. |
| cache.ttl | Duration | "300s" | [lookup.cache]; on-demand only. A non-default value is passed as cache.ttl in whole seconds. |
With strategy = "replicated", a non-default [lookup.cache] value is rejected ("cache.memory and cache.ttl require strategy = 'on-demand'"). There is no cache.hybrid option, and cache.disk is rejected. Keys placed directly under [[lookup]] other than the fields above are ignored, so put pushdown under [lookup.properties].
[[lookup]]
name = "instruments"
connector = "postgres"
strategy = "replicated"
primary_key = ["symbol"]
[lookup.properties]
host = "db.internal"
database = "reference_data"
user = "reader"
password = "${PG_PASSWORD}"
table = "public.instruments"
[[lookup.schema]]
name = "symbol"
type = "VARCHAR"
nullable = false
[[lookup.schema]]
name = "exchange"
type = "VARCHAR"
Connectors
In Rust builds, include the connectors you need with Cargo features, for example cargo add laminar-db --features kafka,delta-lake. These are build options, not runtime approval gates. The v0.33.0 server binaries and Docker image include every connector below.
Set delivery once at [server].delivery (or LaminarDbBuilder::delivery_guarantee when embedded). The runtime injects the selected protocol and owns checkpoint/committer identities; connector properties do not require public writer IDs.
Set the serialization format with the SQL FORMAT clause (... FROM KAFKA (...) FORMAT JSON) or the TOML format field. A 'format' connector option is rejected: "option 'format' is unsupported; declare the format with the FORMAT clause". Format-specific options go in FORMAT fmt WITH (...).
RabbitMQ, IBM MQ, FIX and FpML connectors are optional build features and are not in default builds or the Docker image. The v0.33.0 x86_64 Linux (glibc) binary includes all four, the x86_64 Linux (musl) binary none, and the other binaries FIX and FpML.
Kafka Source
Formats (FORMAT clause / TOML format): json (used when omitted), csv, raw, debezium, avro. Avro needs schema.registry.url; the registry supports Avro only.
Required
| Property | Description |
|---|---|
| bootstrap.servers | Kafka broker addresses (comma-separated) |
| group.id | Consumer group ID |
| topic or topic.pattern | Comma-separated topic list, or a regex pattern. If both are set, topic.pattern wins. A pattern requires best_effort delivery and is rejected in cluster mode; guaranteed delivery needs an explicit topic list. |
Startup position
| Property | Default | Description |
|---|---|---|
| startup.mode | group-offsets | group-offsets, earliest, latest. at_least_once and exactly_once reject group-offsets (another group member could move that cursor) and reject latest on a fresh start, so set earliest or one of the two keys below. |
| startup.specific.offsets | Explicit start positions, partition:offset,... (e.g. 0:100,1:200). Takes precedence over startup.mode; needs an explicit topic list. | |
| startup.timestamp.ms | Start from a timestamp (ms since epoch). Takes precedence over startup.mode. | |
| auto.offset.reset | earliest | earliest, latest, none; used with group-offsets when the group has no committed offset |
Schema Registry
| Property | Default | Description |
|---|---|---|
| schema.registry.url | Confluent Schema Registry URL; required for avro, rejected for other formats. Must not embed credentials. | |
| schema.registry.username | Basic auth; set together with the password | |
| schema.registry.password | Basic auth; set together with the username | |
| schema.registry.ssl.ca.location | CA bundle for the registry (also schema.registry.ssl.certificate.location, schema.registry.ssl.key.location) |
Security
| Property | Default | Description |
|---|---|---|
| security.protocol | plaintext | plaintext, ssl, sasl_plaintext, sasl_ssl |
| sasl.mechanism | PLAIN, SCRAM-SHA-256, SCRAM-SHA-512, GSSAPI (only where librdkafka reports sasl_gssapi; Linux builds do not include Cyrus SASL). Required for the sasl_* protocols. OAUTHBEARER is rejected. | |
| sasl.username | SASL username (PLAIN, SCRAM) | |
| sasl.password | SASL password (PLAIN, SCRAM) | |
| ssl.ca.location | CA certificate path | |
| ssl.certificate.location | Client certificate path (with ssl.key.location and ssl.key.password) |
Consumer Tuning
| Property | Default | Description |
|---|---|---|
| latency.profile | balanced | low_latency, balanced, throughput; supplies batching defaults that explicit options override |
| max.poll.records | 1000 | Records per poll batch (256 with low_latency, 8192 with throughput) |
| isolation.level | read_committed | read_committed, read_uncommitted. Guaranteed delivery requires read_committed. |
| partition.assignment.strategy | range | range, roundrobin, cooperative-sticky |
| metadata.columns | Append any of: key, topic, partition, offset, timestamp, timestamp_type, headers |
Properties prefixed with kafka. are passed to librdkafka with the prefix removed, except protected keys, which fail with "overrides a setting the Kafka connector owns; use the connector option for it instead". Protected keys are everything the connector sets itself (for example kafka.bootstrap.servers, kafka.group.id, kafka.enable.auto.commit, kafka.isolation.level), security and TLS settings, transaction and idempotence settings, kafka.partitioner, and sasl.kerberos.* / sasl.oauthbearer.*.
Kafka Sink
Formats: json (used when omitted), csv, raw, avro. debezium is source-only.
| Property | Default | Description |
|---|---|---|
| bootstrap.servers | required | Kafka broker addresses |
| topic | required | Target topic |
| key.column | Column(s) for the record key; required for envelope = 'upsert' | |
| key.format | native | native, text, kafka, json |
| envelope | append | append or upsert. Upsert writes each key to murmur2(key) % partition.count and a NULL value on delete; it needs key.column, partition.count and the key-hash partitioner. |
| partition.count | Topic partition count; required for upsert | |
| partitioner | key-hash | key-hash, round-robin, sticky |
| commit.topic | Compacted topic for commit markers. Required for exactly_once and rejected otherwise; must differ from topic. Exactly-once Kafka output is embedded/single-node only; cluster Kafka output stays at-least-once. | |
| schema.registry.url | Required for avro; rejected for other formats | |
| latency.profile | balanced | low_latency, balanced, throughput |
| compression.type | none | none, gzip, snappy, lz4, zstd (default lz4 under throughput). zstd needs the kafka-zstd build feature, which default builds (including the Docker image) do not include; the v0.33.0 release workflow builds the x86_64 Linux (gnu) binary with --all-features, which does. snappy is not built on Windows. |
| linger.ms | 5 | Batch linger time (0 with low_latency, 50 with throughput) |
| batch.size | 16384 | Max batch bytes (1048576 with throughput) |
| delivery.timeout.ms | 120000 | Per-record delivery deadline, 1 to 300000 |
| transaction.timeout.ms | 60000 | Exactly-once transaction timeout, up to 900000 |
The sink always uses idempotent production with acks=all; acks is not configurable. Under exactly_once, output becomes visible to read_committed readers once per committed checkpoint.
NATS Source
Formats (FORMAT clause / TOML format): json (used when omitted), csv, raw. The source is ephemeral, so it needs best_effort delivery; it is rejected under at_least_once, the server default.
| Property | Default | Description |
|---|---|---|
| servers | required | NATS server URLs (comma-separated) |
| mode | jetstream | jetstream, core |
| auth.mode | none | none, user_pass, token, creds_file. Credentials with none are rejected. |
| user | Auth username (required for user_pass) | |
| password | Auth password (required for user_pass) | |
| token | Auth token (required for token) | |
| creds.file | Path to creds file (required for creds_file) | |
| tls.enabled | false | Enable TLS |
| tls.ca.location | CA cert path | |
| tls.cert.location | Client certificate path | |
| tls.key.location | Client key path | |
| stream | NATS JetStream stream name (required in jetstream mode) | |
| subject | Subscription subject. Required in core mode; in jetstream mode set subject or subject.filters. | |
| subject.filters | Comma-separated JetStream subject filters; used instead of subject when both are set | |
| consumer | Pull consumer durable name (required in jetstream mode; ephemeral consumers are not supported) | |
| deliver.policy | all | all, new, by_start_sequence, by_start_time |
| start.sequence | Start sequence (required if deliver.policy=by_start_sequence) | |
| start.time | Start RFC3339 timestamp (required if deliver.policy=by_start_time) | |
| ack.policy | explicit | explicit, none |
| ack.wait.ms | 60000 | Ack wait timeout in milliseconds |
| max.deliver | 5 | Max deliveries; -1 for unlimited |
| max.ack.pending | 10000 | Max outstanding unacknowledged messages; -1 for unlimited |
| fetch.batch | 500 | Batch size for fetch operations |
| fetch.max.wait.ms | 500 | Max wait for fetch operations in milliseconds |
| queue.group | Queue group (core mode only) |
JetStream messages receive confirmed acknowledgements asynchronously after successful deserialization; acknowledgement failures and timeouts leave them eligible for broker redelivery. NATS source progress is not checkpoint-coupled, and both JetStream and Core modes are admitted only as ephemeral/best-effort sources.
NATS Sink
Formats: json (used when omitted), csv, raw. Core mode is ephemeral (best_effort only); JetStream mode supports at_least_once. Neither supports exactly_once.
| Property | Default | Description |
|---|---|---|
| servers | required | NATS server URLs (comma-separated) |
| mode | jetstream | jetstream, core |
| auth.mode | none | none, user_pass, token, creds_file |
| user | Auth username (required for user_pass) | |
| password | Auth password (required for user_pass) | |
| token | Auth token (required for token) | |
| creds.file | Path to creds file (required for creds_file) | |
| tls.enabled | false | Enable TLS |
| tls.ca.location | CA cert path | |
| tls.cert.location | Client certificate path | |
| tls.key.location | Client key path | |
| stream | JetStream target, required in JetStream mode and forbidden in Core mode; file storage, three replicas, and publish acknowledgements are validated at startup | |
| subject | Literal publishing subject. Exactly one of subject and subject.column is required. | |
| subject.column | Per-row subject column name. Exactly one of subject and subject.column is required. | |
| dedup.id.column | Column used for JetStream message deduplication IDs; jetstream mode with stream only | |
| min.duplicate.window.ms | 120000 | Minimum duplicate window in milliseconds |
| max.pending | 4096 | Maximum pending publish acknowledgements (1 to 4096) |
| ack.timeout.ms | 30000 | Ack timeout in milliseconds (at most 300000) |
| header.columns | Comma-separated column names to map to NATS headers |
PostgreSQL CDC Source
Reads one table through logical replication (pgoutput, PostgreSQL 17+) as typed keyed upserts or a retractable __weight changelog, with an optional initial snapshot handed off at the slot's consistent point. The table needs a primary key and REPLICA IDENTITY FULL, and the publication must be FOR TABLE exactly that table. The slot advances only after a checkpoint commits. Embedded and single-node server mode, at-least-once; cluster mode and exactly-once reject it. TRUNCATE and table or publication changes stop the source with reset guidance.
| Property | Default | Description |
|---|---|---|
| host | required | PostgreSQL hostname |
| port | 5432 | Port |
| database | required | Database name |
| username | postgres | Auth username |
| password | Auth password | |
| slot.name | required | Prefix (at most 46 bytes) of the replication slot the source creates after its first checkpoint commits, named <slot.name>_<claim id>; other slots under the prefix are reported, never used or dropped |
| publication | required | Publication name |
| ssl.mode | verify-full | verify-full or explicit plaintext disable |
| ssl.ca.cert.path | Optional PEM CA bundle for verify-full; web PKI roots are used by default | |
| table | required | Captured table as schema.table |
| snapshot.mode | initial | initial copies the table, then streams every later change; never streams changes after the slot is created |
| output.mode | upsert | upsert: current rows and key-only deletes for keyed sinks. changelog: Z-set rows with a trailing __weight BIGINT NOT NULL for filters and COUNT/SUM/AVG with EMIT CHANGES; needs snapshot.mode=initial |
| max.buffered.bytes | 268435456 | Total connector-owned payload budget across raw WAL, decoded transactions, and Arrow construction (1 MiB to 4 GiB) |
PostgreSQL Sink
Unknown properties are rejected. The sink is durable at-least-once in every mode; exactly_once rejects it.
| Property | Default | Description |
|---|---|---|
| hostname | required | PostgreSQL hostname |
| port | 5432 | Port |
| database | required | Database name |
| username | required | Auth username |
| password | Auth password | |
| table.name | required | Target table |
| schema.name | public | PostgreSQL schema |
| write.mode | append | append (binary COPY) or upsert (INSERT ... ON CONFLICT DO UPDATE) |
| primary.key | PK columns for upsert (comma-separated). Required if write.mode=upsert. | |
| changelog.mode | false | true applies Z-set records (split by _op) as inserts and deletes; requires write.mode = upsert |
| ssl.mode | verify-full | verify-full or explicit plaintext disable |
| ssl.ca.cert.path | PEM CA bundle for verify-full; web PKI roots are used by default | |
| flush.interval.ms | 250 | Maximum time to buffer before flushing |
| connect.timeout.ms | 10000 | Connection deadline |
| statement.timeout.ms | 30000 | Per-query deadline (at least 1000) |
| auto.create.table | false | Create the table if missing |
MongoDB CDC Source
Reads one collection's change stream and delivers it straight to sinks, with no broker in between. Byte-bounded backpressure, classified bounded reconnects, and deployment- and collection-bound resume-token checkpoints. Embedded and single-node server mode, at-least-once; cluster mode and exactly-once reject it.
| Property | Default | Description |
|---|---|---|
| connection.uri | required | MongoDB connection string (replica set or sharded cluster) |
| database | required | Database name |
| collection | required | Fixed collection name; the collection must exist before source startup |
| output.mode | history | history: one immutable record per change event (append-only). document: post-image puts and key-only deletes for keyed mirrors |
| full.document.mode | delta | delta or required; document mode needs required |
| snapshot.mode | never | initial copies the collection at a committed point in time, then streams every later change (replica sets only) |
| objectid.columns | document mode: VARCHAR columns that hold ObjectIds as lowercase hex | |
| document.json.column | document mode: VARCHAR column receiving the whole document as canonical Extended JSON | |
| pipeline | [] | JSON array of at most 64 $match stages, bounded to 256 KiB; history mode without a snapshot only |
| max.buffered.bytes | 67108864 | Retained BSON-byte limit across the reader queue and poll buffer (1 MiB to 4 GiB) |
Full document modes:
delta sends the update description only (default).
required needs changeStreamPreAndPostImages enabled on the collection; a missing image stops the source and is never treated as a delete.
History schema (version 1):
event_id, event_version, operation, database,
collection, collection_uuid, document_key,
full_document, update_description, event_details,
resume_token, cluster_time_seconds, cluster_time_increment,
wall_time, txn_number, lsid, snapshot_id.
Document values are canonical Extended JSON v2. Operation names are data: no history column
makes a sink delete rows.
Document mode: the PRIMARY KEY must be the document key
(_id, plus shard-key fields); non-key columns are nullable and map exactly or
fail. Sinks must be keyed on the same columns: PostgreSQL upsert with
changelog.mode, or Delta write.mode=upsert. Filters, streams,
joins and aggregates over a document source are rejected.
MongoDB Sink
| Property | Default | Description |
|---|---|---|
| connection.uri | required | MongoDB connection string |
| database | required | Target database name |
| collection | required | Target collection name |
| write.mode | insert | insert, upsert, cdc_replay |
| write.mode.key_fields | Comma-separated top-level keys; required for upsert | |
| replay.source.namespace | cdc_replay: the source database.collection whose events apply to this target | |
| flush.interval.ms | 250 | Max time between flushes (ms) |
| sink.write.timeout.ms | 30000 | End-to-end write deadline |
Write modes:
insert is append-only.
upsert replaces documents by the fields listed in write.mode.key_fields.
cdc_replay applies MongoDB CDC history records: insert, replace and snapshot replace by document key, update applies the post-image (or the update description), delete deletes by key, metadata events write nothing, and destructive lifecycle events stop the sink.
Writes are ordered and majority-journaled; these correctness settings are not user-tunable. Time series collections only support insert.
Delta Lake Source
Replayable, so it works with at_least_once. It is a singleton source; cluster mode rejects it.
| Property | Default | Description |
|---|---|---|
| table.path | required | Delta table path (local, s3://, az://, gs://) |
| read.mode | changes | changes (snapshot, then each later version's changes) or snapshot (latest version only) |
| starting.version | A non-negative version number or latest; only with read.mode = changes. Unset emits the current snapshot first. | |
| poll.interval.ms | 1000 | Version poll interval; must be greater than 0 |
| catalog.type | none | none, glue (needs catalog.database), unity (needs catalog.name and catalog.schema) |
partition.filter and cdf.enabled are rejected.
Delta Lake Sink
| Property | Default | Description |
|---|---|---|
| table.path | required | Delta table path |
| write.mode | append | append, overwrite (each flush replaces the table), upsert (alias merge) |
| merge.key.columns | Key columns for upsert (comma-separated); required for upsert | |
| partition.columns | Partition columns (comma-separated) | |
| target.file.size | 134217728 | Target Parquet file size (128 MiB) |
| max.buffer.records | 100000 | Records before flush |
| max.buffer.duration.ms | 60000 | Time before flush |
| schema.evolution | false | Auto-merge new columns |
| auto.create | false | Create the table if missing (defaults to true when catalog.storage.location is set) |
| write.timeout.ms | 30000 | Write deadline; at least 5000 |
| parquet.compression | zstd | zstd, snappy, lz4, gzip, none |
| catalog.type | none | none, glue, unity (with catalog.* keys) |
Cloud storage options are passed with a storage. prefix, for example storage.aws_access_key_id, storage.aws_secret_access_key, storage.aws_region, storage.aws_endpoint; each falls back to the provider's environment variable. Delivery comes from the pipeline-wide setting. exactly_once is supported only for write.mode = append with the atomic-create DefaultLogStore; custom S3 endpoints must pass an atomic conditional-create probe at sink startup. Cluster exactly-once also requires an S3/S3A table URI.
Apache Iceberg Source & Sink
Connector name iceberg for both directions. Only the REST catalog is enabled in
v0.33.0 builds; glue, hms, s3tables and sql are
rejected at startup. Default builds include S3 and local filesystem storage; GCS and Azure need
the iceberg-gcs / iceberg-azure build features.
Common
| Property | Default | Description |
|---|---|---|
| catalog.uri | required | REST catalog endpoint; must not embed credentials |
| catalog.warehouse | required | Warehouse (legacy alias warehouse) |
| namespace | required | Iceberg namespace |
| table.name | required | Table name |
| catalog.type | rest | Only rest works in this release |
| catalog.auth.type | none | none, bearer (token in catalog.property.token), oauth2 (credential in catalog.property.credential, plus catalog.oauth2.*) |
| storage.type | inferred | s3, gcs, azure, fs |
| storage.endpoint | Object-store endpoint (e.g. MinIO); with storage.region and storage.path_style | |
| table.ref | main | The sink can publish only to main |
Other catalog and storage settings pass through as catalog.property.* and storage.property.*.
Source
| Property | Default | Description |
|---|---|---|
| read.mode | snapshot | snapshot or append (alias incremental); changelog is rejected |
| read.bootstrap | initial | initial or none; only with read.mode = append |
| poll.interval | 60s | Snapshot poll interval (alias poll.interval.ms) |
| start.snapshot.id | Start from this snapshot instead of the current one | |
| projection | Columns to read (alias select.columns) | |
| filter | Scan filter |
Sink
| Property | Default | Description |
|---|---|---|
| write.mode | append | Only append; merge-on-read and copy-on-write are rejected |
| auto.create | false | Create the table; format.version (default 2), partition.spec, sort.order and table.property.* require it |
| target.file.size.bytes | 134217728 | Target data file size (alias target.file.size) |
| parquet.compression | zstd | zstd, snappy, lz4, uncompressed |
| max.buffer.rows | 65536 | Rows buffered before a flush |
| max.buffer.bytes | 67108864 | Bytes buffered before a flush |
| max.flush.age | 300s | Maximum age of buffered data |
| schema.evolution.mode | strict | Only strict; safe is rejected |
Cluster exactly_once is admitted only for an append sink on a REST catalog with no or bearer auth and S3 storage with a direct S3 warehouse.
WebSocket Source
Note: the WebSocket source is a client and is ephemeral (non-replayable), so it requires pipeline-wide best_effort; it is rejected under at_least_once, the server default. Reconnects can create gaps or duplicates. Formats: json (used when omitted), csv (comma-delimited, no header) or binary. Unknown properties are rejected.
| Property | Default | Description |
|---|---|---|
| url | required | ws:// or wss:// URL (comma-separated in failover order) |
| subscribe.message | Message sent after each handshake | |
| reconnect.enabled | true | Auto-reconnect |
| reconnect.initial.delay.ms | 100 | Initial reconnect delay |
| reconnect.max.delay.ms | 30000 | Max backoff delay |
| reconnect.max.retries | Maximum reconnect attempts; empty means unlimited | |
| on.backpressure | block | block or drop_newest |
| max.message.size | 67108864 | Maximum accepted message size in bytes (at most 64 MiB) |
WebSocket Sink
Ephemeral, so best_effort only. The wire format is always JSON: any FORMAT clause (or TOML format) is rejected. Unknown properties are rejected.
| Property | Default | Description |
|---|---|---|
| mode | server | server (accept clients) or client (push to remote) |
| bind.address | Listen address, e.g. 0.0.0.0:9090. Required in server mode; not allowed in client mode. | |
| url | Remote ws:///wss:// URL. Required in client mode; not allowed in server mode. | |
| max.connections | 10000 | Max concurrent clients (server mode, 1 to 65536) |
| ping.interval.ms | 30000 | Ping interval (server mode, at least 1000) |
| ping.timeout.ms | 10000 | Pong timeout (server mode, at least 1000) |
Client mode sends one schema-encoded JSON row per text frame. Server-mode clients must first send {"action":"subscribe"}; the server replies with a subscription ID and then emits {"type":"data","data":[...],"sequence":N} text frames. Server fan-out uses a shared 32-frame ring with 256 KiB frames; an oversized row or burst is rejected before publication, and a lagging subscriber is disconnected so sequence gaps remain visible. The built-in server does not terminate TLS or authenticate clients; expose it only on a trusted network or behind an authenticated TLS reverse proxy.
File Source (Auto-Loader)
Watches a local directory and loads new files. Formats: csv, tsv,
json (aliases jsonl, ndjson), text,
parquet, arrow. Without a FORMAT clause the format is taken from the
extension in path (e.g. /data/in/*.csv); a bare directory needs an explicit
format. CSV and JSON sources need declared columns or schema.inference = true.
Replayable; cluster mode rejects it.
| Property | Default | Description |
|---|---|---|
| path | required | Local directory, or a glob in the last path component; an absolute file:// URL is accepted. Cloud URLs are rejected in this release. Not recursive. |
| glob_pattern | File-name glob; overrides a glob in path | |
| poll_interval | 10s | Duration (e.g. "5s", "500ms"; a bare number means seconds). Used when the directory is watched by polling (network or FUSE mounts); local directories use filesystem events. |
| stabilisation_delay | 1s | Wait after the last modify event before a file is treated as complete |
| max_files_per_poll | 100 | Files per poll cycle |
| max_file_bytes | 268435456 | Largest file accepted (256 MiB) |
| csv.delimiter | , | CSV delimiter (first byte used; tab for tsv) |
| csv.has_header | true | CSV has a header row |
| schema.inference | false | Infer CSV/JSON columns from sample files |
| include_metadata | false | Add a _metadata struct column |
Known limitations in v0.33.0:
- A
jsonfile must contain exactly one JSON object. Files with several JSON Lines records, or a top-level JSON array, fail to decode ("trailing characters" or "JSON value must be an object"), despite thejsonl/ndjsonaliases. - A file that fails to decode is retried continuously, with a warning in the log each time, and stays at the head of the queue, so later files are not loaded. Fixing or deleting the file does not clear it while the server runs (the source then reports that the file changed size or is missing): restart the server after fixing or removing it.
- The CSV reader cannot decode
TIMESTAMPcolumns; declaring one fails with "CSV reader ... cannot decode". UseBIGINT,DOUBLE,BOOLEAN,DATEorVARCHARcolumns, or another format such as Parquet.
File Sink
Writes immutable rolling files to a local directory. Durable at-least-once; exactly_once rejects it. The format (FORMAT clause / TOML format) is required: csv, json, text, parquet or arrow.
| Property | Default | Description |
|---|---|---|
| path | required | Local output directory |
| prefix | part | File name prefix |
| compression | snappy | Parquet only: snappy, gzip, zstd, lz4, none/uncompressed (brotli is rejected) |
| max_file_size | Bytes before rotation (row formats only) | |
| max_buffered_batches | 10000 | Maximum batches buffered between bulk-format (Parquet, Arrow) flushes |
The mode option was removed and is rejected; files are always published as immutable rolling files.
OpenTelemetry Source
Receives OTLP/gRPC traces, metrics or logs. Each source handles one signal type on its own
port and has a fixed schema for that signal, so there is no FORMAT clause. The source is
ephemeral and node-local: it needs best_effort delivery and cannot run in cluster mode.
| Property | Default | Description |
|---|---|---|
| port | 4317 | gRPC listen port |
| bind.address | 0.0.0.0 | Listen address |
| signals | traces | One of traces, metrics, logs; create one source per signal |
| batch_size | 1024 | Max rows per batch |
| channel_capacity | 64 | Bounded batch channel capacity |
CREATE SOURCE traces FROM OTEL (port = '4317', signals = 'traces');
Generator Source
Emits a deterministic sequence with no external system, for demos and tests. It is always
built in. Row i is (seq = i, ts_ms = i * 1000 / rows.per.second, value = "v{i}")
with the fixed schema seq BIGINT NOT NULL, ts_ms BIGINT NOT NULL,
value VARCHAR NOT NULL. Because output depends only on the offset, the source is
replayable and admitted for single-node at_least_once and exactly_once.
It is a singleton source, so cluster mode rejects it.
| Property | Default | Description |
|---|---|---|
| rows.per.second | 1000 | Emission rate; must be greater than 0 |
| batch.max.size | 1024 | Max rows per batch |
| max.rows | unbounded | Stop after this many rows |
[[source]]
name = "gen"
connector = "generator"
[source.properties]
"rows.per.second" = "100"
Join Semantics
Bounded interval joins are the stateful stream-stream joins. Temporal joins look up the
version of a keyed source that was valid at the probe row's event time. Lookup joins enrich
a stream from a [[lookup]] table. Admission rejects unsupported join shapes
before connector I/O. The rules below follow the
SQL reference.
| Join Type | State | Scope | Match |
|---|---|---|---|
| Bounded interval | Keyed stream state | Single node and cluster | All pairs within the time bound |
| Temporal | Versioned right-side history, keyed | Single node and cluster | Point-in-time lookup |
| Lookup | Lookup table (replicated or on-demand cache) | Single node (cluster rejects it) | Key equality |
Interval Join (Stream-Stream)
Matches pairs of rows from two direct sources whose equality keys match and whose event
times satisfy the directional predicate
right.ts BETWEEN left.ts AND left.ts + bound, where the bound is a positive,
finite interval. Both inputs need watermarks on TIMESTAMP NOT NULL event-time
columns. Equality keys are one or more VARCHAR/BIGINT columns of
matching types; NULL keys do not match.
Supported types: INNER, LEFT, RIGHT, FULL, LEFT/RIGHT SEMI, LEFT/RIGHT ANTI
CREATE STREAM matched AS
SELECT o.order_id AS order_id,
o.amount AS amount,
p.status AS status
FROM orders o
INNER JOIN payments p
ON o.order_id = p.order_id
AND p.ts BETWEEN o.ts AND o.ts + INTERVAL '1' HOUR;
- Every projected expression needs an explicit alias, and every projected or filtered column must use its left/right qualifier.
- Outer and anti unmatched rows are emitted only when the opposite input's watermark closes their possible match interval.
- Joining and aggregating in one statement is not supported: name the join output and aggregate it in a separate stream.
- Cross, unbounded, general non-equality, intermediate-input and multi-way joins are rejected on this path.
Temporal Join (FOR SYSTEM_TIME AS OF)
For each left row, looks up the right-side row that was valid at the left row's event time,
using SQL:2011 FOR SYSTEM_TIME AS OF. Runs in single-node and cluster mode for one
direct two-input join. ASOF JOIN is not an alias.
Supported types: INNER, LEFT
CREATE STREAM converted_orders AS
SELECT o.order_id AS order_id,
o.amount AS amount,
r.rate AS rate,
o.amount * r.rate AS converted
FROM orders o
JOIN currency_rates FOR SYSTEM_TIME AS OF o.order_time AS r
ON o.currency = r.currency;
- The left source must be append-only. The right source must declare its equality keys as its
PRIMARY KEY; it may be append-only or keyed-upsert. - Both sides need watermarks on their event-time columns. A result becomes final only after the right watermark passes its probe time.
- Set
[server].temporal_join_idle_history_retention; a pipeline containing a temporal join requires it. It bounds right-side version history while an input is idle.
Lookup Join
Enriches stream rows from a lookup table by equality on its
single-column primary key, with no time bound. A replicated table is held in
memory; an on-demand table fetches missing keys from the connector and keeps them in
a bounded memory cache (cache.memory, cache.ttl). With
pushdown set to auto or enabled, predicates are pushed to the
lookup connector. Cluster mode rejects lookup joins.
Supported types: INNER, LEFT
SELECT o.order_id, o.amount, p.name, p.category
FROM orders o
LEFT JOIN products p
ON o.product_id = p.id;
[supervision]
Single-node only. After a fatal pipeline fault, the server restarts the pipeline from the last
checkpoint with exponential backoff, up to max_restarts times within the window; after
that the pipeline stays faulted. Supervision is not enabled when [[process_function]]
entries are configured. Changes require a restart.
| Field | Type | Default | Description |
|---|---|---|---|
| max_restarts | usize | 5 | Restarts allowed within window_secs |
| window_secs | u64 | 60 | Sliding window for counting restarts; must be greater than 0 |
| initial_backoff_ms | u64 | 500 | Backoff before the first restart in a window |
| max_backoff_secs | u64 | 30 | Cap on the exponential backoff |
AI Functions ([ai] and [models])
SQL functions such as ai_classify, ai_sentiment, ai_embed and
ai_complete run a named model from a registry configured with
[ai.providers.<name>], [models.<name>] and
[ai.defaults]. See the
server README AI section
for the keys and an example.
- AI functions are single-node only; cluster mode rejects them (
[LDB-4007]). - Local (ONNX) models need ONNX Runtime 1.24 or newer installed on the host, on the library search path or via
ORT_DYLIB_PATH. The Docker image does not include ONNX Runtime. Remote providers do not need it. - Remote providers read their API key from the environment variable named in
api_key_env; the key is not stored in the config. - Changes to
[ai]or[models]require a restart.
Environment Variables
Config file: before parsing, the server replaces ${VAR} and
${VAR:-default} anywhere in the TOML text with environment values, including inside
[[pipeline]] SQL and the top-level sql string. A missing variable without a
default is a startup error. Write $${VAR} to keep a literal ${VAR} in the
loaded config so that it is resolved later, when the DDL runs.
Connector and format options in DDL: when a CREATE SOURCE,
CREATE SINK or lookup-table statement runs, ${VAR} and
${VAR:-default} in its connector and format option values are resolved from the
database's config variables (set with LaminarDbBuilder::config_var when embedded), then
from the process environment. This also applies to DDL sent through
POST /api/v1/sql or pgwire. An unresolved name without a default fails the statement.
Other parts of SQL submitted at runtime are not substituted.
[source.properties]
"bootstrap.servers" = "${KAFKA_BROKERS:-localhost:9092}"
"sasl.username" = "${KAFKA_USER}"
"sasl.password" = "$${KAFKA_PASSWORD}" # kept literal; resolved when the DDL runs
[checkpoint]
url = "s3://${S3_BUCKET}/checkpoints" # cluster mode (single-node at_least_once needs file://)
[checkpoint.storage]
aws_access_key_id = "${AWS_ACCESS_KEY_ID}"
aws_secret_access_key = "${AWS_SECRET_ACCESS_KEY}"
Other variables read by the server: LAMINAR_DISABLE_FILE_WATCH=1 (or true) disables the config watcher, TOKIO_WORKER_THREADS sizes the I/O runtime, RUST_LOG overrides --log-level, and MALLOC_CONF tunes jemalloc.
Delivery Guarantees
| Guarantee | How it works | Requirements |
|---|---|---|
| best_effort | No replay promise. This is the low-latency mode for inputs or outputs that cannot survive a failure. | Accepts ephemeral connectors; failure may lose accepted events. |
| at_least_once | Server default. Recovery resumes replayable sources from the finalized checkpoint cut, so some records may be processed again. | Checkpointing, replayable sources, and durably acknowledged sinks. Single-node mode needs a local file:// checkpoint directory (an object-store URL is rejected with [LDB-0014]). Cluster mode needs a cluster-shared s3://, gs:// or az:// checkpoint URL. |
| exactly_once | Barrier-aligned state and source positions are sealed before checkpoint-committable sink output becomes externally visible. | Single-node requires an exclusively locked local file:// checkpoint directory ([LDB-0014] otherwise), a replayable source with exact recovery, and a coordinated sink. Cluster mode requires a shared checkpoint URL and Kafka input with either an S3/S3A append-mode Delta Lake sink or a REST-catalog Iceberg append sink backed by S3/S3A storage. Kafka output is exactly-once in embedded and single-node mode through checkpoint-staged transactions and a fenced committer; cluster Kafka output remains at-least-once. Unsupported combinations fail before connector I/O. |
Delivery is one pipeline-wide setting. Per-connector delivery fields are rejected, and
incompatible connectors fail before external I/O. For example, the WebSocket, OpenTelemetry
and NATS (Core and JetStream) sources are non-replayable and therefore require
best_effort; they fail under the server default at_least_once. Cluster mode
rejects best_effort. Cluster
exactly_once admits Kafka input with an S3/S3A append-mode Delta Lake sink
or a REST-catalog Iceberg append sink backed by S3/S3A storage.
Full Config Example
A single-node, at-least-once pipeline: a Kafka source read from the earliest offset, a
one-minute VWAP window, and two sinks. Set LAMINAR_CONSOLE_TOKEN (at least 8
characters) and KAFKA_BROKERS before starting; a missing variable is a startup
error. To add enrichment, define a [[lookup]] table and join it in
a separate pipeline.
[server]
mode = "single"
bind = "0.0.0.0:8080"
console_token = "${LAMINAR_CONSOLE_TOKEN}" # required for a non-loopback bind
delivery = "at_least_once"
[checkpoint]
# Single-node at_least_once / exactly_once need a local file:// directory.
url = "file:///var/lib/laminardb/checkpoints"
interval = "30s"
# --- Sources ---
[[source]]
name = "trades"
connector = "kafka"
format = "json"
[source.properties]
"bootstrap.servers" = "${KAFKA_BROKERS}"
topic = "market-trades"
"group.id" = "laminar-vwap"
"startup.mode" = "earliest" # group offsets are rejected under at_least_once
[[source.schema]]
name = "symbol"
type = "VARCHAR"
nullable = false
[[source.schema]]
name = "price"
type = "DOUBLE"
[[source.schema]]
name = "volume"
type = "BIGINT"
[[source.schema]]
name = "ts"
type = "TIMESTAMP"
nullable = false
[source.watermark]
column = "ts"
max_out_of_orderness = "5s"
# --- Pipelines ---
[[pipeline]]
name = "vwap"
sql = """
SELECT symbol,
tumble(ts, INTERVAL '1' MINUTE) AS window_start,
SUM(price * CAST(volume AS DOUBLE)) / SUM(CAST(volume AS DOUBLE)) AS vwap,
SUM(volume) AS volume
FROM trades
GROUP BY symbol, tumble(ts, INTERVAL '1' MINUTE)
EMIT ON WINDOW CLOSE
"""
# --- Sinks ---
[[sink]]
name = "kafka_output"
pipeline = "vwap"
connector = "kafka"
format = "json"
[sink.properties]
"bootstrap.servers" = "${KAFKA_BROKERS}"
topic = "vwap-1m"
"key.column" = "symbol"
[[sink]]
name = "archive"
pipeline = "vwap"
connector = "files"
format = "parquet"
[sink.properties]
path = "/var/lib/laminardb/archive/vwap"