From c7393b50c0267d0d8dac07695dad1945887107fd Mon Sep 17 00:00:00 2001 From: Sean Silvius Date: Fri, 28 Aug 2026 23:09:52 -0700 Subject: [PATCH 1/2] docs(clients): the day-one client list and the rule every client follows Seven Tier 1 clients ordered by where traffic originates (TS/JS, Rust with a CLI, Python, C# with a Unity package, Drizzle driver, Kysely dialect, MCP server), a Tier 2 fast-follow (Go, Unreal via the Rust C-ABI, Swift, Kotlin, a Neon-driver server adapter), and what is declined. The rule: a client is fetch and JSON, no sessions, no query builder, and it is done when it passes the client cases in checks/ against the reference server. A per-language table pins what bigint and blob decode to, since that is the only place clients differ. Claude-Session: https://claude.ai/code/session_01RjiPB8PpLbDdK8soLAUPwQ Co-Authored-By: Claude Fable 5 --- README.md | 1 + clients.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 clients.md diff --git a/README.md b/README.md index 0c163a5..ae9a1dc 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ The PostgreSQL wire protocol is a streaming socket protocol — not HTTP, not ed - [examples/](./examples) — curl invocations, reference client and server, and two full Cloudflare implementations (D1-backed and Durable-Object-backed) - [checks/](./checks) — the checks a server passes to call itself http-sql 0.0.1 - [implementations.md](./implementations.md) — known servers and clients +- [clients.md](./clients.md) — the day-one clients we supply, the rule they all follow, and the lossless type per language ## Prior art diff --git a/clients.md b/clients.md new file mode 100644 index 0000000..46edf95 --- /dev/null +++ b/clients.md @@ -0,0 +1,62 @@ +# Day-one clients + +The clients we supply, in the order they ship, and the rule every one of them follows. A spec with no popular client loses to a client with no spec; this list is how http-sql gets traffic. + +## The rule for every client + +A client is `fetch` and JSON. It does one thing: send a statement or a batch, return the envelope, map errors. The whole contract fits in the reference client (~40 lines, `examples/reference-client.ts`), and every client below is that shape in its own language: + +- `POST` (and `QUERY` where the runtime allows it) with `Content-Type: application/http-sql+json`; send `Http-Sql-Accept-Version: 0.0.1`. +- `Authorization: Bearer `, supplied by the caller. The client never stores or mints a token. +- Body is `{sql, params}` or `{batch, atomic}`; params are positional JSON values with the two tagged forms (`blob` as base64, `bigint` as a decimal string). +- Return `columns`, `rows` (arrays of arrays), `rowsAffected`, `lastInsertId` as the language's natural types; decode tagged values into the language's lossless types (see the table). +- Map `error.code` to one typed error per registered code; carry `statementIndex` on non-atomic batch failures; treat an unknown `vendor:` code as the closest registered code by HTTP status family. +- No sessions, no connection objects, no retries beyond what the caller asks for, no query builder, no schema knowledge. If a feature needs the client to be smarter than that, it is a product built on http-sql, not the client. + +A client is done when it passes the client cases in `checks/` against the reference server. Nothing else counts. + +## Lossless types per language + +The one place clients differ is what a tagged `bigint` and `blob` become. Each client decodes to the type in this table and encodes from it; a client that rounds a 64-bit integer to a double is not done. + +| Language | `bigint` | `blob` | Note | +|---|---|---|---| +| TypeScript / JavaScript | `bigint` | `Uint8Array` | JSON numbers above 2^53 never appear on the wire, so `Number` stays safe for everything untagged | +| Rust | `i64` (or `i128` if a server sends past 64 bits) | `Vec` | serde untagged enum over the value shapes | +| Python | `int` | `bytes` | Python ints are arbitrary precision; the tag still matters for encoding on the way out | +| C# | `long` | `byte[]` | `System.Text.Json`; Unity needs the `UnityWebRequest` transport, not `HttpClient` | +| Go | `int64` | `[]byte` | `encoding/json` with a custom unmarshaler for the tagged forms | +| Swift | `Int64` | `Data` | `URLSession`; Codable with a custom decoder for the tagged forms | +| Kotlin | `Long` | `ByteArray` | `okhttp` or `ktor`; kotlinx.serialization | + +## Tier 1: ships before anything is announced + +Ordered by where http-sql traffic actually originates. + +| Client | Runtime | Who it is for | Package name (proposed) | Why day one | +|---|---|---|---|---| +| TypeScript / JavaScript | Browser, Cloudflare Workers, Deno, Vercel Edge, Node, Bun | Local-first web apps, edge functions, React Native / Expo | `@http-sql/client` on npm | Where the edge-era traffic is. One package, zero dependencies, `fetch` only, so the same file runs in a browser and a Worker. The reference client grows into this. | +| Rust | Native and wasm32 | CLIs, daemons, embedded engines, the C-ABI base for game engines | `http-sql` on crates.io | smugglr's own profile is a Rust client already; a standalone crate is the one other clients bind to (C-ABI for Unreal, wasm for the browser). Ships with a tiny `http-sql` binary so a shell or an agent can run a statement without writing code. | +| Python | CPython 3.10+ | Data and analytics scripts, notebooks, agent frameworks | `http-sql` on PyPI | The second-largest source of "send SQL, get rows" traffic after JS, and the language agents are written in. `httpx` optional, `urllib` default so it installs with nothing. | +| C# | .NET Standard 2.1, Unity 2021+ | Unity games (the games segment), .NET services | `HttpSql` on NuGet, plus a Unity package (UPM) | Games are the first segment fence targets and Unity is where they are built. The Unity package uses `UnityWebRequest` and a main-thread-safe callback so it does not block the render loop. | +| Drizzle driver | TypeScript | Anyone already on Drizzle at the edge | `@http-sql/drizzle` | The adoption lever the prior-art research found: PlanetScale implemented Neon's undocumented endpoint because Neon's driver had users. A Drizzle HTTP driver (the shape of `drizzle-orm/neon-http`) makes every Drizzle app an http-sql client with one import. | +| Kysely dialect | TypeScript | Anyone already on Kysely | `@http-sql/kysely` | Same lever, second-most-used TS query builder at the edge. Small; shares the core client. | +| MCP server | Node | A customer's agent (Claude, or any MCP host) | `@http-sql/mcp` | The "customer's agent calls fence directly" persona needs a tool, not a library. One tool, `query`, with the endpoint and token from config. Thin over the TS client. | + +## Tier 2: fast follow, ordered + +| Client | Why not day one | +|---|---| +| Go | Real audience (infra CLIs, Go backends) but not where our first segments originate. Ships once the Tier 1 shape has settled, so it copies rather than invents. | +| Unreal (C++) | Binds the Rust crate's C-ABI rather than being written twice; waits on that ABI, which smugglr-core is defining for its own engine SDKs. Pure UE (`FHttpModule` + JSON) is a fallback if the ABI slips. | +| Swift | Native iOS and macOS apps and games not on Unity. After C#. | +| Kotlin | Native Android and JVM. After Swift, same shape. | +| Neon-driver compatibility | Not a client: a server-side adapter so the existing `@neondatabase/serverless` driver's `fetchEndpoint` works against an http-sql server. Worth having for exactly the reason Drizzle is, but it lives in a server, not here. | + +## Declined for day one + +PHP, Ruby, Java, Elixir. No traffic origin in the segments we serve first, and each is a community that will write its own thirty lines the day it wants one. If someone ships one, it goes in `implementations.md`. + +## Where they live + +`clients//` in this repo, so the spec, the checks, and every client version together and the checks runner tests all of them in one CI job. Each is published independently under the package names above. MIT, like the spec: the clients are free; what you point them at may not be. From 8a5dfd8beb16af805f88decd6f754d53ff382e39 Mon Sep 17 00:00:00 2001 From: Sean Silvius Date: Fri, 28 Aug 2026 23:17:05 -0700 Subject: [PATCH 2/2] docs(clients): Unreal is Tier 1 -- UE 5.x and 6, native plugin, no wait on the C-ABI Operator direction. Unreal joins the day-one list as its own client: FHttpModule transport, FJsonObject envelope, int64 and TArray for the tagged forms, results on the game thread, a Blueprint-callable node. The Tier 2 row becomes the later option of moving the plugin onto the shared Rust C-ABI once smugglr-core defines it. Claude-Session: https://claude.ai/code/session_01RjiPB8PpLbDdK8soLAUPwQ Co-Authored-By: Claude Fable 5 --- clients.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/clients.md b/clients.md index 46edf95..9f9d5d0 100644 --- a/clients.md +++ b/clients.md @@ -25,6 +25,7 @@ The one place clients differ is what a tagged `bigint` and `blob` become. Each c | Rust | `i64` (or `i128` if a server sends past 64 bits) | `Vec` | serde untagged enum over the value shapes | | Python | `int` | `bytes` | Python ints are arbitrary precision; the tag still matters for encoding on the way out | | C# | `long` | `byte[]` | `System.Text.Json`; Unity needs the `UnityWebRequest` transport, not `HttpClient` | +| C++ (Unreal) | `int64` | `TArray` | `FHttpModule` and the engine's `FJsonObject`; no third-party JSON, no STL types on the Blueprint surface | | Go | `int64` | `[]byte` | `encoding/json` with a custom unmarshaler for the tagged forms | | Swift | `Int64` | `Data` | `URLSession`; Codable with a custom decoder for the tagged forms | | Kotlin | `Long` | `ByteArray` | `okhttp` or `ktor`; kotlinx.serialization | @@ -39,6 +40,7 @@ Ordered by where http-sql traffic actually originates. | Rust | Native and wasm32 | CLIs, daemons, embedded engines, the C-ABI base for game engines | `http-sql` on crates.io | smugglr's own profile is a Rust client already; a standalone crate is the one other clients bind to (C-ABI for Unreal, wasm for the browser). Ships with a tiny `http-sql` binary so a shell or an agent can run a statement without writing code. | | Python | CPython 3.10+ | Data and analytics scripts, notebooks, agent frameworks | `http-sql` on PyPI | The second-largest source of "send SQL, get rows" traffic after JS, and the language agents are written in. `httpx` optional, `urllib` default so it installs with nothing. | | C# | .NET Standard 2.1, Unity 2021+ | Unity games (the games segment), .NET services | `HttpSql` on NuGet, plus a Unity package (UPM) | Games are the first segment fence targets and Unity is where they are built. The Unity package uses `UnityWebRequest` and a main-thread-safe callback so it does not block the render loop. | +| Unreal (C++) | UE 5.x and 6 | Unreal games, the other half of the games segment | `HttpSql` plugin on Fab (and the repo) | The same reason as Unity. A native engine plugin: `FHttpModule` for transport, `FJsonObject` for the envelope, `int64` and `TArray` for the tagged forms, results delivered on the game thread, and a Blueprint-callable node so a designer can run a statement without C++. Written against the spec directly; it does not wait on the Rust C-ABI. | | Drizzle driver | TypeScript | Anyone already on Drizzle at the edge | `@http-sql/drizzle` | The adoption lever the prior-art research found: PlanetScale implemented Neon's undocumented endpoint because Neon's driver had users. A Drizzle HTTP driver (the shape of `drizzle-orm/neon-http`) makes every Drizzle app an http-sql client with one import. | | Kysely dialect | TypeScript | Anyone already on Kysely | `@http-sql/kysely` | Same lever, second-most-used TS query builder at the edge. Small; shares the core client. | | MCP server | Node | A customer's agent (Claude, or any MCP host) | `@http-sql/mcp` | The "customer's agent calls fence directly" persona needs a tool, not a library. One tool, `query`, with the endpoint and token from config. Thin over the TS client. | @@ -48,7 +50,7 @@ Ordered by where http-sql traffic actually originates. | Client | Why not day one | |---|---| | Go | Real audience (infra CLIs, Go backends) but not where our first segments originate. Ships once the Tier 1 shape has settled, so it copies rather than invents. | -| Unreal (C++) | Binds the Rust crate's C-ABI rather than being written twice; waits on that ABI, which smugglr-core is defining for its own engine SDKs. Pure UE (`FHttpModule` + JSON) is a fallback if the ABI slips. | +| Unreal on the Rust C-ABI | Not a second client: once smugglr-core's C-ABI exists, the Unreal plugin may switch its transport to the shared Rust crate so the two engines share one implementation. The Tier 1 plugin ships first on `FHttpModule` and does not wait. | | Swift | Native iOS and macOS apps and games not on Unity. After C#. | | Kotlin | Native Android and JVM. After Swift, same shape. | | Neon-driver compatibility | Not a client: a server-side adapter so the existing `@neondatabase/serverless` driver's `fetchEndpoint` works against an http-sql server. Worth having for exactly the reason Drizzle is, but it lives in a server, not here. |