Abstract
This document defines extensions to the Durable Streams Protocol. It adds bucket namespacing, snapshot and bootstrap semantics, and multipart bootstrap delivery. These extensions are general-purpose and applicable to any append-only stream workload, including CRDT synchronization, event sourcing, and agent session replay. All base protocol semantics remain in effect. Where this document is silent, the base protocol governs.Terminology
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174].Table of Contents
1. Buckets
Every stream belongs to a bucket. The stream URL takes the form{base_url}/ds/{bucket_id}/{stream_id}.
1.1. Identifier Constraints
bucket_id: MUST match^[a-z0-9_-]{4,64}$. Globally unique within the service.stream_id: Any UTF-8 string. MUST NOT exceed 122 bytes. MUST NOT contain/,\0, or... On the bucketed/ds/surface, the combined{bucket_id}/{stream_id}key MUST also not exceed 122 bytes.- On the bucketed
/ds/surface, the literal local stream IDstreamsis reserved for the bucket listing endpoint.
1.2. Create Bucket
PUT /ds/{bucket_id}/{stream_id} MUST NOT implicitly create a bucket.
Response Codes:
201 Created: Bucket created successfully.400 Bad Request:bucket_idis invalid.409 Conflict: Bucket already exists.
1.3. Get Bucket Metadata
200 OK: Bucket exists. Returns a JSON object with at leastbucket_id(string) andstreams(integer, count of streams in the bucket).400 Bad Request:bucket_idis invalid.404 Not Found: Bucket does not exist.
1.4. List Bucket Streams
prefixfilters bucket-local stream IDs by prefix. When omitted, all bucket-local stream IDs are eligible.afteris an exclusive cursor over bucket-local stream IDs.limitdefaults to1000and must be in1..=1000.
streams is sorted lexicographically by stream_id.
Response Codes:
200 OK: Matching streams returned.400 Bad Request:bucket_idor query parameters are invalid.404 Not Found: Bucket does not exist.
1.5. Delete Bucket
204 No Content: Bucket deleted.400 Bad Request:bucket_idis invalid.404 Not Found: Bucket does not exist.409 Conflict: Bucket is not empty.
1.6. Stream Operations
All stream operations defined in the base protocol apply under{base_url}/ds/{bucket_id}/{stream_id}. When the bucket does not exist, the server MUST return 404 Not Found.
2. Snapshots
A snapshot is a materialized representation of a stream’s content from offset-1 (inclusive) to snapshot_offset (exclusive). Snapshots enable clients to skip full replay and resume from a compacted state.
2.1. Offset Conventions
- Offsets are opaque tokens as defined in the base protocol. Clients MUST use server-returned
Stream-Next-Offsetvalues.
/bootstrap as message sequences MUST use one consistent retained-message boundary model across ordinary reads, snapshot-offset validation, and bootstrap responses. This extension does not require any particular binary framing format.
Tonbo Stream-specific: Tonbo Stream represents offsets (except the reserved value -1) as zero-padded decimal strings denoting cumulative payload byte boundaries. Servers MUST maintain consistency between numeric semantics and lexicographic ordering. Clients MAY parse two non-reserved offset tokens as integers and subtract them to compute the cumulative payload bytes between two boundaries. The reserved offset -1 is treated as 0 in this arithmetic.
2.2. Publish Snapshot
[-1, snapshot_offset).
Request Headers:
Content-Type: The content type of the snapshot blob. Servers MUST store this value and return it on subsequent reads. Defaults toapplication/octet-stream.
204 No Content: Snapshot published successfully.400 Bad Request:snapshot_offsetis invalid or not aligned to a committed message boundary.404 Not Found: Stream does not exist.409 Conflict:snapshot_offsetexceeds the current tail, or the server cannot produce a consistent view at that offset.410 Gone:snapshot_offsetis older than the current earliest retained offset.413 Payload Too Large: Snapshot exceeds the server’s size limit.
snapshot_offset as the new earliest retained offset. For any offset less than snapshot_offset (including -1), the server MUST return 410 Gone, forcing clients to re-initialize via /bootstrap.
Publishing a new snapshot replaces the previously visible snapshot immediately. The superseded snapshot MAY be garbage-collected asynchronously after the new snapshot becomes visible. Clients MUST NOT depend on older snapshots remaining readable after overwrite.
Concurrency:
Snapshot creation MUST NOT block concurrent appends. The snapshot’s consistency boundary is snapshot_offset — updates at or beyond that offset MUST NOT be folded into the snapshot.
2.3. Read Latest Snapshot
307 Temporary Redirectwhen a latest snapshot exists. TheLocationheader points to{stream_url}/snapshot/{snapshot_offset}.404 Not Foundwhen no snapshot exists.
2.4. Snapshot Metadata on HEAD
Stream-Snapshot-Offset when a latest visible snapshot exists for the stream.
Response Headers:
Stream-Snapshot-Offset: The latest visible snapshot offset.
- Absence of
Stream-Snapshot-Offsetmeans the stream has no visible snapshot. - When present, the value MUST refer to the same latest visible snapshot that
GET {stream_url}/snapshotwould resolve to.
2.5. Read Snapshot
Content-Type: The content type stored at publish time.Stream-Snapshot-Offset: The snapshot offset.Stream-Next-Offset: The next offset after the snapshot.Stream-Up-To-Date: Boolean indicating whether the stream has updates beyond the snapshot.
200 OK: Snapshot exists.404 Not Found: Snapshot does not exist or has been garbage-collected.
2.6. Delete Snapshot
snapshot_offset, subject to bootstrap safety rules.
Response Codes:
404 Not Found: Snapshot does not exist or has been superseded.409 Conflict:snapshot_offsetrefers to the latest visible snapshot and cannot be deleted because/bootstrapwould become incomplete.
404 Not Found. In Tonbo Stream’s current implementation, only the latest snapshot is reachable, and it is always protected from deletion (409), so DELETE effectively always returns 404 or 409.
3. Bootstrap
Bootstrap provides single-request initialization: a snapshot (if any) plus all retained updates after the snapshot point, returned as a single ordered response that preserves per-message content types.3.1. Request
/bootstrapis a one-shot initialization endpoint. It does not define any query parameters of its own.- Servers SHOULD reject any
livequery parameter on/bootstrapwith400 Bad Request.
3.2. Response
Response Headers:Content-Type: multipart/mixed; boundary=<token>Stream-Snapshot-Offset: The snapshot offset, or-1if no snapshot exists.Stream-Next-Offset: The next offset after all returned data.Stream-Up-To-Date: Boolean.
multipart/mixed entity. Each MIME part is one logical bootstrap message. The multipart boundary, not any outer binary framing, defines the bootstrap message boundaries.
- First part: Snapshot message. If a snapshot exists, the part body MUST be the raw snapshot bytes and the part
Content-TypeMUST equal the snapshot blob’s stored content type. - Subsequent parts: Retained updates after the snapshot point, one update message per part. Each part body MUST be exactly one retained update message, and the part
Content-TypeMUST be the content type of that update. For streams with a fixed stream-level content type, all update parts will normally share that value.
Stream-Snapshot-Offset MUST be -1, and that empty part’s Content-Type MUST be application/octet-stream. Update parts then begin at the earliest retained offset.
Servers MUST NOT wrap the entire bootstrap response in an outer binary framing container. Each update part contains exactly one retained update message from the stream. Implementations MUST preserve the same retained-message boundaries they use for ordinary reads and snapshot-offset validation, and they MUST NOT reinterpret or reframe binary payloads when constructing bootstrap parts.
Example:
200 OK: Bootstrap data returned.404 Not Found: Stream does not exist.
3.3. Follow-Up Live Reads
Bootstrap is one-shot only:/bootstrapreturns an ordered initialization payload once./bootstrapdoes not supportlive=sseorlive=long-poll.- After applying the bootstrap response, clients MUST continue tailing through the ordinary stream read APIs using the returned
Stream-Next-Offset. - Clients SHOULD prefer
GET {stream_url}?offset=<offset>&live=sseand fall back toGET {stream_url}?offset=<offset>&live=long-pollwhen SSE is unavailable.
3.4. Compatibility
- Regular
GET {stream_url}?offset=...MUST NOT return snapshot bytes. Snapshots are only delivered through/bootstrap. - When a server returns
410 Gonefor a read (because retention has advanced), clients SHOULD call/bootstrapto rebuild state, then continue tailing from the returnedStream-Next-Offsetthrough the ordinary read APIs. - If a server has begun returning
410 Gonefor a stream (i.e., earliest retained offset >-1), the server MUST ensure/bootstrapis available and can provide a snapshot covering that earliest retained offset. - Clients MUST parse
/bootstrapas an ordered message sequence. Message boundaries come from MIME parts.
4. Ordinary SSE Compatibility
Ordinary SSE behavior, including binary stream handling, is defined bydocs/specs/durable-stream.md.
- For binary streams, ordinary
event: datapayloads are raw base64 text and the response MUST includestream-sse-data-encoding: base64. - Extension endpoints MUST NOT redefine the payload shape of ordinary binary SSE
event: dataframes unless they document an explicit endpoint-local contract.
5. URL Encoding
snapshot_offset values originate from Stream-Next-Offset and MAY contain characters requiring URL encoding. Clients and servers MUST apply standard URL encoding/decoding when using offset values in path segments.
