Prisma Streams is a Bun + TypeScript implementation of the Durable Streams HTTP protocol.
It provides:
- a full server mode with SQLite WAL storage, segmenting, upload, and index maintenance
- a trusted local mode for Prisma development workflows
- a stream profile model that cleanly separates durable storage semantics from payload structure
The canonical documentation index is docs/index.md. The dedicated stream profile reference is docs/stream-profiles.md.
The system now has three separate concepts:
- a stream is the durable append-only storage object with Durable Streams semantics
- a profile defines the semantic contract of that stream
- a schema defines payload structure
Short rule:
- profile = semantics
- schema = structure
More concretely:
- the stream owns ordered append/read behavior, offsets, and durable storage
- the profile owns semantic behavior, profile-specific endpoints, and profile-specific runtime configuration
- the schema owns JSON validation, version boundaries, lenses, and routing-key extraction
Profiles sit on top of the durable stream engine. They do not replace streams.
Built-in profiles are implemented under src/profiles/. The core engine
resolves a profile definition and dispatches through its hooks instead of
branching on profile kinds in request or background processing paths.
Profile-specific behavior must live in a dedicated profile module such as
src/profiles/stateProtocol.ts. The supported extension model is:
- implement the profile in its own file or subdirectory under
src/profiles/ - register it once in
src/profiles/index.ts - let core paths call profile hooks instead of adding
if (profile.kind === "...")checks
Every stream has a profile.
- if you declare a profile, that is the stream's profile
- if you create a stream without declaring a profile, the server treats it as a
genericstream
This means old or unconfigured streams still have a clear meaning: they are
plain generic durable streams.
Current built-ins:
evloggenericmetricsstate-protocol
Planned next built-ins:
queue
evlog is the built-in profile for request-centric wide-event logging.
It means:
- the stream content type must be
application/json - JSON appends are normalized into a canonical evlog envelope
- sensitive context keys are redacted before durable append
- installing the profile also installs the canonical evlog schema registry and default search fields
- the default routing key is
requestId, withtraceIdfallback
V1 evlog uses the normal stream append and read APIs. It does not add a local
SQLite observability index, and it does not require a separate manual
/_schema call for the default search-ready setup.
generic means:
- plain ordered append-only storage
- optional user-managed schema validation
- optional schema-managed routing-key extraction
- no profile-owned payload envelope
- no profile-specific endpoints
generic is intentionally narrow. It is the baseline durable stream and the
automatic default when no other profile is declared.
state-protocol is the built-in profile for JSON streams that carry State
Protocol change records and expose the live /touch/* API surface.
It means:
- the stream content type must be
application/json - the payload semantics are State Protocol records
- touch configuration belongs to the profile
/touch/*exists only whentouch.enabled=true
Schemas remain optional on state-protocol streams. If present, they validate
the JSON payload shape, but they do not own live/touch behavior.
State Protocol is a profile, not a schema feature, because it defines stream semantics and profile-owned endpoints, not just JSON shape.
metrics is the built-in profile for canonical metric interval streams.
It means:
- the stream content type must be
application/json - JSON appends are normalized into the canonical metrics interval envelope
- installing the profile also installs the canonical metrics schema/search and default rollups
- the canonical routing key is
seriesKey - metrics streams use the
.mblkmetrics-block family in addition to.agg
The internal __stream_metrics__ stream is created with this profile
automatically.
What belongs in a profile:
- semantic meaning of records
- profile-specific runtime config
- profile-specific endpoints
- profile-owned indexes or projections
- future canonical envelopes for specialized stream types
What belongs in a schema:
- JSON validation
- version boundaries
- lens-based read promotion
- routing-key extraction rules
- schema-owned
searchfield declarations used byGET ...?filter=...andPOST .../_search
What does not belong in /_schema:
- profile selection
- touch configuration
- State Protocol runtime behavior
- evlog envelope normalization or redaction
The supported model is strict: /_profile manages profile semantics,
/_schema manages schema evolution.
Indexed JSON streams can also use the main read path with filter=....
Filters are limited to schema search.fields, use the internal exact family
and bundled .cix companion sections to prune sealed history where possible,
and still scan the local unsealed tail for correctness. If an exact field
definition changes on an existing stream, the old exact state is treated as
stale and the read path falls back cleanly until async rebuild catches up. One
filtered response stops after 100 MB of examined payload bytes and reports that
cap in response headers.
JSON streams with search configured also support _search:
POST /v1/stream/{name}/_searchGET /v1/stream/{name}/_search?q=...
The current _search surface supports fielded exact keyword queries, keyword
prefix, typed equality/range, has:field, bare terms over
search.defaultFields, and quoted phrase queries on text fields with
positions=true.
Schema-owned rollups are also available through:
POST /v1/stream/{name}/_aggregate
Rollups are configured under search.rollups, stored as object-store-native
bundled companion sections, and used for aligned time windows with raw source
scans for partial edges and uncovered ranges.
For stream management UIs, the current per-stream inspection surface is:
GET /v1/streamsSummary list view with name, offsets, expiry, and profile kind.GET /v1/stream/{name}/_profileFull typed profile resource.GET /v1/stream/{name}/_schemaFull schema registry, including profile-owned canonical registries such as the auto-installed evlog schema.GET /v1/stream/{name}/_index_statusCurrent manifest, segment, and async index/search-family status for that stream.GET /v1/stream/{name}/_detailsCombined stream summary, includingstream.total_size_bytes, full profile resource, full schema registry, nested index status, storage accounting, and node-local object-store request counters in one response. This endpoint also supports conditional long-polling withIf-None-Match,live=long-poll, andtimeout=..., and only wakes when the stream head or descriptor-visible metadata changes.
/_details.stream is the full per-stream summary shape. In practice that means
it includes the stream head and lifecycle fields a UI usually needs for an
active stream page, including:
created_atexpires_atepochnext_offsetsealed_throughuploaded_throughtotal_size_bytes
For a storage/cost popover, /_details also includes:
storage.object_storageUploaded bytes and object counts split into segments, indexes, and manifest/schema metadata.storage.local_storageCurrent local retained bytes split into WAL, pending sealed segments, caches, and shared SQLite footprint.storage.companion_familiesBundled companion bytes split intocol,fts,agg, andmblk.object_store_requestsNode-local per-stream object-store request counters, split into puts and reads, plus a per-artifact breakdown.
That means a GUI can create streams, inspect the active profile and schema,
show current indexing progress, and edit profile/schema configuration through
the normal API surface. A charting UI can additionally use /_aggregate for
time-window summaries driven by schema search.rollups.
For an active stream page, the recommended pattern is:
- fetch
GET /v1/stream/{name}/_details - keep the returned
ETag - reissue
GET /v1/stream/{name}/_details?live=long-poll&timeout=30swithIf-None-Match: <etag>
The server returns:
200with a new body and newETagwhen events or descriptor metadata have changed304when the timeout expires without a visible change
Profiles are managed through a dedicated subresource:
GET /v1/stream/{name}/_profilePOST /v1/stream/{name}/_profile
Example default response:
{
"apiVersion": "durable.streams/profile/v1",
"profile": { "kind": "generic" }
}Explicit generic declaration:
{
"apiVersion": "durable.streams/profile/v1",
"profile": { "kind": "generic" }
}State Protocol profile with touch enabled:
{
"apiVersion": "durable.streams/profile/v1",
"profile": {
"kind": "state-protocol",
"touch": {
"enabled": true,
"onMissingBefore": "coarse"
}
}
}Evlog profile with redaction:
{
"apiVersion": "durable.streams/profile/v1",
"profile": {
"kind": "evlog",
"redactKeys": ["sessiontoken"]
}
}To switch a stream back to the baseline behavior, set profile to
{ "kind": "generic" }.
The stored profile is kept in stream metadata.
streams.profilestores the profile kindstream_profiles.profile_jsonstores non-generic profile configurationNULLprofile metadata means there is no explicit stored entry and the stream is treated asgeneric
This keeps storage simple while still letting runtime code assume a profile always exists.
In full mode today:
- append ACK means the write is durable in local SQLite
- object-store durability happens only after segment upload and manifest publication
--bootstrap-from-r2 rebuilds published stream history and metadata from
manifest, segment, and schema objects in object storage. It does
not restore transient local SQLite state such as the unuploaded WAL tail,
producer dedupe state, or runtime live/template state.
A stream becomes recoverable from object storage after its first manifest is published.
Not implemented today:
- an object-store-acked mode that would batch writes and ACK only after persistence to R2
- a cluster quorum mode that would ACK only after a durability quorum accepts the write
The supported behavior is:
- use
/_profileto choosegeneric,state-protocol, orevlog - use
/_schemaonly for schema validation, routing-key config, and schema evolution - use
/touch/*only onstate-protocolstreams with touch enabled - use normal JSON appends on
evlogstreams to store canonical evlog events
Legacy compatibility branches are intentionally not part of the supported surface.
- docs/index.md for the documentation map
- docs/overview.md for product and package overview
- docs/stream-profiles.md for the full stream / profile / schema reference
- docs/durable-streams-spec.md for the HTTP protocol contract
- docs/live.md for the State Protocol live/touch model
- docs/schemas.md for schema registry and lens behavior