Start the server

Run twill-server with two arguments: --listen HOST:PORT for the bind address and --db URL for the storage backend. The --db URL scheme selects the backend exactly as the embedded path does — file:// for pure-embedded, s3:///r2:///gs:// for disaggregated.

# Embedded backend (local file)
cargo run -p twill-server -- --listen 127.0.0.1:5433 --db file://./srv.db

# Disaggregated backend (object storage; credentials from the environment)
cargo run -p twill-server -- --listen 0.0.0.0:5433 --db s3://my-bucket/app

With no flags it defaults to --listen 127.0.0.1:5433 and --db file://./engine-server.db. The short forms -l and -d are accepted.

Same engine, just behind a listener

The SQL parser, MVCC, WAL, local cache, and Storage trait are byte-for-byte the embedded build. The only thing server mode adds is a protocol listener that turns wire messages into the same engine calls bun:ffi makes in-process. Develop embedded and deploy as a server (or the reverse) without changing your SQL or your data.

Authentication and TLS

The server speaks the Postgres frontend/backend protocol (version 3.0). For the current build, connect cleartext with sslmode=disable using trust auth — pass any user (e.g. postgres); no password is required. Always set sslmode=disable so the client does not attempt a TLS upgrade.

  • MUST set sslmode=disable on the client connection string; the listener serves cleartext.
  • MUST keep this on a trusted network — there is no transport encryption on the cleartext path. Put real bucket credentials in the server's environment, never in the client URL.
  • SHOULD front the server with a TLS-terminating pooler or proxy when exposing it beyond localhost (see Connection pooling).

Connect with psql

Point psql at the listener with key/value parameters or a URI. Either form works as long as SSL is disabled.

psql "host=127.0.0.1 port=5433 user=postgres sslmode=disable"

# or as a URI
psql "postgres://[email protected]:5433/srv?sslmode=disable"
CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);
INSERT INTO notes VALUES (1, 'hello');
SELECT id, body FROM notes WHERE id = 1;

Connect with Bun's SQL client

Bun ships a built-in Postgres client, so a Bun app connects with zero extra dependencies — just a connection string with sslmode=disable. Parameterized tagged-template queries map onto the extended-query path.

import { SQL } from "bun";

const sql = new SQL("postgres://[email protected]:5433/srv?sslmode=disable");

await sql`CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT)`;
await sql`INSERT INTO notes VALUES (${1}, ${"hello"})`;   // parameter binding
const rows = await sql`SELECT id, body FROM notes WHERE id = ${1}`;

Embedded vs. wire

For a single app that owns its database, the embedded path (Connect from Bun) avoids the socket entirely and runs at function-call latency. Reach for Bun.sql when you want multiple clients, existing Postgres tooling, or a network boundary.

Connect with a generic Postgres driver

Any driver that speaks protocol 3.0 and tolerates trust auth over cleartext will connect — node-postgres, pgx, psycopg, and similar. Use the simple-query path for un-parameterized statements and the extended-query path (Parse/Bind/Describe/Execute/Sync) for parameter binding and prepared statements.

import { Client } from "pg"; // node-postgres

const client = new Client({
  host: "127.0.0.1",
  port: 5433,
  user: "postgres",
  ssl: false,                 // sslmode=disable
});
await client.connect();
const { rows } = await client.query("SELECT id, body FROM notes WHERE id = $1", [1]);
await client.end();

What the wire subset supports

The target is a subset of the Postgres wire protocol — the message flows real clients in this stack actually use — not bug-for-bug Postgres fidelity. What is supported:

CapabilityStatus
Startup + trust auth (cleartext, sslmode=disable)Supported
Simple query protocol (Query → rows → CommandComplete)Supported
Extended query protocol (Parse/Bind/Describe/Execute/Sync/Close)Supported
Parameter binding and row descriptionSupported
Field-tagged ErrorResponse with SQLSTATESupported

It is a subset, not full Postgres

Aiming for full Postgres wire/SQL fidelity is out of scope. The SQL surface is the engine's focused subset (see the SQL reference); a driver that depends on Postgres-specific catalog introspection or features outside the subset may not connect cleanly. Stick to the clients and queries above.

REST for free with PostgREST

Because the engine speaks pgwire in server mode, PostgREST can point at it directly: it introspects the schema and serves an auto-generated REST API with no bespoke REST code. Run PostgREST in front of engine-server and inherit the existing REST layer rather than building one.

# postgrest.conf (sketch)
db-uri = "postgres://[email protected]:5433/srv?sslmode=disable"
db-schemas = "public"
db-anon-role = "postgres"

Keep PostgREST and the server on a trusted network; the cleartext connection between them carries no transport security on its own.

Pooling for bursty clients

Server mode is the only mode with sockets, so it is the only mode that needs a connection pool. For serverless or bursty client populations, place a transaction-mode pooler (PgBouncer / pgcat) in front of engine-server — see Connection pooling for the config and caveats. engine-server never bundles a pooler; it is a separate, composable process.

Related

Twill DB documentation · Licensed under AGPL-3.0. · Author