This document defines the transport-family boundary for ducknng. It is intentionally narrower than docs/protocol.md: the protocol document defines the manifest methods, session lifecycle, and payload rules, while this document defines how those same contracts are carried over concrete transport adapters.
The core rule is simple. Transport adapters may change how bytes move, but they do not change what the bytes mean. The ducknng envelope remains the method layer, Arrow IPC remains the tabular payload layer, JSON remains the control-metadata layer, and the manifest remains the source of truth for public RPC methods. A new transport adapter is therefore not a license to mint parallel method names or alternate session semantics.
Today ducknng has two implemented transport families. The NNG family covers inproc://, ipc://, tcp://, tls+tcp://, ws://, and wss://, all of which resolve into the NNG adapter layer isolated in src/ducknng_nng_compat.c. The HTTP family covers http:// and https:// through the separate adapter in src/ducknng_http_compat.c; native builds use NNG’s supplemental HTTP APIs rather than NNG protocol sockets, while browser wasm builds route client requests through the browser-safe HTTP bridge behind the same adapter boundary. The transport-family parser that classifies URL schemes lives separately in src/ducknng_transport.c so the SQL layer can pick one family cleanly before handing control to the appropriate adapter.
The stable scheme matrix is:
| Surface | Accepted schemes | TLS handle accepted on | Explicitly rejected |
|---|---|---|---|
ducknng_start_server(...) |
inproc://, ipc://, tcp://, tls+tcp://, ws://, wss://, http://, https:// |
tls+tcp://, wss://, https:// |
unknown schemes; TLS handles on non-TLS schemes |
generic socket API (ducknng_dial_socket, ducknng_listen_socket) |
inproc://, ipc://, tcp://, tls+tcp://, ws://, wss:// |
tls+tcp://, wss:// |
http://, https:// |
synchronous RPC/session helpers (ducknng_request, ducknng_get_rpc_manifest, ducknng_run_rpc[_params], ducknng_query_rpc[_params], ducknng_prepare_query[_params], and explicit open/fetch/close/cancel helpers) |
NNG schemes plus http://, https:// |
tls+tcp://, wss://, https:// |
unknown schemes; TLS handles on non-TLS schemes |
synchronous raw RPC/session helpers (ducknng_get_rpc_manifest_raw, ducknng_run_rpc_raw, ducknng_open_query_raw, ducknng_fetch_query_raw, ducknng_close_query_raw, ducknng_cancel_query_raw, ducknng_request_raw) |
NNG schemes plus http://, https:// |
tls+tcp://, wss://, https:// |
unknown schemes; TLS handles on non-TLS schemes |
raw RPC/session AIO helpers (ducknng_request_raw_aio, ducknng_get_rpc_manifest_raw_aio, ducknng_run_rpc_raw_aio, ducknng_open_query_raw_aio, ducknng_fetch_query_raw_aio, ducknng_close_query_raw_aio, ducknng_cancel_query_raw_aio) |
NNG schemes plus http://, https:// |
tls+tcp://, wss://, https:// |
unknown schemes; TLS handles on non-TLS schemes |
HTTP client helpers (ducknng_ncurl, ducknng_ncurl_aio, ducknng_ncurl_stream_open_aio, ducknng_ncurl_stream_recv_aio, ducknng_ncurl_table) |
http://, https:// |
https:// |
NNG schemes, unknown schemes, TLS handles on http:// |
tls_config_id = 0 means no TLS handle. Supplying a non-zero TLS handle on a non-TLS URL is rejected rather than silently ignored. This is part of the security contract: if an operator supplied TLS material, the connection must either use a TLS-capable scheme or fail before dialing/listening. For TLS-capable client URLs (tls+tcp://, wss://, and https://), a TLS handle with auth_mode = 0 still verifies the server certificate by default; the auth_mode = 0 no-client-certificate behavior applies to server/listener mode only.
The generic socket API reports expected failures in-band. Its synchronous helpers return a struct with ok, error, nullable nng_error, nullable nng_error_message, socket_id, payload, and url. NNG-returned failures use the numeric NNG error and nng_strerror() text; ducknng validation failures, such as trying to dial https:// through the NNG socket layer, keep those NNG fields NULL.
HTTP is not just a spelling of NNG tcp://, and HTTPS is not just a spelling of NNG tls+tcp://. They share lower network layers, but their application framing is different. NNG tcp:// and tls+tcp:// carry NNG scalability-protocol frames directly over streams. HTTP/HTTPS carries HTTP requests and responses with methods, paths, headers, status codes, and bodies; ducknng places exactly one framed RPC envelope in that HTTP body. Likewise, NNG ws:// and wss:// use an HTTP/WebSocket handshake but then carry the NNG WebSocket mapping for scalability protocols, so they remain in the NNG family rather than becoming HTTP carrier routes.
The HTTP and HTTPS family surfaces through the same primary server helper as the NNG family: ducknng_start_server(...) chooses the carrier from the URL scheme, and http:// / https:// listeners use contexts = 1 because the HTTP carrier does not expose the NNG REP-context model. ducknng_ncurl(...) and unary ducknng_ncurl_aio(...) remain low-level whole-response HTTP primitives. The additive ncurl stream open/receive/close family exposes response headers before body completion and de-frames incremental body reads without changing the unary contract. Open operations resolve scoped credential profiles inside the same client path. SQL scalar helpers that perform transport or lifecycle effects are VOLATILE. Dynamic-schema table functions have no C API volatility flag, so helpers such as ducknng_ncurl_table(...) and ducknng_query_rpc(...) are not per-row retry or mutation primitives. The route helpers remain local configuration, not manifest methods. The manifest describes manifest, handshake, query_prepare, exec, query_open, fetch, close, and cancel once regardless of carrier; docs/http.md pins the framed carrier and docs/http_server_framework.md the companion route layer.
Sessions, authentication, and execution policy are carrier-neutral. query_open accepts the same Arrow row with SQL, optional controls, and optional parameter tuple over every carrier; fetch/close/cancel retain the same session token and JSON control contract. Required mTLS attaches the same verified identity to session ownership over NNG and HTTP families. Peer/IP admission, service limits, SQL authorization, execution model, and monitor telemetry also retain their meaning when the outer carrier changes.
The same rule holds for Arrow record batches. The HTTP adapter does not reinterpret row batches as ad hoc JSON arrays or text serialization just because the outer transport is HTTP. When a method returns Arrow IPC or ducknng_quack_batch over NNG, it returns the same row payload over HTTP. When a method returns JSON control metadata over NNG, it returns JSON control metadata over HTTP. Content headers may help a client understand the body, but they do not replace the method contract.
The intended code boundary is therefore three-way. Transport-family parsing belongs in transport-neutral code such as ducknng_transport.c. NNG-specific socket, listener, TLS, peer-identity extraction from NNG pipes, and aio details belong in ducknng_nng_compat.c. HTTP-specific client/server details and peer-identity extraction from HTTP connections belong in ducknng_http_compat.c. The SQL layer should choose a transport family once from the URL scheme and then hand off to the appropriate adapter instead of scattering scheme-specific branches through session, wire, or Arrow code.
Because this transport-family split is explicit, http:// and https:// URLs are rejected by the generic NNG socket layer rather than being silently passed into NNG operations. At the same time, the synchronous request, RPC, and session helpers now route over those schemes through the HTTP adapter, while the unary and incremental ncurl families remain the low-level HTTP/HTTPS transport primitives. ws:// and wss:// stay on the NNG side of the boundary on native builds: they are NNG transports (SP-over-WebSocket), not aliases for the HTTP carrier. The one exception is the browser (wasm) backend, which has no NNG socket at all: there ws:///wss:// route through an async ducknng-frame-over-WebSocket carrier (issue #11) that talks to the server’s raw-WebSocket frame endpoint beside the HTTP mount, and the synchronous helpers reject them (no synchronous WebSocket receive) in favor of the AIO helpers. This routing is data-driven via ducknng_net_backend_carrier_scheme(), which reports ws/wss as carrier schemes only on a backend with no NNG transport, so the native SP behavior is unchanged.