A PostgreSQL wire protocol library for Zig. Building blocks for building PostgreSQL-compatible servers.
Early development. Public API is not yet finalized. 75 unit tests passing.
- Zig 0.16.0+
zig fetch --save "git+https://github.com/SOG-web/pgwire#master"Then add the dependency to your build.zig:
const pgwire_dep = b.dependency("pgwire", .{ .target = target, .optimize = optimize, });
const pgwire = @import("pgwire"); const msg = pgwire.server.message; const ConnectionState = pgwire.server.connection.ConnectionState; // Read a startup message from a client connection const startup = try msg.readStartupMessage(reader, alloc); // Authenticate try msg.sendAuthOk(writer); // Send query results try msg.sendRowDescription(writer, &columns); try msg.sendDataRow(writer, &row); try msg.sendCommandComplete(writer, "SELECT 1"); try msg.sendReadyForQuery(writer, .idle);
See src/main.zig for a full working example.
zig build
| Option | Default | Description |
|---|---|---|
use_llvm |
true |
Force using LLVM as the codegen backend |
use_lld |
true |
Force using LLD as the linker |
zig build -Dtarget=aarch64-linux-gnu zig build -Dtarget=x86_64-macos
zig build run # Run the test server zig build test # Run all tests zig build --help # List all available steps
zig build testThe library provides primitives — the consumer owns the server loop, query handling, and auth decisions.
src/
├── root.zig # library entry point
├── types.zig # protocol constants (message types, OIDs, SQLSTATE codes, auth methods)
├── server/
│ ├── message.zig # read/write functions for protocol messages
│ ├── connection.zig # ConnectionState tracker + extended query parsing
│ ├── auth.zig # MD5 and SCRAM-SHA-256 crypto
│ └── buffered.zig # MessageWriter for atomic buffered output
├── test.zig # unit tests
└── main.zig # test server harness (consumer example)
- ✅ Frontend-Backend protocol messages (3.0)
- ❌ Frontend-Backend protocol messages (3.2, PostgreSQL 18)
- ❌ Streaming replication protocol
- ❌ Logical streaming replication protocol messages
- ✅ SSL Request and Response (decline/accept)
- ❌ PostgreSQL 17 direct SSL negotiation
- ❌ GSSAPI Request and Response (no encryption support)
- ❌ GSSAPI/SSPI authentication
- ✅ Protocol negotiation (version + key=value params)
- ✅ No authentication (AuthOk)
- ✅ MD5 password authentication
- ✅ SCRAM-SHA-256 authentication
- ❌ SCRAM-SHA-256-PLUS (channel binding)
- ❌ Clear-text password authentication
- ❌ SASL OAUTH (PostgreSQL 18)
- ✅ Simple Query protocol (Q messages)
- ✅ Multi-statement queries (quote-aware splitting)
- ✅ RowDescription, DataRow, CommandComplete
- ✅ EmptyQueryResponse
- ✅ Parse (P) + ParseComplete
- ✅ Bind (B) + BindComplete
- ❌ Describe (prepared statement) + ParameterDescription
- ❌ Describe (portal) + RowDescription
- ✅ Execute (E) + PortalSuspended
- ✅ Close (C) + CloseComplete
- ✅ Sync (S) + ReadyForQuery
- ✅ Flush (H)
- ❌ Portal naming (unnamed portal)
- ❌ Extended Query state machine
- ✅ Terminate (X) handling
- ✅ CancelRequest detection
- ❌ Query cancellation execution (BackendKeyData lookup)
- ✅ ErrorResponse with SQLSTATE codes
- ✅ NoticeResponse
- ✅ ErrorFieldType constants (S/C/M/D/H/P/etc.)
- ✅ Error/Warning severity levels
- ✅ CopyInResponse (server ← client)
- ✅ CopyOutResponse (server → client)
- ✅ CopyData
- ✅ CopyDone
- ✅ CopyFail
- ❌ CopyBoth (simultaneous in/out)
- ❌ Notify (server → client async notification)
- ✅ Text format (null-terminated strings)
- ❌ Binary format (raw bytes)
- ✅ message.zig — standalone read/write functions for all protocol messages
- ✅ connection.zig — ConnectionState + extended query parsing
- ✅ auth.zig — MD5 and SCRAM-SHA-256 crypto
- ✅ buffered.zig — MessageWriter for atomic buffered output
- ✅ All write/send functions return
WriteError!void - ❌ Extended Query state machine (session state tracking)
- ❌ QueryParser API (prepared statement transformation)
- ❌ ResultSet builder/encoder API
- ❌ AuthSource API (fetching and hashing passwords)
- ❌ Server parameters API (dynamic parameter management)
- ❌ Streaming replication API
- ❌ Logical streaming replication API
- ❌ SSL/TLS connection upgrade
- ❌ Client/frontend API (planned for future)
- ❌ TCP/TLS server (consumer owns the server loop)
- ✅ 75 unit tests (message building, parsing, auth, buffered writer)
- ❌ Integration tests with real PG client
- ❌ End-to-end tests with libpq / psql
Protocol constants: MessageType, AuthMethod, DataType, ErrorCode, TransactionStatus, ErrorFieldType, ErrorSeverity, ColumnDesc, Message, StartupMessage.
Standalone functions for reading/writing protocol messages:
const msg = pgwire.server.message; // Read msg.readStartupMessage(reader, alloc) -> ?StartupMessage msg.readMessage(reader, alloc) -> ?Message msg.readQuery(message) -> []const u8 msg.readInt16(payload, offset) -> i16 msg.readInt32(payload, offset) -> i32 msg.readCString(payload, offset) -> { value, end } // Auth msg.sendAuthOk(writer) msg.sendAuthMD5Password(writer, salt) msg.sendAuthSASL(writer, mechanisms) msg.sendAuthSASLContinue(writer, server_data) msg.sendAuthSASLFinal(writer, server_data) // Connection msg.sendParameterStatus(writer, name, value) msg.sendBackendKeyData(writer, pid, secret) msg.sendReadyForQuery(writer, status) // Query results msg.sendRowDescription(writer, columns) msg.sendDataRow(writer, values) msg.sendCommandComplete(writer, tag) msg.sendEmptyQueryResponse(writer) // Errors msg.sendErrorResponse(writer, .{ .sqlstate = "42P01", .message = "relation not found" }) msg.sendErrorResponseSimple(writer, "42P01", "relation not found") msg.sendNoticeResponse(writer, .{ .message = "warning" }) // Extended Query msg.sendParseComplete(writer) msg.sendBindComplete(writer) msg.sendCloseComplete(writer) msg.sendParameterDescription(writer, param_types) msg.sendNoData(writer) msg.sendPortalSuspended(writer) // COPY msg.sendCopyInResponse(writer, format) msg.sendCopyOutResponse(writer, format) msg.sendCopyData(writer, data) msg.sendCopyDone(writer) msg.sendCopyFail(writer, error_message) // Statement splitting msg.splitStatements("SELECT 1; SELECT 2", alloc) -> []Statement // All write/send functions return WriteError!void
Per-connection state tracker and extended query parsing:
const conn = pgwire.server.connection; // State var state = conn.ConnectionState{}; state.setParameter("user", "postgres"); state.authenticate(196608); state.detectTransactionCommand("BEGIN"); state.beginTransaction(); state.commitTransaction(); // Extended Query parsing conn.parseStatement(payload, alloc) -> ?ParseResult conn.parseBind(payload, alloc) -> ?BindResult conn.parseDescribe(payload) -> ?DescribeResult conn.parseExecute(payload) -> ?ExecuteResult conn.parseClose(payload) -> ?CloseResult
MD5 and SCRAM-SHA-256 cryptography. Functions that need randomness take a std.Io parameter.
const auth = pgwire.server.auth; // MD5 const salt = auth.generateSalt(io); // requires Io const hash = auth.computeMD5Password(username, password, salt); // returns [35]u8 const ok = auth.verifyMD5Password(username, password, salt, client_hash); // SCRAM-SHA-256 const creds = auth.generateScramCredentials(password, salt, 4096); const valid = auth.verifyScramClientProof(creds.stored_key, auth_message, client_proof); const sig = auth.generateScramServerSignature(creds.server_key, auth_message); // SCRAM message builders var buf: [256]u8 = undefined; const server_first = auth.buildScramServerFirst(client_nonce, server_nonce, salt, 4096, &buf); const server_final = auth.buildScramServerFinal(server_sig, &buf); const auth_msg = auth.buildScramAuthMessage(client_first_bare, server_first, client_final, &buf); auth.generateScramNonce(io, &buf); // requires Io
Atomic buffered writer:
const MsgWriter = pgwire.server.buffered.MessageWriter(8192); var mw = MsgWriter.init(&writer); try msg.sendRowDescription(&mw.buf_writer, &columns); try msg.sendDataRow(&mw.buf_writer, &row); try msg.sendCommandComplete(&mw.buf_writer, "SELECT 1"); try mw.flush(); // sends everything atomically
zig build run
Connects on port 6432. Use with psql:
psql -h 127.0.0.1 -p 6432 -U user -d db
See ROADMAP.md for planned work and integration test status. See docs/protocol.md for wire protocol details.