# Nautilus Protocol
JSON-RPC 2.0 protocol for multi-language Nautilus clients.
## Overview
This crate defines the stable wire format for communication between:
- **Language-specific clients** (JavaScript, Python, etc.)
- **Nautilus engine** (Rust binary running over stdin/stdout)
### Public API
| `wire` | `RpcRequest`, `RpcResponse`, `RpcError`, `RpcId`, `ok()`, `err()` |
| `methods` | 11 method-name constants (`ENGINE_HANDSHAKE`, `QUERY_*`, `SCHEMA_VALIDATE`), request param structs, response structs |
| `error` | `ProtocolError` enum (18 variants), stable error-code constants, `Result<T>` alias |
| `version` | `PROTOCOL_VERSION` constant, `ProtocolVersion` wrapper |
### Design Notes
- **Single consumer** — only `nautilus-engine` depends on this crate today, but the API is designed for any language client that speaks JSON-RPC 2.0 over stdin/stdout.
- **`schema.validate`** types are defined but not yet implemented in the engine; they are reserved for a future release.
- **Value encoding** (base64 for bytes, string for decimals, etc.) is specified in this README as the wire-format contract, but the Rust encoding/decoding logic lives in `nautilus-engine`, not here.
### Protocol Stack
- **Transport**: Line-delimited JSON over stdin/stdout
- **Format**: JSON-RPC 2.0
- **Versioning**: Protocol version included in every request
## Protocol Version
Current version: **1**
All client requests must include `protocolVersion: 1` in their params. The engine will reject requests with unsupported versions.
## JSON Encoding
The protocol uses a stable JSON encoding for Nautilus `Value` types to ensure cross-language compatibility.
### Value Type Mappings
> **Note:** These encoding rules define the stable wire format between clients
> and the engine. The actual encoding/decoding logic lives in the engine layer,
> not in this crate — `nautilus-protocol` only defines the request/response
> structures and their `Serialize` / `Deserialize` implementations.
| `Null` | `null` | `null` | |
| `Bool` | `boolean` | `true`, `false` | |
| `I32` | `number` | `42` | 32-bit signed integer |
| `I64` | `number` | `9007199254740991` | 64-bit signed integer* |
| `F64` | `number` | `3.14159` | IEEE 754 double |
| `String` | `string` | `"hello"` | UTF-8 text |
| `Bytes` | `string` | `"SGVsbG8="` | **Base64 encoded** |
| `Decimal` | `string` | `"123.45"` | **String to avoid precision loss** |
| `DateTime` | `string` | `"2026-02-18T10:30:00Z"` | **RFC3339 / ISO 8601** |
| `Uuid` | `string` | `"550e8400-e29b-41d4-a716-446655440000"` | **Hyphenated lowercase** |
| `Json` | any | `{"key": "value"}` | Pass-through JSON value |
| `Array` | `array` | `["a", "b", "c"]` | Homogeneous array |
| `Array2D` | `array` | `[["a", "b"], ["c", "d"]]` | 2D array |
**\*Note on BigInt**: JavaScript clients should be aware of `Number.MAX_SAFE_INTEGER` (2^53 - 1). For values outside this range, the engine may return strings in future protocol versions.
### Example Value Encodings
```json
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"age": 25,
"balance": "1234.56",
"createdAt": "2026-02-18T10:30:00Z",
"isActive": true,
"avatar": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"tags": ["rust", "database", "orm"],
"metadata": {
"custom": "json",
"nested": true
}
}
```
## Methods
### `engine.handshake`
Initial handshake to validate protocol compatibility.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "engine.handshake",
"params": {
"protocolVersion": 1,
"clientName": "nautilus-js",
"clientVersion": "0.1.0"
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"engineVersion": "0.1.0",
"protocolVersion": 1
}
}
```
### `query.findMany`
Execute a findMany query on a model.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 2,
"method": "query.findMany",
"params": {
"protocolVersion": 1,
"model": "User",
"args": {
"where": {
"email": {
"contains": "test"
}
},
"orderBy": {
"createdAt": "desc"
},
"take": 10
}
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "test@example.com",
"createdAt": "2026-02-18T10:30:00Z"
}
]
}
}
```
### `query.create`
Create a new record.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 3,
"method": "query.create",
"params": {
"protocolVersion": 1,
"model": "User",
"data": {
"email": "new@example.com",
"name": "New User"
}
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"count": 1,
"data": [
{
"id": "660f9511-f3ac-52e5-b827-557766551111",
"email": "new@example.com",
"name": "New User"
}
]
}
}
```
### `query.findFirst`
Return the first record matching the given arguments (or `null`).
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 4,
"method": "query.findFirst",
"params": {
"protocolVersion": 1,
"model": "User",
"args": {
"where": { "isActive": true },
"orderBy": { "createdAt": "desc" }
}
}
}
```
**Response:** Same shape as `query.findMany` (`QueryResult`).
### `query.findFirstOrThrow`
Same as `query.findFirst`, but returns error code `3004` (Record not found)
if no matching record exists.
### `query.findUnique`
Find a single record by a unique filter.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 5,
"method": "query.findUnique",
"params": {
"protocolVersion": 1,
"model": "User",
"filter": { "id": 42 }
}
}
```
**Response:** Same shape as `query.findMany` (`QueryResult`).
### `query.findUniqueOrThrow`
Same as `query.findUnique`, but returns error code `3004` (Record not found)
if no matching record exists.
### `query.createMany`
Create multiple records in a single operation.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 6,
"method": "query.createMany",
"params": {
"protocolVersion": 1,
"model": "User",
"data": [
{ "email": "alice@example.com", "name": "Alice" },
{ "email": "bob@example.com", "name": "Bob" }
]
}
}
```
**Response:** `MutationResult` with `count` reflecting the number of inserted rows.
### `query.update`
Update a record matching a filter.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 7,
"method": "query.update",
"params": {
"protocolVersion": 1,
"model": "User",
"filter": { "id": 1 },
"data": { "name": "Updated Name" }
}
}
```
**Response:** `MutationResult` with `count` and optional `data` (when
the dialect supports `RETURNING`).
### `query.delete`
Delete a record matching a filter.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 8,
"method": "query.delete",
"params": {
"protocolVersion": 1,
"model": "Post",
"filter": { "id": 99 }
}
}
```
**Response:** `MutationResult` with `count`.
### `schema.validate` *(reserved — not yet implemented)*
Validate a schema string without applying it.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 9,
"method": "schema.validate",
"params": {
"protocolVersion": 1,
"schema": "model User { id Int @id\n name String }"
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 9,
"result": {
"valid": true,
"errors": null
}
}
```
## Error Codes
### Standard JSON-RPC Errors
| `-32700` | Parse error | Invalid JSON received |
| `-32600` | Invalid Request | Invalid JSON-RPC request object |
| `-32601` | Method not found | Unknown method name |
| `-32602` | Invalid params | Invalid method parameters |
| `-32603` | Internal error | Internal JSON-RPC error |
### Nautilus Error Codes
| `1000-1999` | Schema/Validation | `1000` Schema validation<br>`1001` Invalid model<br>`1002` Invalid field<br>`1003` Type mismatch |
| `2000-2999` | Query Planning | `2000` Query planning<br>`2001` Invalid filter<br>`2002` Invalid orderBy<br>`2003` Unsupported operation |
| `3000-3999` | Database Execution | `3000` Database execution<br>`3001` Connection failed<br>`3002` Constraint violation<br>`3003` Query timeout |
| `9000-9999` | Internal Engine | `9000` Internal error<br>`9001` Unsupported protocol version<br>`9002` Invalid method<br>`9003` Invalid request params |
**Error Response Example:**
```json
{
"jsonrpc": "2.0",
"id": 4,
"error": {
"code": 1001,
"message": "Invalid model: UnknownModel"
}
}
```
## Versioning Policy
### Backward Compatibility
- **Minor changes** (new optional fields, new methods): No version bump, clients ignore unknown fields
- **Breaking changes** (renamed/removed fields, changed semantics): Protocol version increment
### Client Compatibility
Clients should:
1. Send handshake with their protocol version
2. Check engine's protocol version in response
3. Abort if versions are incompatible
### Migration Path
When protocol version 2 is released:
- Engine will support both v1 and v2 simultaneously (for a transition period)
- Clients send their version in every request
- Engine responds according to requested version
## Transport
### Line-Delimited JSON
Each request and response is a single JSON object on one line, terminated by `\n`:
```
{"jsonrpc":"2.0","id":1,"method":"engine.handshake","params":{"protocolVersion":1}}\n
{"jsonrpc":"2.0","id":1,"result":{"engineVersion":"0.1.0","protocolVersion":1}}\n
{"jsonrpc":"2.0","id":2,"method":"query.findMany","params":{"protocolVersion":1,"model":"User"}}\n
```
### Multiplexing
The protocol supports concurrent in-flight requests:
- Each request has a unique `id`
- Responses may arrive out-of-order
- Clients match responses to requests by `id`
### Logging
The engine writes:
- **stdout**: JSON-RPC responses only (one per line)
- **stderr**: Debug logs, warnings, errors (not JSON)
Clients should:
- Read stdout for responses
- Optionally capture stderr for debugging
## Usage Example
```rust
use nautilus_protocol::*;
use serde_json::json;
// Create a findMany request
let request = RpcRequest {
jsonrpc: "2.0".to_string(),
id: Some(RpcId::Number(1)),
method: QUERY_FIND_MANY.to_string(),
params: serde_json::to_value(FindManyParams {
protocol_version: PROTOCOL_VERSION,
model: "User".to_string(),
args: Some(json!({
"where": { "email": { "contains": "test" } }
})),
}).unwrap(),
};
// Serialize to JSON line
let line = format!("{}\n", serde_json::to_string(&request).unwrap());
// Write to engine stdin...
// Read response from engine stdout...
```
## License
MIT or Apache-2.0