Choose the right seed packet.
This is the public SDK surface, with the behavior that matters when code reaches a durable database. Flower is unreleased; APIs, storage formats, and wire contracts may change without backward compatibility.
npm install @flower-js/sdk
npx flower --help| API | Contract |
|---|---|
@flower-js/sdk | Application declarations, contexts, JSON identity, the client, and client/watch types. Safe to bundle for QuickJS; client-only code is removed when unused. |
@flower-js/sdk/client | The HTTP/SSE client and its request/result types. Uses the host’s Fetch API by default; usable in browsers and Node. |
@flower-js/sdk/temporal | Expiring records and fenced worker queues, implemented through ordinary TypeScript context operations. |
@flower-js/sdk/scheduler | Durable delayed callbacks with explicit retry policy and private maintenance handlers. |
@flower-js/sdk/crypto | Native NaCl and JWT facades and their types, for deployed Flower TypeScript. No native crypto provider is installed in the external Node/browser client. |
@flower-js/sdk/http2 | Node-only pooled HTTP/2 transport over cleartext h2c or verified TLS. Keep this import outside deployed application bundles. |
@flower-js/sdk/bundle | Node-only build/load/write helpers using esbuild. Keep these outside deployed applications. |
@flower-js/sdk/package.json | Package metadata. There are no supported imports into internal SDK files. |
Browse the SDK source on GitHub. The server is a separate Rust executable and needs no Node process. The SDK requires Node 22.18+ for its CLI and Node-only tools. Inside this repository, run npm ci and npm run build before using the package’s own imports. A checkout’s server is ./target/release/flower; npx flower is the SDK command, not the server.
Values, identity, and purity.
| API | Contract |
|---|---|
Json | The durable value format: null | boolean | number | string | Json[] | { [key: string]: Json }. Numbers must be finite. TypeScript generics improve ergonomics; runtime validation still applies. |
canonicalJson(value: unknown): string | Validates a value and serializes it with sorted object keys. Arrays retain order. Used for stable instance identities and equality; -0 normalizes to 0. Throws TypeError for cycles, nesting beyond 128, non-finite numbers, BigInt, undefined, functions, symbols, accessors, hidden properties, array holes/named properties, or non-plain objects. |
QuickJS supports more JavaScript values than Flower stores. BigInt, Map, Set, Date, typed arrays, and class instances do not cross the database/HTTP boundary automatically. Encode them explicitly as JSON, such as decimal strings with a versioned tag. Numbers outside JavaScript’s exact integer range remain floating-point numbers; use safe integer units where exact arithmetic is required.
Callbacks are synchronous. Return a finite JSON value, including explicit null for “nothing”; do not return a Promise or undefined. No filesystem, network, Node globals, timers or ambient wall clock is available inside application callbacks. Explicit native crypto capabilities provide mutation-only application randomness; shared-key handles are opaque invocation-local capabilities with no observable random token, as described below. Use ctx.now() for database time and external workers for effects. Each callback starts from an isolated QuickJS/Wasm image. Mutating module globals cannot create durable state or communicate with another callback.
The compiled module and Wasmtime image are reused, while guest heaps are isolated. Rust owns records, indexes, dependency tracking, staging, and Raft application. More expensive callbacks, large results, nested derived evaluations, and broad scans still cost CPU and memory; isolation does not make arbitrary functions incremental.
Collections, equality, and ordered scans.
| API | Contract |
|---|---|
collection<T = Record<string, Json>>(name: string): Collection<T> | Creates a reference to a namespaced source collection; it does not contact a server. Names must be nonempty. A record key is a nonempty string, and missing records read as null. |
ref.index(name, fields): Collection<T> | Declares one or more distinct nonempty field names and returns the same reference for chaining. Duplicate index names are rejected. Declare before define(), which snapshots the schema. |
ref.by(name).eq(value: Json): Query<T> | Builds an equality query for a declared index name. One field accepts its JSON equality value; multiple fields require an array of exactly that length, in field order. Unknown indexes and wrong tuple arity throw TypeError. |
Collection<T> | Readonly kind: "collection", name, and indexes: Record<string, readonly string[]>, plus index and by. This is a definition reference, not a client-side table API. |
Query<T> | Readonly kind: "query", collection: string, fields: readonly string[], value: Json. Its optional __recordType exists only for typing. |
ref.by(name).range(options): RangeQuery<T> | Builds an ordered bounded query. Declare the collection in define({collections}) for persistent index seeks; otherwise Rust scans source records and retains at most limit + 1 candidates. |
IndexScalar | null | boolean | number | string; numbers must be finite. Order: null, false, true, numeric ascending, then UTF-16 strings. Composite tuples compare field by field; source keys break ties in UTF-16 order. Missing or nonscalar fields are absent from the ordered index but can still participate in JSON equality queries. |
ScanOptions | Optional index: string selects an index declared on the collection reference; omission walks source keys. prefix: readonly IndexScalar[] fixes initial fields; gt or gte and lt or lte bound the next field. Full prefixes cannot also have bounds, and duplicate lower/upper alternatives are rejected. Without an index, bounds must be strings and prefix is empty or one exact source key. reverse: boolean defaults false and reverses the complete tuple and source-key order. offset and limit are nonnegative safe integers, applied in that order after constraints and ordering. Offset defaults to zero; omitted limit returns all remaining matches, and zero limit returns no rows. Unknown options and index names are rejected. |
RangeOptions | Required positive safe integer limit. Optional prefix: readonly IndexScalar[] fixes initial fields; gt or gte and lt or lte bound the next field. No bounds after a full tuple prefix, no duplicate lower/upper alternatives. Optional reverse: boolean defaults false; after: string continues a returned cursor. Operator evaluation/result budgets still apply. |
RangeQuery<T> | Readonly kind: "range", collection, fields, options; optional phantom __recordType for typing. |
RangePage<T> | rows: {key: string, value: T}[] and cursor: string | null. Null means no later row in this invocation’s snapshot. Cursors bind the collection, fields, prefix, bounds, and direction; changing limit is allowed. They are opaque traversal positions, not capabilities or retained snapshots. Concurrent edits between pages can skip or repeat moved rows. |
CollectionManifest | Readonly name: string and indexes: Readonly<Record<string, readonly string[]>>; the serialized collection schema stored in a module manifest. |
import { collection, define, query } from "@flower-js/sdk";
const orders = collection<{ tenant: string; state: string; cents: number }>("orders")
.index("byTenantState", ["tenant", "state"]);
const pending = query("orders.pending", (ctx, tenant: string) =>
ctx.query(orders.by("byTenantState").eq([tenant, "pending"])));
export default define({ collections: [orders], http: { pending } });Include indexed collections in define({ collections }) to maintain durable indexes. Omitting a collection keeps scan-based query behavior. Indexes support JSON equality and ordered scalar ranges; they do not enforce uniqueness constraints or arbitrary projections. Missing fields do not match null. Object key order does not change equality. Results retain source-key order, including Unicode keys.
For a due timer index ["state", "dueAt"], use ctx.range(timers.by("due").range({prefix: ["pending"], lte: ctx.now(), limit: 1})). Empty ranges also track phantom dependencies. This implementation conservatively invalidates all ordered readers of an index when any scalar-indexed row changes, including value-only updates; it does not yet track exact intervals.
Rows and index entries commit together. Queries inside mutations see staged writes. Derived equality queries depend on the matching bucket and records: inserting into an empty bucket invalidates its readers; unrelated buckets do not. Direct deployment backfills added indexes and removes dropped entries in one candidate under normal budgets. For durable bounded progress, use staged deployment; operators explicitly advance pages and activate the result, with no automatic background backfill. See index and reducer operation details.
What application code can do.
| API | Contract |
|---|---|
Context | The read-only callback context; also exported under the name QueryContext. Context objects belong to one invocation and cannot be persisted. |
ctx.now(): number | Server milliseconds fixed for the invocation; records a time dependency. Time is bounded by committed time and the local monotonic floor. Uncommitted query time can move backward after leader changes; synchronize host clocks. |
ctx.get(collection, key): T | null | Read one source record. Null means missing; if null is stored as the value, the return alone does not distinguish it from absence. |
ctx.get(derived, args): Value | Evaluate or reuse the named derived instance for canonical JSON arguments. Tracks a dependency. Derived errors propagate when read. |
ctx.scan(collection, options?: ScanOptions): { key: string; value: T }[] | Read source rows with optional index selection, constraints, reverse traversal, offset, and limit. Includes pending mutation writes. Source-key scans include every row and depend on the collection; indexed scans omit missing/nonscalar fields and conservatively depend on the index. Declare the collection in define({collections}) for persistent ordered index seeks; undeclared indexes use a Rust scan fallback. Large offsets still require walking past matching rows; use ctx.range for cursor continuation. |
ctx.query(query): T[] | Read equality matches, including pending mutation changes. Returns values, not keys. Use an appropriate durable index to avoid broad scans. |
ctx.range(query): RangePage<T> | Read an ordered bounded page with source keys and values, including staged mutation changes. See the scalar range/cursor contract above. |
MutationContext | Extends Context with the following writes. Query callbacks cannot write, even if a type cast pretends otherwise. |
ctx.set(collection, key, value): void | Replace a source record in the transaction overlay. All source changes and dependent materialized updates commit atomically. |
ctx.delete(collection, key): void | Remove a source record; deleting a missing key is harmless. |
ctx.materialize(derived, args): void | Retain a derived instance as a root. Its reachable dependency graph is maintained when affected data changes. |
ctx.unmaterialize(derived, args): void | Remove that root. Cells no longer reachable from any materialized root can be collected; source rows are not deleted. |
Clock-independent query results can be reused across unrelated revisions when their dependency certificates remain valid. Certificates cover point reads (including absence), collection/index membership, returned indexed rows, derived outcomes, and code/schema/managed-policy identities. Fresh hits still require the normal quorum fence; every public hit still runs authorization and keys by the complete principal. Time and managed-crypto dependencies remain uncacheable. Certificate memory shares the query cache budget and does not retain old JSON payloads. FLOWER_QUERY_CACHE_BYTES defaults to 16 MiB per logical database; FLOWER_QUERY_FLIGHT_BYTES defaults to 512 KiB for concurrent shared-evaluation metadata. Zero disables either optimization; there is no fixed entry-count limit. Certificates are process-local and reconstructed from applied state after restart or snapshot installation; they do not change durability or permit a hit against a different read fence.
Reactive parents can skip their callback when their only potentially changed derived child returns an equal outcome. Changed dependency edges, errors, cycle validation, and garbage collection still apply. Several potentially changed branches fall back to application evaluation order, so stale branches are not speculatively evaluated.
A mutation either publishes its entire resulting state or none of it. Throws, invalid results, or exhausted budgets discard staged writes. Ordinary query evaluation does not install durable roots. Use materialization for frequently read values whose maintenance cost is justified; leave expensive, infrequently read rankings unmaterialized. Cycles and excessive graph depth fail evaluation. Dynamic dependencies are tracked each time a callback runs.
Definitions and the public method table.
| API | Contract |
|---|---|
derive<Args, Value>(name, compute): Derived<Args, Value> | Pure reactive function (ctx: Context, args: Args) => Value. Register it in definitions and read it through ctx.get. It is not directly callable over HTTP. |
query<Args, Value>(name, compute, options?): QueryMethod<Args, Value> | Read-only method (ctx: QueryContext, args: Args) => Value. Fresh reads are the default; expose an HTTP alias explicitly. |
mutation<Args, Value>(name, compute): MutationMethod<Args, Value> | Atomic method (ctx: MutationContext, args: Args) => Value. Validate caller inputs and authorization in code before reading or writing protected data. |
QueryConsistency | Exactly "linearizable" | "replica-local". |
QueryOptions | Optional readonly consistency: QueryConsistency. Omitted or linearizable means fresh. Unknown options or policies are rejected. |
Derived<Args = Json, Value = Json> | Readonly kind: "derived", name, compute, and optional aggregate: AggregateMetadata. |
QueryMethod<Args = Json, Value = Json> | Readonly kind: "queryMethod", name, compute, and optional consistency. |
MutationMethod<Args = Json, Value = Json> | Readonly kind: "mutationMethod", name, and compute. |
Definition | Union of Derived, QueryMethod, MutationMethod, and TransactionMethod. |
HttpMethod | Union of QueryMethod, MutationMethod, and TransactionMethod; Derived is excluded. |
define(config: ModuleConfig = {}): FlowerModule | Freezes and validates the module manifest. HTTP methods and maintenance handlers are registered automatically. Conflicting definitions with the same name are rejected; reusing the same reference is allowed. |
ModuleConfig | Optional collections: readonly Collection[], definitions: readonly Definition[], http: Record<string, HttpMethod>, and maintenance: MutationMethod | MaintenanceHandlers. Unknown fields, getters, and non-plain objects are rejected. Optional keys: readonly ManagedKey[] declares native key capabilities; absent/empty means no declarations. Include every managed handle used by callbacks. |
FlowerModule | Readonly collections?: readonly CollectionManifest[], definitions: Record<string, Definition>, http: Record<alias, {name, kind, consistency?}>, and maintenance: MaintenanceManifest | null. HTTP kind is query, mutation, or transaction; only queries carry consistency. Optional keys: readonly ManagedKey[] declares native key capabilities; absent/empty means no declarations. Include every managed handle used by callbacks. |
All application traffic goes through the deployed http table. Definition names do not become public routes automatically. A deployment atomically changes code, aliases, schema, maintenance registration, and recomputed materialized state. A rejected deployment leaves the previous application active.
Fresh queries on any replica obtain a quorum-confirmed position and wait for local application. Concurrent queries can share fences and run independently of the writer. Replica-local queries read one coherent locally applied committed snapshot without a per-read fence; state, code, and aliases may lag without a bound, and switching replicas can move backward in revision. This is an explicit application policy, not a client switch. A restarted node whose durable log extends beyond its recovered application checkpoint first requires a quorum-confirmed recovery fence and local replay, including before replica-local reads and watches. After this shared startup barrier clears, replica-local reads again require no per-read quorum.
Incremental aggregates.
| API | Contract |
|---|---|
aggregate<Row, Value>(name, options: AggregateOptions<Row, Value>): Aggregate<Value> | A derived value keyed by the JSON equality value of one source index. Retained accumulators process row deltas rather than rereading every matching row. |
Aggregate<T = Json> | Extends Derived<Json, T> and has required readonly aggregate: AggregateMetadata. |
AggregateMetadata | Readonly collection: string and fields: readonly string[]. |
AggregateOptions<Row, Value> | Required source: Collection<Row>, index: string, initial(group: Json): Value, add(value, row, key: string, group: Json): Value, and remove(value, row, key: string, group: Json): Value. The source/index must be declared in the module. |
import { aggregate, collection } from "@flower-js/sdk";
const lines = collection<{ shop: string; cents: number }>("lines")
.index("byShop", ["shop"]);
const total = aggregate("shop.total", {
source: lines, index: "byShop",
initial: () => 0,
add: (sum, row) => sum + row.cents,
remove: (sum, row) => sum - row.cents,
});
// Register collections: [lines], definitions: [total].
// In a mutation: ctx.materialize(total, shop);
// In any callback: ctx.get(total, shop);Reducers receive no database context. They must be deterministic and order-independent, with remove undoing add. Flower cannot prove that algebra for user code. Use safe integer units for exact totals; floating-point sums can depend on history. Updating a row removes its old contribution and adds its new contribution; moving equality groups updates both accumulators.
The first evaluation scans matching rows. Retained accumulators subsequently process changed rows. Unmaterialized query-only aggregates may rebuild on each query. A reducer error is stored as a derived error; a later relevant change rebuilds rather than reusing a partial accumulator. Redeploying rebuilds retained aggregates. Backfill, initialization, and rebuilds are synchronous and budgeted. Index entries, accumulator state, rows, and dependencies replicate and recover together.
Call methods from outside the database.
import { FlowerClient, FlowerError } from "@flower-js/sdk/client";
const db = new FlowerClient("http://node-1:7101", {
queryUrls: ["http://node-1:7101", "http://node-2:7101", "http://node-3:7101"],
});
const requestId = crypto.randomUUID(); // Persist before sending.
try {
await db.mutate("orders.save", { id: "42", cents: 900 }, { requestId });
} catch (error) {
if (error instanceof FlowerError) console.error(error.status, error.code);
// After an uncertain response, retry the same body and requestId.
}| API | Contract |
|---|---|
new FlowerClient(url?, options?) | Default URL http://127.0.0.1:7101. HTTP or HTTPS required. Readonly url exposes the normalized entry endpoint, which may be any reachable member. The server handles leader routing; the SDK does not retry entry-node failures or reconnect watches. |
constructor(url?, options: FlowerClientOptions = {}) | Construct a client. This does not open an application session or query the cluster. |
initialize(members, options?): Promise<void> | Admin bootstrap with Record<string, string> node IDs to host:port addresses. Only for an uninitialized group; never initialize a restarted member again. |
deploy(bundle, {requestId?, signal?, preparation?}?): Promise<DeploymentReceipt> | Admin deployment to the primary URL. Default preparation: online builds an isolated candidate while old code serves writes, then atomically validates its base revision and publishes. A concurrent write returns DEPLOYMENT_CONFLICT; retry the same ID or choose preparation: blocking for an exclusive writer window. Preparation mode is excluded from retry identity. Save an explicit requestId before sending when retry safety matters. |
query<Args, Value>(name, args = null, options?): Promise<QueryResult<Value>> | POST /v1/query; rotates through queryUrls. The method must be an exposed query. Deployed code selects read consistency. |
mutate<Args, Value>(name, args = null, options?): Promise<MutationResult<Value>> | POST /v1/mutate at the primary URL, with a request ID and optional expected revision. Invokes an exposed mutation or transaction alias. |
call<Args, Value>(name, args = null, options?): Promise<MutationResult<Value>> | POST /v1/call at the primary URL. Deployed code selects query, mutation, or transaction. This method does not rotate through queryUrls, even for a query alias. |
FlowerClientOptions | Optional adminToken: string, fetch: FlowerFetch, queryUrls: readonly string[]. queryUrls must be nonempty if supplied. It changes routing, never consistency. Admin tokens are attached only to admin requests. |
RequestOptions | Optional signal: AbortSignal. |
MutationOptions | Extends RequestOptions with optional requestId: string and expectedRevision: number. Request ID omitted means a new random UUID for each invocation. |
QueryResult<Value = Json> | Required revision: number and value: Value. Revision belongs to the addressed logical database; named partitions preserve it across physical group moves. |
MutationResult<Value = Json> | Extends QueryResult with duplicate: boolean. A receipt replay preserves the original result and revision. |
DeploymentOptions | Optional requestId: string, signal: AbortSignal, preparation: "online" | "blocking". Online is optimistic full preparation, not resumable/chunked backfill; existing time, memory and transaction budgets apply. Blocking pauses writes for preparation; reads retain the prior committed snapshot until cutover. |
DeploymentReceipt | Required revision: number, value: Json, and duplicate: boolean. |
Bundle | Required hash: string and javascript: string; hash is SHA-256 of the exact bundled JavaScript. |
FlowerRequestInit | Flower’s fetch subset: method: "POST", headers: Record<string,string>, body: string, optional signal. |
FlowerFetch | Function (url: string, init: FlowerRequestInit) => Promise<Response>. A custom transport must support JSON responses and a streaming Response body for watches. |
new FlowerError(message, status = 0, code = "FLOWER_ERROR") | Extends Error; readonly numeric status and string code. HTTP failures become FlowerError. Local argument validation can throw TypeError; Fetch/transport failures and cancellation may be ordinary Error/DOMException, so do not assume every failure is FlowerError. |
constructor(message, status = 0, code = "FLOWER_ERROR") | Set the readable message, HTTP status (zero when unavailable), and machine-readable code. |
A lost response does not prove a mutation failed. Preserve the exact request ID, arguments, and expected revision when retrying an uncertain call. Changing content under the same ID yields REQUEST_ID_REUSED; a stale expected revision yields REVISION_CONFLICT. A full admission queue returns 503. Cancellation stops waiting, not an already committed mutation. Receipts are durable. Without initialized retry retention they remain indefinite; initialized histories use explicit epoch floors and authenticated session acknowledgements. Expired or acknowledged requests cannot execute again, even after result collection; see retry retention.
The SDK does not retry failed entry-node connections automatically. Use any reachable member: servers forward writes and deployments to the current leader while retaining request IDs and expected revisions. If that entry member itself is unavailable, select another reachable endpoint. The benchmark’s discovery and retry loop is harness behavior, not a hidden SDK guarantee. Query endpoint rotation also has no automatic retry when one selected endpoint fails.
Move a tenant; keep its identity.
A named partition is an isolated logical database with its own application bundle, data, derived graph, indexes, timers, lease tokens, revision and retry receipts. Multiple partitions share a physical Raft group. Existing tenant keys in the default database do not automatically become movable partitions: choose this boundary when creating a database. Cross-partition reads and cross-group transaction methods are not supported inside named partitions in this first version.
import { FlowerClient } from "@flower-js/sdk/client";
const cluster = new FlowerClient("http://catalog-1:7101", {
adminToken: process.env.FLOWER_ADMIN_TOKEN,
});
await cluster.registerGroup({ id: "west", addresses: ["west-1:7101", "west-2:7101", "west-3:7101"] });
await cluster.registerGroup({ id: "east", addresses: ["east-1:7101", "east-2:7101", "east-3:7101"] });
await cluster.createPartition("tenant-a", "west", { requestId: "create-tenant-a" });
await cluster.waitForPartition("tenant-a");
const tenant = cluster.partition("tenant-a");
// Deploy a bundle through tenant.deploy(bundle), then call its exposed methods.
await cluster.resize(["west", "east"], { requestId: "grow-two-groups" });
console.log(await cluster.layout()); // Durable progress; resize continues after this client exits.| API | Contract |
|---|---|
partition(name): FlowerClient | Build a client at the stable /partitions/{name} gateway prefix. Preserves custom transport, admin token and query endpoint rotation. The server routes by ownership epoch; application requests still invoke only deployed methods. Names must be nonempty and contain no control characters. This is logical isolation, not tenant authentication. |
layout(options?): Promise<ClusterLayout> | Fresh operator view of registered groups, current placements, retained moves and the latest rebalance plan. |
registerGroup(group, options?): Promise<ClusterGroup> | Checks an already provisioned group and records its identity and peer addresses. Repeating identical registration is safe. This does not start replicas or initialize Raft. |
removeGroup(id, options?): Promise<{removed: string}> | Unregister an empty group with no pending ownership/plan references. The designated catalog cannot be removed. Processes and files are left to the operator. |
createPartition(name, group, options?): Promise<PartitionPlacement> | Durably starts an empty partition at epoch 1. Wait for active, then deploy its bundle. Save an explicit operation requestId before sending. |
movePartition(name, destination, options?): Promise<PartitionMove> | Durably starts movement to a different registered group. Returns before completion. The catalog leader recovers interrupted work. |
resize(groupIds, options?): Promise<RebalancePlan> | Balance counts over a nonempty, distinct set of registered groups. Retain existing ownership where possible, move surplus partitions one at a time, and drain groups excluded from the set. Does not balance bytes, CPU, traffic or hot keys. Provision/register new groups first; unregister drained groups afterward. Overlapping plans and manual moves are rejected. Planning examines each partition and checks involved groups before admission; this is linear control-plane work with network requests. Each move rechecks capacity, so later configuration reductions or outages can leave the plan pending until capacity recovers. |
partitionStatus(name, options?): Promise<PartitionPlacement> | Fresh operator lookup, including pending phase, current owner and epoch. |
waitForPartition(name, options?): Promise<PartitionPlacement> | Poll until active. Defaults: timeoutMs 30,000; intervalMs 100. Positive integer milliseconds within the JavaScript timer range. Abort cancels waiting only; errors propagate and timeout raises PARTITION_WAIT_TIMEOUT. A destination can be active while old-source cleanup is still finishing. |
ControlOptions | Optional signal and requestId. Create/move/resize generate a UUID when omitted; explicitly retain it for uncertain retries. These are operation identities, not data-method receipts. |
PartitionWaitOptions | Optional signal, timeoutMs and intervalMs for activation waiting. |
ClusterGroup | {id: string, addresses: string[]}; addresses are distinct host:port peers, without schemes or paths. Registered addresses are immutable until the unused group is removed. These addresses are seeds: servers discover or forward to the current member that leads. Keep a registered member reachable across membership changes. To replace the entire seed list, drain, remove and re-register the group. |
PartitionPlacement | {partition, epoch, owner: ClusterGroup, status: "creating" | "active" | "moving", operation, movement: PartitionMove | null}. |
PartitionMove | {operation, partition, source: ClusterGroup, destination: ClusterGroup, source_epoch, epoch, phase}. Epoch advances on ownership transfer; the partition's application revision and receipts survive the move. |
PartitionMovePhase | "copying" | "freezing" | "importing" | "activating" | "retiring" | "complete". Movement rolls forward; there is no cancel/rollback API. |
RebalanceMove | {partition, source, destination, operation}; source/destination are group IDs. |
RebalancePlan | {operation, groups: ClusterGroup[], moves: RebalanceMove[], next: number, complete: boolean}. next is the move cursor, not a count of migrated bytes. Poll layout to observe the current plan. |
ClusterLayout | {groups: ClusterGroup[], partitions: PartitionPlacement[], moves: PartitionMove[], rebalance: RebalancePlan | null}. Move and plan history are retained; no automatic history collection. |
Operation and failure tradeoffs
Every participating process sets FLOWER_GROUP, FLOWER_CATALOG_GROUP, and FLOWER_GROUPS, a JSON map from bootstrap group IDs to arrays of host:port peers. The map must include itself and the stable catalog. Keep a configured catalog bootstrap peer reachable through membership changes. Initialize each physical group separately; all peers use compatible protocol versions and the same peer credential; operator calls use the separate operator credential. Registered worker groups can then change without restarting the catalog. The catalog's default namespace stores placement metadata; use named partitions for applications.
The source first captures a durable immutable base and keeps serving reads and writes during copying. The destination imports that base into invisible state. The source then freezes, transfers a final difference including record and receipt deletions, and verifies the complete final image before catalog cutover and destination activation. The source becomes a fenced tombstone; its retained base is released. Every step is idempotent and recoverable. In-flight writes either commit before freeze and travel with their receipts, or fail the epoch/status check. Stale owners cannot acknowledge new writes. In-doubt transactions prevent freeze. A failed transfer after freezing remains paused until its required groups regain quorum; failure during copying leaves the source serving. Losing the client does not cancel a move. Logical-partition transaction methods remain supported; unresolved participants or coordinators delay the freeze, while completion and recovery can still proceed.
Other partitions have separate writer actors and state, but still share CPU, Wasm pools, disk bandwidth and their physical Raft log. Large imports can cause contention. Pause duration grows with image size, network/storage speed and failures; it is not guaranteed to be a few milliseconds. The source persists its captured base until retirement and computes the final difference against frozen state, rather than retaining every intermediate update. If that difference is larger than a full image or exceeds FLOWER_PARTITION_TAIL_MAX_BYTES, transfer falls back to a full frozen image. Optional FLOWER_PARTITION_BASE_MAX_BYTES bounds base admission; both settings are unset by default. Migration export still caches a complete encoded base, difference, or fallback image in memory despite chunked transport. Encoding acquires shared Control capacity before retaining source roots, and the cached image holds its byte reservation until replacement or retirement. Difference construction also reserves workspace; exhausted control budgets can delay a move after freeze. This differs from file-backed Raft snapshots: neither mechanism removes the live in-memory database or guarantees a fixed pause duration.
Active routes share a configurable cache: FLOWER_ROUTE_CACHE_MS defaults to 1,000 ms. Expired entries require a fresh catalog lookup; catalog unavailability then blocks named requests and terminates affected watches. This exchanges some catalog dependence for prompt ownership updates. Replica-local data can still lag within the selected owner; cached ownership may lag within this interval. Native replicated epoch checks protect writes and fresh reads independently of cached routing. On migration, an SSE stream ends with an ownership/unavailable error; reconnect through the stable partition client for a new snapshot. There is no event-log replay or automatic SDK reconnect.
Control calls require the operator token. Application partition URLs are not an authentication boundary: put a trusted gateway in front of public deployments and bind callers to allowed partitions. Default-namespace clients use server-side leader routing; named clients also use server-side group routing. Neither requires SDK leader discovery. Retry uncertain mutations with the exact same request ID and body. Preflight checks transfer and control envelopes. Destination HTTP and evaluator budgets must also suit the application workload; heterogeneous leader budgets or later policy reductions can leave a move pending until capacity is restored. The current pizza capacity report measures static groups, not migration overhead.
One watched value, efficient deltas.
| API | Contract |
|---|---|
watch<Args, Value>(name, args = null, options?): AsyncGenerator<QueryResult<Value>> | POST SSE subscription. Reconstructs an initial snapshot and later JSON patches. Yields independent value copies; changing a yielded value cannot corrupt its internal baseline. Equal values are suppressed even when the database revision changes. |
watchDeltas<Args, Value>(name, args = null, options?): AsyncGenerator<WatchDelta<Value>> | Raw snapshot/patch events. Does not reconstruct values for you. Each call selects one read endpoint and starts with a complete snapshot at the producer’s current sequence, which can be nonzero; no replay cursor or automatic reconnect. |
watchPoll<Args, Value>(name, args = null, options?): AsyncGenerator<QueryResult<Value>> | Explicit polling. Selects one endpoint for that iterator and emits revision changes even when values are equal. The interval begins after each completed query. |
WatchOptions | Optional signal plus maxEventBytes (17 MiB), maxValueBytes (16 MiB), and maxPatchOperations (256). Positive safe integers; local client budgets are never sent to the server. The deprecated intervalMs field is rejected by SSE methods; use watchPoll. |
WatchPollOptions | Optional signal and intervalMs, default 250; integer timer range 1 through 2,147,483,647 ms. |
WatchSnapshot<Value = Json> | Required type: "snapshot", sequence: number, revision: number, and value: Value. |
WatchPatch | Required type: "patch", sequence, baseSequence, revision, and patch: JsonPatchOperation[]. |
WatchDelta<Value = Json> | Union of WatchSnapshot<Value> and WatchPatch. |
JsonPatchOperation | Either {op: "add" | "replace", path: string, value: Json} or {op: "remove", path: string}. Paths are JSON pointers with ~0/~1 escaping; an empty path addresses the whole value. Move, copy, and test are not emitted. |
import { FlowerClient } from "@flower-js/sdk/client";
const db = new FlowerClient("http://node-1:7101");
const stop = new AbortController();
for await (const { revision, value } of db.watch("pizza.board", null, { signal: stop.signal })) {
console.log(revision, value);
// break or stop.abort() releases the subscription.
}Watches can be spread across replicas. Fresh watches use the same quorum/read semantics as fresh queries; replica-local watches may lag and can go backward after reconnecting elsewhere. A stream can coalesce intermediate revisions. It is a value subscription, not an event log or exactly-once change feed. A patch is used only when it is worthwhile and fits server allowances; otherwise the server sends a replacement snapshot.
Identical invocations share evaluation, diffing and immutable encoded buffers within a node/logical database, scoped by the full admitted principal, deployment and declared consistency. Every subscriber independently authorizes and obtains its read fence on refresh, including idle ticks; credentials are not shared. The first snapshot may start above zero. Subsequent snapshots must increase sequence and may skip values as a complete reset; patches require consecutive sequence and an exact baseSequence. Deployment or principal-scope changes terminate with WATCH_SCOPE_CHANGED, requiring a new watch.
The SDK validates content type, UTF-8 SSE framing, sequence/baseSequence, revision progression within a stream, JSON pointers, operation counts, and reconstructed-value size. Malformed data becomes WATCH_PROTOCOL_ERROR. Server error events terminate the iterator. Breaking, returning the iterator, or aborting interrupts a pending read and releases transport resources. Consumers must explicitly reconnect and accept a new snapshot after interruption.
Server send buffers and evaluation admission are bounded. Updates send immediately while the one-item output queue has room. When a changed update finds the queue full, the subscriber waits for space and batches intermediate changes into one refresh of the latest value, reacquiring admission, its declared read fence and current authorization before sending. A missed producer update uses a full reset snapshot. Consumers waiting for room to send that update close after the configured send timeout. There is no fixed total watch-count cap. Memory, sockets, evaluation cost, quorum checks, and update frequency determine useful capacity. Exposed reads still need application authorization; one watched value can contain sensitive data from many records.
Expiry is ordinary TypeScript.
| API | Contract |
|---|---|
Expiration | Exactly null | {at: number} | {afterCreationMs: number} | {afterUpdateMs: number}. Null disables expiry. One deadline rule only; values are nonnegative safe integer milliseconds. |
ExpiringEntry<T> | Required value: T, createdAt: number, updatedAt: number, and expiresAt: number | null. |
expiringCollection<T = Json>(name, {expiration?} = {}) | Returns the helper below. The default expiration is null and is copied at construction. Names beginning with $flower. are reserved. |
helper.records: Collection<ExpiringEntry<T>> | Internal backing collection; direct reads can see physically retained expired entries. Public methods should use the helper. |
entry(ctx, key): ExpiringEntry<T> | null | Return the live record and timestamps, or null at/after its deadline. |
get(ctx, key): T | null | Return the live payload only. |
scan(ctx): {key: string, value: T}[] | Scan and filter live records. Cost grows with retained backing rows, including expired ones. |
set(ctx, key, value, expiration?): ExpiringEntry<T> | Write using this call’s override or the collection default. Preserve createdAt only while the previous entry is still live; writing an expired key starts a new lifetime. Overrides do not become the default for later writes. |
delete(ctx, key): void | Remove the backing record, live or expired. |
sweep(ctx): number | Seek expired deadlines in bounded pages and physically delete them, returning the deletion count. Must be called from a mutation/maintenance handler; no automatic sweeper is installed. |
Include helper.records in define({collections}) for indexed sweeps. Expiration is enforced when read, even without a sweep. The deadline means now >= expiresAt is expired. Zero-duration policies expire immediately. Physical deletion is a separate policy and consumes transaction work; schedule sweeps at a cadence appropriate to dataset size. Use the scheduler for business actions at a deadline instead of assuming expiry itself triggers a callback.
Lease work, then fence the result.
| API | Contract |
|---|---|
workQueue<Payload = Json, Result = Json>(name, {maxLeaseMs, defaultLeaseMs?, scope?}) | Return a queue helper. maxLeaseMs is required and positive; defaultLeaseMs defaults to it and cannot exceed it. Durations and resulting deadlines must fit safe integers. Optional string scope defaults to the empty string; independent scopes share one declared backing collection and indexes. |
Lease | Required owner: string, token: number, expiresAt: number. |
LeaseIdentity | Required id: string, owner: string, and token: number; pass it to complete/fail. Optional history:HistoryIdentity accompanies initialized retry histories and must be forwarded unchanged from the claim. The helper rejects a history mismatch even before lease expiry; downstream effects also need sink-enforced fencing. |
Claim<Payload = Json> | Extends Lease with id: string, payload: Payload, and attempt: number. |
Job<Payload = Json, Result = Json> | Required payload, scope: string, id: string, leaseExpiresAt: number | null (indexed mirror of the current lease), state: "pending" | "leased" | "completed" | "failed", createdAt, updatedAt, attempts, lease: Lease | null, result: Result | null, and error: Json. |
queue.records: Collection<Job<Payload, Result>> | Backing records keyed by canonical [scope,id] tuples, available to internal code. Mutating them directly can bypass queue invariants. Raw scans include every scope; use queue.scan(ctx) for scoped inspection. |
enqueue(ctx, id, payload, {replaceFinished?} = {}): Job | Create a pending job. Duplicate IDs fail unless replaceFinished is true and the existing job is completed or failed. Replacement resets job timestamps/attempts but never resets fencing tokens. |
get(ctx, id): Job | null | Return effective state. An expired lease appears pending with a LEASE_EXPIRED error even before physical reclamation. |
scan(ctx): {key: string, value: Job}[] | Read all records within this helper’s scope in external ID order, including physically leased records whose deadline passed. This explicit inspection returns the complete scoped result. |
claim(ctx, owner, leaseMs = defaultLeaseMs): Claim | null | Choose the oldest available job by createdAt, then ID. Expired leases are eligible. Increment attempts and a persisted fencing counter; return null when nothing is available. |
complete(ctx, identity, result): Job | Accept only the matching current owner/token before expiry; set completed, clear lease, and store the JSON result. |
fail(ctx, identity, error: Json): Job | Apply the same lease checks, then set failed, clear lease, and retain the error. |
retry(ctx, id): Job | Only a failed job can be explicitly retried. Set pending, clear result/lease, retain attempts and error history fields. |
sweep(ctx): number | Physically reclaim expired leases as pending, returning the count. Reclaiming does not delete completed or failed jobs. |
All mutating helper calls require a MutationContext. Include queue.records in define({collections}) once per backing collection. Claims seek the oldest pending item and inspect expired leases in bounded pages to preserve original creation-order FIFO; cost grows with expired leases, not completed history or valid leases. Sweeps traverse only expired leases. Scopes provide logical separation, not authorization. There is no lease-renewal method.
A mismatched, missing, expired, or replaced lease fails with LEASE_LOST. Tokens increase across these helpers within one logical database, using a reserved counter that helpers never delete. A named partition carries its counter when moved; separate partitions or default databases have independent counters, even when sharing a physical Raft group. A deduplicated claim retry returns its original deadline; that lease may already have expired. Workers must check it before starting work and must enforce fencing at external sinks too. Flower cannot make an arbitrary external side effect exactly once.
Run business logic later.
| API | Contract |
|---|---|
scheduler(name, handlers, options = {}) | Construct durable timers and private maintenance. handlers maps private aliases to MutationMethod references. Register collections: [timers.records] and maintenance: timers.maintenance in define; scheduler handler aliases need not be HTTP aliases. |
SchedulerOptions | Optional maxAttempts default 3; retryDelayMs default 1,000; maxRetryDelayMs default 60,000. Positive safe integers; max delay must be at least the initial delay. |
ScheduledTimer<Args = Json> | Required state: "pending" | "failed", handler, args, dueAt, attempts, error: {code, message} | null, createdAt, updatedAt. |
timers.records: Collection<ScheduledTimer> | Internal timer backing records. Avoid direct writes that bypass validated timer shape. |
timers.maintenance: {run, onError} | Private mutation handlers to register in define. One due timer runs per maintenance transaction. |
get(ctx, id): ScheduledTimer | null | Read and validate one stored timer. |
scan(ctx, state?): (ScheduledTimer & {id: string})[] | Optional pending/failed filter; sort by dueAt then ID. Scans the backing collection. |
after(ctx, id, delayMs, handler, args): ScheduledTimer | Schedule relative to ctx.now(). A repeated ID replaces/debounces previous work, resets attempts/error, and retains original createdAt. |
at(ctx, id, dueAt, handler, args): ScheduledTimer | Schedule an absolute nonnegative safe integer millisecond deadline. Past deadlines become eligible at the next maintenance opportunity. |
cancel(ctx, id): boolean | Delete a pending or failed timer; return whether it existed. |
retry(ctx, id, delayMs = 0): ScheduledTimer | Reschedule only a failed timer using currently deployed handlers with a fresh attempt budget; unknown handlers fail. |
MaintenanceFailure | Required error: {code: string, message: string} and failedAt: number passed to an error handler after failed maintenance is rolled back. |
MaintenanceContinuation | Return hint {$flower: {continue: boolean}} to request another separately committed invocation while due work remains. |
MaintenanceHandlers | Required run: MutationMethod and onError: MutationMethod<MaintenanceFailure, any>. |
MaintenanceManifest | Readonly name, kind: "mutation", optional readonly onError: {name, kind: "mutation"}. |
import { collection, define, mutation } from "@flower-js/sdk";
import { scheduler } from "@flower-js/sdk/scheduler";
const orders = collection<{ state: string }>("orders");
const bake = mutation("internal.bake", (ctx, id: string) => {
const order = ctx.get(orders, id);
if (order) ctx.set(orders, id, { ...order, state: "ready" });
return null;
});
const timers = scheduler("oven", { bake });
const place = mutation("orders.place", (ctx, id: string) => {
ctx.set(orders, id, { state: "baking" });
timers.after(ctx, `bake:${id}`, 5_000, "bake", id);
return { id };
});
export default define({ collections: [timers.records], http: { place }, maintenance: timers.maintenance });Maintenance seeks the earliest pending deadline through the declared ordered index; explicit scan() remains a complete inspection. Deadlines mean “not before,” not exact scheduling. Queued writes, maintenance cadence, elections, outages, and CPU work can delay execution. On success, the timer’s removal and handler writes commit together; the handler may replace its own timer ID. On failure, all handler writes roll back, then the error handler stores a retry or failed state. Retry delay doubles after each failed attempt up to the configured maximum; exhausted attempts remain inspectable as failed timers.
The default host polls maintenance every 250 ms and permits a 50 ms catch-up burst, checked between invocations. Each continuation evaluates against the preceding staged state with its own revision and rollback boundary; successful continuations can share one durable batch commit. An idle call stops the burst. The error handler sees the failed invocation’s original ctx.now(), while failedAt records completion time for retry delays. These are operator settings, not scheduler promises. Callback code may execute again before a durable commit; place external effects in a worker queue.
Sign the petals. Seal the envelope.
nacl and jwt are synchronous native crypto facades. Import them from @flower-js/sdk/crypto or the SDK root in deployed TypeScript. They require Flower’s QuickJS host; this package does not install a Node/browser crypto polyfill. The server keeps cryptographic work in Rust while typed-array bytes cross the Wasm bridge directly.
NaCl: the complete high-level surface
Raw binary arguments/results are Uint8Array; subarray offsets and lengths are respected. Applicable key arguments also accept the managed handles described below, and managed box.before returns an opaque SharedKey. Functions do not overwrite their input arrays. Detached, shared, out-of-bounds, proxy-wrapped, and differently typed inputs are rejected. There is no lowlevel API. All functions and their attached constant properties are frozen.
| API | Contract |
|---|---|
nacl.randomBytes(length): Uint8Array | System entropy by default; available only inside a mutation callback. Length is a nonnegative uint32, further bounded by configured budgets and the engine’s ArrayBuffer representation. |
nacl.setPRNG(source: NaClPRNG | null): void | Set a guest-local fill function; null restores the native source. This override affects randomBytes and random key-pair helpers, never JWT automatic nonces. The caller owns entropy quality and determinism; mutable PRNG state resets with each fresh callback image. |
nacl.secretbox(message, nonce, key): Uint8Array | XSalsa20-Poly1305 authenticated encryption; output is 16-byte tag followed by ciphertext. Key 32 bytes; nonce 24 bytes. Use a unique nonce for each message under a key. |
nacl.secretbox.open(box, nonce, key): Uint8Array | null | Authenticate and decrypt; null for tampering, wrong authentication key, or a box shorter than its tag. Wrong argument types/key/nonce lengths throw. |
nacl.scalarMult(secret, publicKey): Uint8Array | X25519 with 32-byte inputs/output. Retains NaCl’s all-zero result for low-order peer points. A key-exchange protocol must authenticate peers and explicitly handle that result. |
nacl.scalarMult.base(secret): Uint8Array | Derive the 32-byte X25519 public key from a 32-byte scalar. |
nacl.box(message, nonce, publicKey, secretKey): Uint8Array | NaCl public-key authenticated encryption (X25519, HSalsa20, XSalsa20-Poly1305). Peer public key and own secret key are 32 bytes; nonce 24; output overhead 16. |
nacl.box.open(box, nonce, publicKey, secretKey): Uint8Array | null | Authenticate/decrypt with the peer public key and own secret key. Authentication failure returns null; invalid key/nonce shapes throw. |
nacl.box.before(publicKey, secretKey): Uint8Array | SharedKey | Raw secret keys produce a 32-byte shared key; managed secret keys produce an opaque invocation-local SharedKey for repeated box operations. Neither result persists across callbacks. |
nacl.box.after(message, nonce, sharedKey): Uint8Array | Same operation as secretbox, using the key returned by box.before. |
nacl.box.open.after(box, nonce, sharedKey): Uint8Array | null | Same operation as secretbox.open. |
nacl.box.keyPair(): NaClKeyPair | Generate a random 32-byte X25519 secret and its public key. Default entropy requires mutation context; custom PRNG rules apply. |
nacl.box.keyPair.fromSecretKey(secret): NaClKeyPair | Derive/copy a pair from a supplied 32-byte X25519 secret; deterministic and usable in a query. |
nacl.sign(message, secretKey): Uint8Array | Ed25519; output is the 64-byte signature followed by the message. Secret key is 64 bytes, encoded seed || public key. |
nacl.sign.open(signedMessage, publicKey): Uint8Array | null | Verify a signed message and return its message bytes; null for invalid signatures or messages shorter than 64 bytes. Public key must be 32 bytes. |
nacl.sign.detached(message, secretKey): Uint8Array | Produce a detached 64-byte Ed25519 signature. |
nacl.sign.detached.verify(message, signature, publicKey): boolean | Verify a 64-byte signature using a 32-byte public key. Invalid signatures return false; wrong byte lengths/types throw. Strict verification rejects weak keys and malleable legacy signatures. |
nacl.sign.keyPair(): NaClKeyPair | Generate a random 32-byte seed, 32-byte public key and 64-byte Ed25519 secret key. Default entropy requires mutation context. |
nacl.sign.keyPair.fromSeed(seed): NaClKeyPair | Derive the Ed25519 pair from an explicit 32-byte seed; deterministic and usable in a query. |
nacl.sign.keyPair.fromSecretKey(secret): NaClKeyPair | Validate/copy an explicit 64-byte Ed25519 secret. Its public half must match its seed; mismatched keys throw instead of reproducing legacy TweetNaCl behavior. |
nacl.hash(message): Uint8Array | SHA-512; returns 64 bytes. No password-hardening or key-derivation policy is implied. |
nacl.verify(a, b): boolean | Constant-time comparison of equal-length byte contents; false for unequal lengths or two empty arrays. Length is not hidden. |
nacl.box.after.open(box, nonce, key): Uint8Array | null | The same secretbox.open function, exposed through the callable box.after alias. |
Every NaCl length constant
| API | Contract |
|---|---|
nacl.secretbox.keyLength | 32 bytes. |
nacl.secretbox.nonceLength | 24 bytes. |
nacl.secretbox.overheadLength | 16 bytes. |
nacl.scalarMult.scalarLength | 32 bytes. |
nacl.scalarMult.groupElementLength | 32 bytes. |
nacl.box.publicKeyLength | 32 bytes. |
nacl.box.secretKeyLength | 32 bytes. |
nacl.box.sharedKeyLength | 32 bytes. |
nacl.box.nonceLength | 24 bytes. |
nacl.box.overheadLength | 16 bytes. |
nacl.box.after.keyLength | 32 bytes. |
nacl.box.after.nonceLength | 24 bytes. |
nacl.box.after.overheadLength | 16 bytes. |
nacl.sign.publicKeyLength | 32 bytes. |
nacl.sign.secretKeyLength | 64 bytes. |
nacl.sign.seedLength | 32 bytes. |
nacl.sign.signatureLength | 64 bytes. |
nacl.hash.hashLength | 64 bytes. |
NaCl wire layouts and high-level names follow TweetNaCl. Two deliberate compatibility differences strengthen Ed25519 handling: verification rejects weak/malleable legacy cases, and signing rejects secret keys whose public half disagrees with the seed. X25519 retains its all-zero low-order-point behavior. These primitives do not provide peer identity, key rotation, nonce management, or protocol design.
JWT: compact signed and encrypted tokens
| API | Contract |
|---|---|
jwt.sign(claims, key, options): string | Sign a compact three-segment JWS. HS256 uses at least 32 raw secret bytes; PEM-looking bytes cannot be used as an HMAC secret. RS256 uses RSA; ES256 uses P-256 with deterministic RFC 6979 signing; EdDSA uses Ed25519. Signing is deterministic given explicit inputs and may run in a query. It does not add timestamps, expiration or application authorization automatically. |
jwt.verify<Claims>(token, key, options): JWTVerified<Claims> | Verify the signature with the supplied trusted key and algorithm allowlist, then validate standard claims against the invocation clock. Returns claims plus protectedHeader; invalid tokens throw. It does not merely decode untrusted claims. |
jwt.encrypt(claims, key, options = {}): string | Create compact five-segment JWE using only alg: "dir", enc: "A256GCM". Key is 32 raw bytes or a managed A256GCM handle; nonce is 12 bytes; tag is 16 bytes. Omitting nonce requires a mutation. A supplied unique nonce allows deterministic use in queries; the caller then owns uniqueness. Anyone with the symmetric key can also issue valid encrypted tokens. |
jwt.decrypt<Claims>(token, key, options = {}): JWTVerified<Claims> | Authenticate/decrypt dir/A256GCM and validate standard claims against invocation time. Key is 32 raw bytes or a managed A256GCM handle. Returns claims plus protectedHeader; authentication, parsing or claim failure throws. |
| API | Contract |
|---|---|
NaClKeyPair | {publicKey: Uint8Array; secretKey: Uint8Array}. Public and secret arrays have separate buffers; accessing the public buffer does not expose the secret bytes. |
NaClPRNG | (output: Uint8Array, length: number) => void. Fill exactly length bytes. If it throws, the newly allocated output is zeroed and the error propagates. The library cannot establish that a custom generator is cryptographically safe. |
JWTAlgorithm | "HS256" | "RS256" | "ES256" | "EdDSA". ES256 is P-256; EdDSA is Ed25519. No algorithm is inferred as trusted solely from a token header. |
JWTKey | Uint8Array | string | ManagedKey. Raw HMAC/AES use bytes; raw asymmetric keys use PEM strings or explicit DER bytes. Managed handles resolve through operator bindings. NaCl’s raw seed/public-key encodings are not PEM/DER JWT keys. |
JWTKeyFormat | "raw" | "pem" | "der". Defaults to pem for strings and raw for Uint8Array. DER must be requested explicitly: PKCS#8 private key for signing, SPKI public key for verification. PEM accepts PRIVATE KEY (PKCS#8), PUBLIC KEY (SPKI), RSA PRIVATE KEY and RSA PUBLIC KEY (PKCS#1). Certificates and SEC1 EC PRIVATE KEY wrappers are unsupported. |
JWTClaims | Readonly<Record<string, Json>>. A finite plain JSON object. NumericDate claims exp, nbf and iat use seconds, not epoch milliseconds. |
JWTSignOptions | {algorithm; keyFormat?; kid?; typ?}. For raw keys algorithm is required. kid is optional metadata; typ defaults to JWT. Unknown options are rejected. |
JWTValidationOptions | {issuer?; audience?: readonly string[]; subject?; clockToleranceSeconds?; requireExpiration?; typ?}. Expiration is required by default; tolerance defaults to zero nonnegative seconds. Audience must be explicitly supplied whenever the token contains aud. Details below. |
JWTVerifyOptions | JWTValidationOptions & {algorithms: readonly JWTAlgorithm[]; keyFormat?}. Required nonempty allowlist must match the explicit key format; symmetric and asymmetric algorithms cannot share an allowlist/key. |
JWTEncryptOptions | {nonce?: Uint8Array; kid?: string; typ?: string}. A supplied nonce must be 12 bytes and unique for the key. Omitting it obtains system entropy directly in a mutation, bypassing nacl.setPRNG. typ defaults to JWT. With a managed handle kid is set by the native version; supplying kid is rejected. |
JWTProtectedHeader | {alg: JWTAlgorithm | "dir"; enc?: "A256GCM"; kid?: string; typ?: string}. The authenticated protected header returned with claims; kid does not trigger key discovery. |
JWTVerified | JWTVerified<Claims extends object = JWTClaims> = {claims: Claims; protectedHeader: JWTProtectedHeader}. The generic parameter changes TypeScript’s view; it does not validate custom claim shapes or permissions. |
Verification/decryption require exp unless requireExpiration: false. A present exp is always checked: nowSeconds - tolerance < exp. A present nbf requires nowSeconds + tolerance >= nbf. Present exp/nbf/iat must be finite NumericDates; iat is type-checked but does not itself impose a maximum age or reject future issuance. Requested issuer, subject and type must match exactly. Present iss/sub must be strings. aud may be one string or an array of strings: at least one configured audience must match; a token audience without an explicit validation audience is rejected.
Time comes from Flower’s sampled invocation clock, never a caller override or ambient OS clock. verify/decrypt register that clock read as a reactive dependency, so query caching cannot indefinitely reuse an earlier expiration decision. Reading a value produced earlier does not itself revoke a session or delete a token; methods must perform the validation required by their own authorization rules.
Protected headers accept only alg, kid and typ, plus enc for JWE. Unsupported/critical header extensions, compression, alternate serialization forms, duplicate JSON members, malformed base64url and algorithm/key mismatches are rejected. There is no JWKS fetching or unrestricted key selection, implicit HMAC conversion of public-key PEM, nested-token policy, or unverified decode helper. Claims and options must be finite JSON objects; custom claims still need application validation.
Before calling the maintained key parser, Flower checks canonical DER framing, at most 128 nested ASN.1 values, and tag/OID components that fit unsigned 64-bit integers. These guards bound native parser stack use and identifier arithmetic against hostile keys; supported standard key identifiers fit. They are key-parser representation and recursion boundaries, not a message-size or workload cap.
Execution, keys and budgets
Native entropy is forbidden during static and per-invocation bundle initialization and in queries/derived callbacks, including derives evaluated within a mutation. Explicit seeds, keys and nonces support pure callbacks. A custom nacl.setPRNG implementation can generate bytes in JavaScript, but its mutable state resets with the callback snapshot; an unsafe deterministic generator can repeat keys or nonces. Prefer the native mutation-only source for fresh randomness. JWT’s automatic nonce source ignores that override.
Binary inputs are borrowed from guest memory without JSON/base64 conversion. Rust output is copied into the guest once and adopted as a Uint8Array. Claims/options/token strings use native string conversion and JSON/JOSE processing where required. HTTP arguments, stored values and ordinary method results remain finite JSON: encode bytes explicitly when crossing those boundaries. A fresh guest is used for every callback. Raw caller-supplied keys are not persistently cached; managed keys reuse native contexts without exposing private bytes to JS. Raw keys embedded in bundles or records become ordinary replicated application state.
Aggregate native input and output bytes are each bounded by FLOWER_RESULT_MAX_BYTES. Native workspace admission additionally uses FLOWER_RUST_MEMORY_BYTES: a conservative 64 × total input bytes for JWT and managed dispatch, or 2 × predicted output for raw NaCl. These estimates are intentionally conservative, not precise RSS accounting. Wasmtime’s configured memory bound applies to guest buffers. The shared evaluation deadline is checked before and after native calls; long native operations cannot be interrupted halfway by Wasm epochs. Exceeded budgets poison the invocation even if JS catches an ordinary crypto exception. Invalid types, lengths, keys and JWT validation failures throw; NaCl authenticated-open/sign-open failures return null and detached verification returns false.
See the short usage example, native implementation, and binary guest ABI.
Keys stay below the petals.
Declare a capability in TypeScript; keep private material and reusable crypto contexts in Rust. A declaration grants nothing until an operator binds it inside the same logical database.
| API | Contract |
|---|---|
key(name: string, options: KeyOptions): ManagedKey | Return a frozen public descriptor. Names must be nonempty; usages must be known, distinct and nonempty. Include it in define({keys: [handle]}). Duplicate declaration names fail. Resolving a handle during bundle initialization is forbidden; declaring one is safe in static images. |
ManagedKeyAlgorithm | Exactly "Ed25519" | "P256" | "RSA" | "HS256" | "A256GCM" | "XSalsa20Poly1305" | "X25519". |
KeyUsage | Exactly "sign" | "verify" | "encrypt" | "decrypt" | "derive" | "publicKey". Runtime validation also checks that the algorithm supports the requested usage. |
KeyOptions | {readonly algorithm: ManagedKeyAlgorithm; readonly usages: readonly KeyUsage[]}. No unknown properties. |
ManagedKey | KeyOptions & {readonly kind: "key"; readonly name: string}. Public, JSON-compatible declaration metadata. It contains no private bytes and is not an authorization token; native code checks the deployed declaration and the operator binding on every use. |
publicKey(key: ManagedKey): Uint8Array | Requires publicKey permission. Ed25519/X25519 return 32 raw public bytes; P256/RSA return SPKI DER. Symmetric keys have no public component. Exporting a public key does not grant signing authority. |
SharedKey | An opaque frozen QuickJS native object returned by managed nacl.box.before. It has no visible token or own properties, cannot be serialized, and is valid only in that callback. Native operations reject forged descriptors, copied prototypes, proxies, and numeric IDs. It exposes no randomness to pure callbacks. |
ManagedJWTSignOptions | {algorithm?: JWTAlgorithm; typ?: string}. Optional for managed signing: the binding pins Ed25519→EdDSA, P256→ES256, RSA→RS256 or HS256→HS256. A supplied algorithm must agree. The native immutable version sets kid; callers cannot supply kid or keyFormat. |
ManagedJWTVerifyOptions | JWTValidationOptions & {algorithms?: readonly JWTAlgorithm[]}. Optional; if supplied, algorithms must be exactly the singleton algorithm for the bound key. The default expiration requirement and other claim validation remain unchanged. No keyFormat or caller-selected kid. |
Which operation can use which handle?
| API | Contract |
|---|---|
Ed25519 | JWT sign/verify and NaCl sign, sign.open, sign.detached, sign.detached.verify; usages sign/verify. publicKey exports the raw public key. |
P256 / RSA / HS256 | JWT sign/verify; usages sign/verify. P256/RSA permit publicKey. Managed imports contain private keys; public-only verification keys remain usable with the raw API. |
A256GCM | JWT encrypt/decrypt; usages encrypt/decrypt. Nonce behavior is unchanged: explicit unique 12-byte nonce, or mutation-only native randomness when omitted. |
XSalsa20Poly1305 | NaCl secretbox/open and box.after/open.after aliases; usages encrypt/decrypt. Supply a unique 24-byte nonce for each encryption with this key. |
X25519 | NaCl box/open take a raw peer public key plus the managed private handle; usages encrypt/decrypt. box.before requires derive and yields SharedKey; later after/open.after additionally require encrypt/decrypt on the original declaration and binding. scalarMult.base requires publicKey. |
SharedKey | Accepted by secretbox/open and their box.after/open.after aliases during the same callback. It never yields private shared bytes. Managed scalarMult and keyPair.fromSecretKey are unsupported because their raw API returns secret material. |
Existing byte-key overloads still work. Managed JWT sign/verify infer their trusted algorithm; NaCl lengths, formats, aliases and failure results otherwise remain as documented above. Managed NaCl uses the active version by default and carries no embedded version identifier. Save keyVersion(key).version alongside ciphertext/signatures, then use keyVersion(key, savedVersion) for historical decryption/verification. Native permission checks restrict historical selectors to read operations under the original binding; they cannot sign, encrypt or derive a new shared handle. Managed JWT verification/decryption instead selects an unrevoked immutable version from its authenticated kid, restricted to the explicitly bound key; external tokens without that managed kid need the raw public-key API.
Provision and administer.
Use new FlowerClient(url, {adminToken}); select a named database with client.partition(id). All key methods require operator authorization. Mutating methods accept ControlOptions (requestId?, signal?), automatically generating an ID if absent. Preserve the exact ID and request contents after an uncertain response. Methods return {revision, value, duplicate}; revision is the committed database revision and value is the public catalog, with the cache exception below.
| API | Contract |
|---|---|
client.keyList(options?: RequestOptions) | Fresh public catalog; no mutation or request ID. A live member can forward catalog administration to its leader. |
client.keyCacheStats(options?: RequestOptions) | Node-local global cache statistics with no quorum fence or leader forwarding. Even a partition-prefixed URL inspects the ingress physical node, not the owner or a per-tenant cache. The outer revision is local physical database transport metadata. |
client.keyGenerate(name, algorithm, options?: KeyGenerateOptions) | Generate native private material with OS randomness; create immutable version 1. The key name must be new. Returns metadata only. |
client.keyImport(name, algorithm, sealed, options?: ControlOptions) | Import only a SealedKeyImport produced by the native seal command using the cluster wrapping key. SDK and server reject plaintext-shaped uploads. Authentication/format/key validation precede catalog commit. |
client.keyBind(alias, keyName, usages, options?: ControlOptions) | Create or replace the operator binding for a declared alias. Permissions must be valid for that key algorithm. A usable operation is in both the deployed declaration and this binding; merely deploying code cannot grant more permissions. |
client.keyUnbind(alias, options?: ControlOptions) | Remove an existing binding; does not delete stored key versions. An unknown alias fails. |
client.keyRotate(keyName, options?: KeyGenerateOptions) | Generate a new immutable version and select it for new operations. Previous versions remain retained and unrevoked. RSA rotation preserves its current modulus size unless bits is supplied. |
client.keyRevoke(keyName, options?: KeyRevokeOptions) | Irreversibly mark one version revoked, or all existing versions when version is absent. Revoking the active version blocks new operations until rotation creates a new active version. Does not delete ciphertext/history or retroactively cancel an already admitted call. |
KeyGenerateOptions | ControlOptions & {bits?: number}. RSA generation defaults to 2048; supported sizes are 2048, 3072, 4096 and 8192. bits is invalid for other algorithms; RSA generation can be slow: native preparation runs under serialized operator admission and blocks that logical database’s writer until complete. There is no preparation deadline that interrupts the provider. An HTTP timeout can leave the outcome uncertain; retry with the same request ID. |
KeyRevokeOptions | ControlOptions & {version?: number}. A supplied version must identify an existing positive integer version. |
SealedKeyImport | {version: 1; wrappingId: string; nonce: string; ciphertext: string}. Canonical base64url strings: SHA-256 wrapping identity (32 bytes), nonce (12 bytes), authenticated ciphertext. This transport envelope carries an encrypted format tag; no private bytes appear in the JSON. The destination reseals under its own stable catalog domain. |
ManagedKeyCatalog | {domain: string | null; revision: number; keys; bindings}. Empty catalog has null domain/revision 0. keys maps names to {id, algorithm, activeVersion, versions: {version, revoked, retired, destroyed, wrappingId: string | null, kid}[]}; bindings maps aliases to {key, usages}. This revision is the catalog policy revision, distinct from the outer database revision. No envelopes or private bytes are returned. |
KeyCacheStats | {entries, bytes, budgetBytes, hits, misses, loads, evictions, flightEntries, flightBytes, coalesced}, all numeric. bytes counts conservative retained-context admission weights; flightBytes counts metadata for coalesced cold loads, both sharing budgetBytes. Neither is process RSS. Distinct cold keys prepare concurrently; coalesced counts calls sharing the same cold preparation. Metrics are global to one node and reset on restart. |
# Install the same protected wrapping file on every authorized server.
umask 077
openssl rand 32 > /secure/flower-wrapping.key
# Start each server with FLOWER_KEYRING_FILE=/secure/flower-wrapping.key.
# SDK CLI: supply FLOWER_URL and FLOWER_ADMIN_TOKEN for your cluster.
flower key generate session-signing --algorithm Ed25519 --request-id create-session-key
flower key bind sessions session-signing --usages sign,verify --request-id bind-sessions
flower deploy sessions.ts --request-id deploy-sessions
flower key rotate session-signing --request-id rotate-sessions-2
flower key list
flower key cache# Native runtime executable: plaintext stays on this local machine.
/path/to/native/flower key seal --wrapping-key-file /secure/flower-wrapping.key \
--format pem < private.pem > sealed.json
# SDK CLI sends only the authenticated encrypted envelope.
flower key import imported-signing sealed.json --algorithm Ed25519 \
--request-id import-signingBoth executables are called flower: the SDK CLI handles network administration; only the native runtime executable implements key seal. The latter reads plaintext from stdin, accepts --format raw|pem|der and requires --wrapping-key-file PATH. It outputs sealed JSON only. SDK key import NAME FILE --algorithm ALGORITHM reads that JSON; FILE - reads encrypted JSON from stdin. No raw-secret command-line option exists. Keep the wrapping file separate from the Raft data directory and backups; losing it loses access to the encrypted keys.
Import formats: HS256 uses raw bytes (at least 32); A256GCM, XSalsa20Poly1305 and X25519 use exactly 32 raw bytes. Ed25519 accepts a 32-byte seed, a validated 64-byte NaCl secret, or PKCS#8 PEM/DER. P256 accepts a valid 32-byte scalar or PKCS#8 PEM/DER. RSA accepts PKCS#8 or PKCS#1 private PEM/DER (2048–8192 bits). Public-only imports, certificates, SEC1 EC wrappers and password-encrypted PEM are unsupported. Normal DER parser guards also apply.
Fresh key/nonce generation uses the host OS CSPRNG, not a seeded consensus RNG. Mutation evaluation runs on the leader and commits resulting changes; no PRNG state is replicated or advanced inside a reusable QuickJS snapshot. Explicit-input HMAC/Ed25519 signing is deterministic, P256 uses RFC 6979, and provider-internal randomness such as RSA blinding remains native. Opaque SharedKey identities can use native randomness even in read callbacks; they are transient control tokens, not persistent random application data. Supplying explicit nonces makes the caller responsible for uniqueness across retries and concurrent calls.
Authorization, rotation and reactive state.
The catalog belongs to a logical database and has a stable random security domain; named partitions isolate their catalogs and preserve identity when moved. A bundle declaration contains no key bytes and grants no access to another database. Managed calls are unavailable during both forms of bundle initialization. Each callback pins one snapshot of declarations and policy; a rotation cannot switch its version halfway through execution.
When any managed keys are declared, Flower upgrades all query/watch evaluations to fresh quorum-backed snapshots, including aliases marked replica-local. This deliberately gives up stale-read availability for current revocation policy. Already acquired snapshots may finish under their admitted policy. Secret-dependent query results bypass result caching; managed key resolution records a reactive catalog dependency. Binding changes, rotation and revocation recompute dependent materialized values in the same atomic update. Writer revision validation prevents committing speculative work against a conflicting policy revision. A revoked/locked dependency is an error, never an obsolete materialized answer.
Key generation/import and envelope encryption happen before log submission. Raft commits encrypted catalog changes, derived results and request receipts; followers apply those changes without rerunning TypeScript or generating keys. Failed or uncertain administrative calls may have computed discarded material; request receipts identify the sole committed result. As with other derived computations, policy changes may cause dependent values to contain an error instead of a formerly valid result.
Retention is separate from Raft log history: compaction/snapshots reclaim old log entries while current records remain mutable. Request receipts retain fingerprints and original results. Uninitialized histories retain them indefinitely; opt-in epochs and session acknowledgements reclaim results while preserving replay fences. Rotation retains encrypted prior key versions, and revocation marks them unusable without deleting their envelopes. Key-version destruction and wrapping-key rewrap are explicit operator actions; neither happens automatically. Snapshots retain this metadata and receipt history.
Revocation affects newly admitted Flower operations; it cannot retract already returned bytes, revoke tokens checked by external systems, erase every in-flight context, or provide end-user authentication. Application methods still decide who may ask them to sign, decrypt or expose public keys. A managed key is non-exportable to the guest, not to an authorized host administrator.
Native reuse and operating limits.
| API | Contract |
|---|---|
FLOWER_KEYRING_FILE | Optional path to exactly 32 raw bytes. On Unix the file must have no group/other permission bits (chmod 600). Loaded once at startup; missing configuration locks managed crypto, invalid configured files fail startup. Changing its contents requires restarting the process. |
FLOWER_KEY_CACHE_BYTES | Default 16 MiB. Byte-weighted node cache of actual native signer/verifier/HMAC/AES contexts and zeroizing XSalsa/X25519 key buffers. Zero disables retention; oversized entries still execute without retention. Cold loads coalesce per immutable key identity. Unwrap/parse work and cryptographic operations run outside the global cache mutex; independent keys can prepare concurrently. Flight metadata shares the byte budget. |
FLOWER_KEY_CACHE_TTL_MS | Default 0 means no expiry. Positive values bound the age of an actual prepared context since creation; this also applies to callback-local reuse. After expiry, the next use unwraps and reparses. Reload uses the wrapping key already resident in the process. This is not an external revocation lease or a periodic reread of the file. |
The first resolution of an operation/key/version authorizes it against the callback’s pinned policy and records its dependency. Repeated calls may reuse that authorized native context, while enforcing its pinned permissions; this is not a fresh Raft barrier or catalog parse per crypto function. The node-wide cache is consulted only when callback-local reuse misses, so its hit/miss counters exclude that fast path. Contexts are shared across isolated QuickJS callbacks and reader workers, keyed by domain, immutable key/version, algorithm and authenticated envelope. Ed25519 JWT and strict NaCl verification keep their distinct native verification semantics. No plaintext, nonce, JWT validity decision or claims result is retained in this cache. Retention budgets do not cap all active/in-flight provider allocations or process RSS, and expired entries are removed lazily on access/eviction rather than by a secure-erasure timer.
A node must unlock required material before serving managed work; missing/wrong wrapping keys fail closed. Readiness follows the derived values actually read: a source-only query can still run on a locked node. Reusing a managed materialized value conservatively unlocks/prepares all unrevoked catalog versions, so a cold read can pay more than the cost of its own key. Migration carries encrypted catalog metadata with data/timers/receipts; the destination validates all unrevoked key versions before cutover. Give authorized destination nodes the same wrapping key. Otherwise a move remains staged/paused until configuration is corrected and nodes restarted; it rolls forward after recovery. KMS/HSM providers, cross-wrapping-key migration without rewrapping, private-key export, raw-key persistent context caching, and general exportable secrets are not implemented.
Envelopes authenticate domain, key ID, immutable version, algorithm and purpose. Replicated storage/backups contain only encrypted key material; the protected host still has unwrapped keys in memory. Zeroizing owned buffers does not promise protection from process dumps, swapping or host compromise. Sealed imports protect private material; native TLS protects the full connection. The default h2c mode requires a trusted network or external transport protection. For design and recovery details see managed-key operating contract.
Retain the version with the ciphertext
keyVersion(key, version?) returns a ManagedKeyVersion: frozen public metadata {kind:"keyVersion",key:ManagedKey,version:string}. Omit the version to resolve the current full flower.<keyId>.<version> identifier. Save that identifier beside raw NaCl ciphertext and use the version-qualified key for later decryption. Selectors cannot escape the bound logical key. Historical versions permit verification/decryption/public-key access, not signing/encryption/derivation. JWT headers already carry their kid.
client.keyRetire(name, options?: KeyRevokeOptions) | Retire one version or all existing versions. Disables new signing/encryption/derivation while preserving verification/decryption/public-key access. Rotate to create a new active writable version. |
|---|---|
client.keyDestroy(name, options?: KeyRevokeOptions) | Remove current encrypted material and retain a revoked/retired version tombstone. Does not erase backups, prior receipts, or already-admitted native memory. Destroyed active RSA keys require explicit bits when rotated. |
client.keyRewrap(name, options?: KeyRevokeOptions) | Re-encrypt each selected version's wrapping envelope with the current mounted KEK. Material ciphertext and identity remain unchanged. Mount previous KEKs using FLOWER_KEYRING_PREVIOUS_FILES, a JSON array of protected file paths; restart to load them, rewrap every catalog, then unmount old KEKs and restart again. |
ManagedKeyCatalog version metadata also reports retired, destroyed and wrappingId (null after destruction). The CLI exposes key retire|destroy|rewrap NAME [--version N]; preserve --request-id across uncertain retries. Caches resolve current policy before reuse; cache TTL is not a revocation delay.
Transactions between logical databases.
| API | Contract |
|---|---|
transaction<Args, Value = Json>(name, plan): TransactionMethod<Args, Value> | Plan callback (args: Args) => TransactionPlan<Value>; receives no context and cannot access database state. Expose it as an HTTP alias and invoke through client.call. |
TransactionCall | Exactly one readonly group:string or partition:string, plus method:string and optional args:Json. Named targets resolve to a durably pinned placement epoch; barriers affect that logical database. Participants expose a query or mutation; nested transactions are rejected. |
TransactionPlan<Value = Json> | Readonly calls: readonly TransactionCall[] and optional value: Value. Calls are a fixed plan; one participant result cannot determine subsequent calls. |
TransactionMethod<Args = Json, Value = Json> | Readonly kind: "transactionMethod", name, and compute. The declaration helper adapts the pure planner to the runtime; application code should use transaction rather than manually calling compute. |
Validate business rules inside participant methods. Calls in one logical target see earlier staged calls in that target; results preserve original plan order. A successful call receipt contains {results, value?}. Its revision and expectedRevision belong only to the coordinator, not a global sequence.
Two-phase commit durably prepares each participant, then commits an immutable coordinator decision. A prepared lock blocks fresh reads, new watches, writes, deployment, and maintenance throughout its participating logical database, including unrelated keys. Other named partitions on the same physical group remain available; a group target locks that group’s root namespace. Replica-local readers can lag behind the lock and are excluded from atomic visibility guarantees. Existing reads with an acquired snapshot may finish. Separate requests still do not form a global snapshot; related reads must be part of one transaction if they require participant barriers.
Unavailable coordinators can leave participants blocked indefinitely. There is no timeout that silently drops prepared work. TRANSACTION_PREPARED requires recovery/connectivity; TRANSACTION_ABORTED is durable for that request ID. Retry uncertain operations with identical content and the same ID; start a new ID after correcting a durable abort. Operator-driven closure and bounded collection reclaim completed decision and participant detail behind durable rejection floors; receipt retirement is a separate explicit protocol. Incomplete work, unavailable participants, and still-admissible aborted request IDs can block closure. See retry retention and transaction closure. Automatic time-based retirement and arbitrary cross-group point-in-time restore are not implemented.
Configure FLOWER_GROUP, FLOWER_GROUPS, and a shared peer secret on every participant, with separate operator credentials. Internal calls use peer-authenticated pooled HTTP/2 (verified HTTPS with native TLS) and checked compatibility contracts. Plans share existing time, memory, command, and RPC budgets. See the full transaction and recovery protocol. Independent-group benchmark throughput does not measure this coordination workload.
Pool connections with HTTP/2.
| API | Contract |
|---|---|
createHttp2Transport(options = {}): Http2Transport | Node-only HTTP/2 adapter: h2c for http://, verified TLS/ALPN for https://. Reuses one multiplexed session per origin; URLs must not contain credentials. |
Http2TransportOptions | Optional requestTimeoutMs (30,000), idleTimeoutMs (30,000), maxSessions (16 including draining sessions), maxRequestBytes (8 MiB), maxResponseBytes (64 MiB). Numeric budgets are positive safe integers; timer values fit 2,147,483,647 and body allowances fit Node buffer representation. Optional ca?: string | Uint8Array | readonly (string | Uint8Array)[] supplies nonempty PEM trust roots; byte inputs are copied, explicit roots replace Node defaults, and omission uses Node’s configured defaults. Hostname/certificate verification remains mandatory. |
Http2Transport | Required fetch: FlowerFetch and close(): Promise<void>. |
transport.fetch(url, init): Promise<Response> | Flower JSON POST subset; streams SSE responses under Accept: text/event-stream. Ordinary JSON is buffered within maxResponseBytes. |
transport.close(): Promise<void> | Idempotently cancel active streams and release owned sessions. Await this in a finally block. |
import { FlowerClient } from "@flower-js/sdk/client";
import { createHttp2Transport } from "@flower-js/sdk/http2";
const transport = createHttp2Transport();
try {
const db = new FlowerClient("http://localhost:7101", { fetch: transport.fetch });
console.log(await db.query("pizza.board"));
} finally {
await transport.close();
}The ordinary request deadline covers headers and body; for successful SSE it covers response headers only. AbortSignal or close controls stream lifetime. SSE uses a bounded backpressured stream rather than a lifetime response cap; event and reconstructed-value allowances belong to WatchOptions. Idle timeout closes unused sessions, not active streams.
This adapter performs no HTTP/1 fallback, redirects, automatic retries, or compressed-response decoding. HTTPS uses verified TLS, with no insecure switch or hostname override. Session admission, request/response size, truncation, invalid status, unsupported encoding, and premature stream closure produce Error objects with H2_* codes. HTTP status errors still become FlowerError in the client. A failed connection may leave a mutation’s outcome uncertain.
Build application bundles.
| API | Contract |
|---|---|
BuildOptions | Optional initialization: "per-invocation" | "static"; default per-invocation. |
buildBundle(entry: string, options = {}): Promise<Bundle> | Bundle a default-exported define module using esbuild: neutral platform, ES2020 target, IIFE format, tree shaking, ASCII output. Return JavaScript and its SHA-256. Relative entry filenames resolve from the calling directory. |
loadBundle(path: string, options = {}): Promise<Bundle> | For non-.json paths, build the source. For .json, validate hash/javascript strings and their SHA-256; an initialization override is rejected because the bundle already fixes it. |
writeBundle(entry, output, options = {}): Promise<Bundle> | Build, write the JSON bundle to output, and return it. Parent directories must already exist. |
Per-invocation initialization reruns module initialization for each callback. Static initialization runs module code once before snapshotting; choose it only when initialization does not depend on invocation bindings. Before capturing a reusable base or static image, QuickJS collects unreachable cycles and resets its normal 50% allocation headroom. Automatic collection and configured memory/execution limits remain enabled. Method and derived contexts are frozen during trusted sandbox setup, before module initialization; application overrides of Object.freeze do not intercept their construction. Each callback still receives a fresh isolated image; static initialization does not make mutable globals persistent. Deploying validates and recomputes the application under runtime budgets; successful bundling alone is not successful deployment.
Imported packages must be compatible with the isolated runtime and synchronous finite-JSON contract. Bundling a Node dependency does not grant server filesystem or network access. Keep client transport/build helpers in external programs; only application declarations and pure helper code belong in the bundle.
CLI: build, deploy, call, watch.
The SDK installs the flower command. Use npx flower in an npm project or node sdk/cli.ts in a checkout. Commands print JSON; failures print a readable code/status to stderr and exit nonzero. Watch prints one reconstructed result per line and stops on SIGINT/SIGTERM.
| API | Contract |
|---|---|
build FILE [OUTPUT] | Build a TypeScript module. Default output is <basename>.flower.json in the working directory; prints its absolute output path and hash. |
deploy FILE | Build .ts or load .json, then deploy to the primary server with admin authorization; prints receipt and bundle hash. |
init --members ID=ADDR,... | Bootstrap only a fresh group. Positive integer node IDs; addresses are host:port with no URL scheme; duplicate IDs rejected. |
call NAME [JSON_ARGS] | Invoke an exposed alias; code selects its mode. |
mutate NAME [JSON_ARGS] | Invoke an exposed mutation or transaction alias. |
query NAME [JSON_ARGS] | Invoke a query using its deployed consistency policy. |
watch NAME [JSON_ARGS] | Watch one query via SSE and print reconstructed values, not raw patches. |
--url URL | Precedence: explicit flag, FLOWER_URL, then http://127.0.0.1:7101. |
--admin-token TOKEN | Explicit flag or FLOWER_ADMIN_TOKEN. Prefer environment handling appropriate to your deployment; required for admin calls. |
--request-id ID | Idempotency key for call, mutate, deploy, and mutating key commands. Preserve it for uncertain retries. |
--expected-revision N | Nonnegative safe integer revision precondition for call/mutate. |
--preparation MODE | Deploy only: online (default) prepares while old code serves, then checks the base revision. blocking holds the writer lane throughout preparation. Preserve --request-id after DEPLOYMENT_CONFLICT when switching modes. |
--initialization MODE | Build/deploy source only; per-invocation or static. Cannot override an already-built JSON bundle. |
--max-event-bytes N | Watch only; default 17,825,792. |
--max-value-bytes N | Watch only; default 16,777,216. |
--max-patch-operations N | Watch only; default 256. |
--partition ID | Route application/deploy/key commands to a named logical database. Not valid for local build or group init. |
key list | cache | Public catalog or ingress physical-node cache metrics. See the managed-key section for routing and revision semantics. |
key generate NAME --algorithm ALG [--bits N] | Generate native private material; bits applies only to RSA. |
key import NAME FILE --algorithm ALG | Import sealed JSON only. FILE may be - for encrypted JSON stdin; plaintext belongs to the separate native key seal command. |
key bind ALIAS NAME --usages sign,verify,... | Grant selected allowed uses to an application declaration. |
key unbind ALIAS | Remove an operator binding. |
key rotate NAME [--bits N] | Generate/select a new immutable key version. |
key revoke NAME [--version N] | Revoke one version or every existing version. |
key retire NAME [--version N] | Allow only verification/decryption/public-key access for selected existing versions. |
key destroy NAME [--version N] | Remove selected current encrypted material while preserving revoked metadata; backups and prior plaintext are unaffected. |
key rewrap NAME [--version N] | Use the current mounted wrapping key for selected version envelopes; old wrapping keys must remain mounted until rewrap succeeds. All mutating key commands accept --request-id; none accept --expected-revision. |
--help / -h | Print help. Other flags accept --name VALUE or --name=VALUE; duplicate and unknown options are rejected. |
JSON_ARGS defaults to null. Prefix a filename with @ to read its JSON contents. Quote shell JSON carefully. CLI watch allowances are positive safe integers and affect only the local parser. The CLI does not discover leaders, retry uncertain writes, reconnect watches, or expose membership changes; use the operator HTTP APIs for those operations.
Authorize every delivery.
define({authorize}) registers a private fresh query as the admission hook. It runs before public method execution, retry receipt replay, query-cache access, and every watch refresh. Return a principal to admit the call or null/throw to deny it. Modules without a hook remain public. Business checks that change state stay inside the mutation.
| API | Contract |
|---|---|
HistoryIdentity | Readonly {database:string,incarnation:string}; 128-bit hex identities. Stable on normal restart and tenant movement. Explicitly fenced disaster restore changes incarnation. |
ctx.history() | Query/mutation-only HistoryIdentity|null, null before retention initialization. The value is server-owned and participates in dependency certificates. It distinguishes histories; external sinks must separately establish which history they accept. |
Principal | {subject: string, tenant?: string, claims?: Json}. Nonempty stable subject; optional nonempty tenant; application claims. A named partition requires tenant equal to the addressed partition. Only the trusted hook supplies this value. |
AuthorizationRequest | {credentials: Json, method: string, args: Json, partition: string|null, delegation: {coordinator:string,principal:Principal|null}|null}. Credentials default to null. Delegation is supplied only by authenticated transaction coordination; the local hook must decide which coordinators to trust. |
ctx.principal() | QueryContext/MutationContext return the admitted principal or null for a public method. Derived values and argument-only transaction planners cannot depend on a caller principal. Changing a returned JS object does not change native authority. |
FlowerClientOptions.credentials | JSON credentials or a synchronous/asynchronous function returning them for each call or watch connection. RequestOptions.credentials overrides it for one request, including watch/watchDeltas/watchPoll. A watch connection keeps those credentials until it closes; reconnect with refreshed credentials after expiry. |
Credentials are a top-level request field, separate from args and the durable business fingerprint. Refreshed credentials for the same subject/tenant recover the original result using the same request ID. A different owner cannot recover it. Claims may change without changing intent, but the original result remains the original result. Authorization failures return 403 FORBIDDEN. Putting JWT checks only inside a mutation does not reauthorize its receipt replay; use the hook for admission.
Authorization currently requires fresh policy and therefore makes protected replica-local methods quorum-dependent. Each check uses one snapshot; mutation preparation is ordered and revision-fenced. Idle watches recheck policy/time on refresh and close on denial. Already-delivered bytes cannot be revoked. Peer and operator traffic still require a protected network; application authorization does not replace transport security.
Prepare indexes and computations while the garden grows.
Editing local computation code changes nothing on the server until its bundle is built and deployed. Direct deployment recomputes retained materializations in one atomic candidate. Staged deployment prepares durable index and graph pages while existing code and policy keep serving, then explicitly activates the prepared graph; it does not defer rebuilding until public queries. Operators control progress and cleanup, with no automatic activation.
| API | Contract |
|---|---|
StagedDeploymentState | {requestId, phase, baseRevision, baseBundleHash: string|null, bundleHash, cursor: string|null, scannedRows, builtEntries, generation: string|null, graphCursor: string|null, rebuiltRoots, error?: string|null, cleanupCursor: string|null}. phase is backfill, rebuilding, ready, failed, active, canceled or collected. Status survives restart and partition movement; counters describe completed native work, not a completion percentage. Operation failures use ordinary FlowerError responses and leave committed progress intact. |
StagedDeploymentAction | {operation: "advance"|"collect", requestId, maxBytes?: number} or {operation: "activate"|"cancel", requestId}. Retain the original job ID. maxBytes is a positive work budget subject to the node's transaction limit; a row too large for the work budget requires a larger budget. Operations return durable progress. After an uncertain response, inspect status and use the same deployment ID; a repeated advance may process the next page rather than replaying the previous page result. |
client.stageDeployment(bundle, options?) | Operator call returning QueryResult<StagedDeploymentState>. Starts durable index backfill and materialized-graph rebuilding while old code, policy and schema remain active. options is ControlOptions; preserve requestId on retries. One unresolved job per logical database. Source writes maintain staged indexes until activation or cancellation; completed target roots are maintained until activation, cancellation, or target graph failure. Uses POST /admin/deployments at root or named-partition URLs. |
client.stagedDeploymentStatus(options?) | Fresh operator QueryResult<StagedDeploymentState|null>; null means no staged job. options is RequestOptions. Inspect after restart, an uncertain operation or ownership movement before choosing the next step. |
client.controlStagedDeployment(action, options?) | Operator QueryResult<StagedDeploymentState>. advance backfills index pages, then rebuilds adaptive batches of materialized roots and their required dependencies per page; activate atomically switches a ready job's bundle, indexes, aliases, authorization, key declarations and active graph generation; cancel makes its deployment identity terminal; collect reclaims obsolete indexes and graph records in bounded pages. Phases include backfill, rebuilding, ready, failed, active, canceled and collected. graphCursor and rebuiltRoots expose durable graph progress; generation identifies the target graph. A fatal target failure during dual maintenance records error and marks the job failed while preserving active writes; cancel and collect before starting corrected code. One root dependency closure, graph metadata and activation clock/key refresh still obey evaluation budgets; arbitrary record migrations require application progress. options is RequestOptions. Cancellation of the HTTP request does not undo committed work. |
Phases, resume and failure
backfill: added index definitions are maintained with active indexes whileadvancefills bounded source pages. Code-only deployments start inrebuilding.rebuilding:advanceevaluates adaptive batches of retained roots and their required dependencies in a hidden generation. Source rows are stored once. Each active mutation runs its method body once; its final source/root changes also drive target derived callbacks. Active and completed target results publish atomically. Public methods and authorization still use the active bundle and graph.ready: root traversal is complete. Ordinary mutations synchronously maintain the prepared graph, including root additions and removals; readiness does not revert to rebuilding after a write, and there is no catch-up queue. Target maintenance continues until activation, cancellation or a fatal target failure.active: explicit activation has switched bundle, indexes, aliases, policy, key declarations and graph pointer together. Clock-independent outcomes are reused; clock/key-dependent work is refreshed within normal budgets.canceled/collected: cancellation preserves the active application and makes the target intent terminal. Repeated bounded collection removes obsolete or canceled-build state while protecting active generations and shared indexes. Collect the completed job before beginning another. After activation, use a new deployment to roll forward.
Status and its index/graph cursors survive leader changes, full restarts, snapshots and logical-partition moves. Save the deployment ID with its original bundle, inspect status after uncertainty, and verify both requestId and bundleHash before resuming. Only advance backfill or rebuilding, activate ready, and collect active or canceled; do not try to reactivate a canceled or already-active job. rebuiltRoots counts roots visited by graph pages, excluding roots added by concurrent writes. Counters are not a completion percentage. Aborting an HTTP request does not roll back a committed page. See the restart-aware lifecycle example.
An explicit page or activation error leaves prior committed progress intact. Correct a budget constraint and retry when appropriate, or cancel and collect before staging corrected code. A fatal target-maintenance error inside a source mutation instead records failed with an error; the active mutation can still commit if its output and failure status fit the normal budgets. A failed graph cannot activate and must be canceled and collected. Ordinary derived exceptions retain their stored-error semantics. While a cross-group transaction is prepared, state-changing deployment controls return TRANSACTION_PREPARED; status remains inspectable.
The latest collected summary remains available, so status does not necessarily become null after cleanup. The live deployment request ID is reserved against other writes. If its retry epoch or session is retired during preparation, activation is rejected while cancellation and collection remain available; terminal retry results still obey normal receipt budgets. See retry retention before retiring a deployment identity.
Independent page scheduling and limits
FLOWER_DEPLOYMENT_PAGE_MS accepts positive integer milliseconds, defaults to 200, and is capped by FLOWER_EVALUATION_TIMEOUT_MS. It is independent of FLOWER_WRITER_BATCH_MS and is not an HTTP request field. For example, set FLOWER_DEPLOYMENT_PAGE_MS=20 alongside FLOWER_WRITER_BATCH_MS=200 before starting each node. Settings are read at startup; restart nodes to apply changes. Lower targets favor more, smaller graph pages and shorter competing write stalls; larger targets favor fewer commits and rebuild throughput. Each page holds the logical writer during preparation/commit while committed snapshots remain readable.
maxBytes independently bounds inspected source/index work or the graph patch for one progress call, subject to exact command and transaction limits. Graph pages adapt from observed time and bytes, growing by at most twice per page. The time target is soft: an unsuccessful multi-root candidate can retry only its first root with the full normal evaluation timeout. A page can therefore consume the target plus one full evaluation, admission and commit time. One indivisible root dependency closure and an initial aggregate-group scan must still fit one evaluation; the retained graph metadata must fit memory. Removals, changed existing derived edges and restored metadata can require whole-graph topology validation. Activation clock/key refresh still obeys normal budgets.
Completed target roots add derived maintenance work; the extra generation consumes storage until collection. Speculative mutation preparation is bypassed during target maintenance, while serial batches and grouped commits remain available. Pure build-progress changes preserve valid query caches. Measure both rebuild duration and foreground write latency on the application workload; the target is not an end-to-end latency guarantee. The operator guide explains the tuning workflow and links historical benchmark results with their original settings and limitations.
Source transformations are application mutations
Neither direct nor staged code deployment automatically rewrites stored source records. Deploy compatibility code that accepts both old and new versions, then run bounded application migration mutations. Store each batch's transformed rows and durable cursor/version in the same mutation so progress cannot move ahead of the data. Use record-version guards for resumed work and retain each batch's request ID after uncertain responses. For all-record conversions, page by source key with ctx.scan(rows, {gt: afterKey, limit: 100}), omitting gt initially. Read the saved key from the migration record inside the mutation and commit its next value with converted rows; record completion when a page is empty. Make ordinary writers emit the new shape before traversal so inserts behind the cursor are safe. For indexed subsets, use a declared index with ctx.range and stable ordering fields; cursors use each call's current snapshot, so moved rows may be skipped or repeated and missing/nonscalar indexed fields are excluded. Avoid unbounded scans and growing offsets.
Keep concurrent writers compatible and verify remaining old versions plus application invariants before activating code that removes compatibility. The active and staged target computations must both understand intermediate rows. Target maintenance runs target derived callbacks against the active mutation's final source/root changes; it does not rerun the target mutation method body. If the migration needs two source representations, the application must write both explicitly. Application migration progress is separate from the deployment's graphCursor. Collection and derived names are durable identities: changing them or TypeScript interfaces does not rename records or roots. Explicitly copy source records and materialize/unmaterialize roots through a compatibility transition. Protect migration methods with application authorization; the operator token protects admin APIs, not public mutation access. Logical-partition movement preserves stored contents and code and is distinct from record-shape conversion. See the migration workflow, range contracts and mutation context.
Bound history without reviving requests.
Retention is opt-in and uses replicated logical epochs. Initialize once, issue scoped IDs, advance the accepted epoch floor, then collect retired receipts in bounded steps. There is no automatic wall-clock expiry: a clock jump must not erase a promised retry window. Epoch retirement is irreversible within one history.
| API | Contract |
|---|---|
RetryIdentity | {database,incarnation,currentEpoch,minEpoch}. IDs are 32 lowercase hexadecimal characters; epochs are nonnegative safe integers. Identity survives ordinary snapshot/restart and tenant movement. |
RetentionState | RetryIdentity plus receiptBytes, receiptCount, maxReceiptBytes (number|null), gcCursor (string|null), gcComplete, sessionBytes, sessionCount, gcReceiptsComplete and gcSessionCursor. Logical receipt bytes exclude allocator, index, and database-file overhead. |
RetentionAction | Native wire union: initialize {database,incarnation,max_receipt_bytes}; advance {incarnation,current_epoch,min_epoch}; collect {incarnation,limit}; set_budget {incarnation,max_receipt_bytes}; reincarnate {incarnation,new_incarnation,fence_attestation}. Every variant includes operation. limit bounds inspected receipts, not just deletions. Initialization rejects existing state; epochs cannot decrease. reincarnate requires external history fencing and refuses retained transaction protocol records; an attestation records the operator assertion and does not fence an old cluster itself. |
client.retentionStatus(options?) | Operator-only fresh QueryResult<RetentionState|null>. |
client.controlRetention(expectedRevision, action, options?) | Operator-only revision-conditional transition returning MutationResult<{state:RetentionState,collected:number}>. After a lost response inspect status; do not blindly send a different transition. |
client.refreshRetryIdentity(options?) | Fetch current public identity metadata and replace this client's cached identity for new intent. Never converts an old request ID to the new epoch. Rejects an uninitialized history. |
client.newRequestId(intent?, options?) | Creates f1:database:incarnation:epoch:SHA256(intent), using the cached identity or fetching it once. Default intent is a cryptographic UUID. Persist the complete returned ID before sending; preserve it on uncertain retries. The same intent in a new epoch is a different request. |
RetrySession | {database,incarnation,id,epoch,acknowledgedThrough,closed}. The server binds ownership to the current authorization principal. Opening requires a deployed authorization hook; the hook sees $flower.session.open, .status, .ack or .close with session metadata. |
SessionOptions | RequestOptions plus limit?:number (default 256 inspected records) and abandon?:boolean (default false; ACK only). Larger cleanup budgets are explicit, not fixed protocol limits. |
client.openRetrySession(id?, options?) | Returns QueryResult<RetrySession>. Default id is a UUID without dashes; persist your own 32-character lowercase hex ID before opening to recover a lost response. Uses the cached current identity. Reopening the same still-active identity is idempotent; closed, retired or different-owner identities cannot reopen. |
client.sessionRequestId(session, sequence) | Returns an f2 request ID. Sequence must be a positive safe integer above the known ACK watermark. You own durable sequence allocation and intent/result storage; the SDK never advances or acknowledges implicitly. |
client.retrySessionStatus({id,incarnation}, options?) | Fresh, authorized QueryResult<RetrySession>. Recover the current watermark after an uncertain ACK response. Another principal cannot inspect the session. |
client.acknowledgeRetrySession(session, through, options?) | Returns the updated session. Strict ACK requires a complete contiguous receipt prefix fitting limit. Acknowledge only after durably consuming results. abandon:true explicitly fences missing/unknown outcomes too; those intents can never execute later. Deleted or retained receipts at/below the watermark return ALREADY_ACKNOWLEDGED, never re-execute. |
client.closeRetrySession(session, options?) | Terminal close with bounded physical cleanup. Remaining receipts can be collected later; closing may make in-flight outcomes unavailable. A closed identity cannot be reused. Session tombstones remain until their epoch retires. |
FlowerClientOptions.boundedRetries | Default false. When true, new automatic mutation/call/deployment/key-operation IDs use the cached scoped identity. Explicit IDs are passed unchanged. The SDK never automatically renews an expired identity; refresh explicitly for new work. Partition clients carry this setting and fetch their own identity. |
After initialization, unscoped external mutation IDs are rejected. Existing legacy receipts remain retained; new scoped receipts can be collected only below the replicated floor. RETRY_WINDOW_EXPIRED means the outcome is no longer available through this protocol, not that the mutation failed. HISTORY_MISMATCH rejects another history. RECEIPT_BUDGET_EXCEEDED rejects new work instead of evicting promised results. Initialization and epoch changes wait for distributed transactions to complete; receipt GC does not delete their decisions or participant fences.
Normal Raft recovery preserves identity. Disaster restore needs external fencing and a new history; merely restoring an older floor or key catalog is unsafe. Physical database files and backups may retain reclaimed data. See the retention protocol and restore tradeoffs.
Distributed transaction closure
| API | Contract |
|---|---|
TransactionClosureTarget | {group,partition:string|null,epoch,addresses?:string[]}. Durable placement metadata in a closure intent; applications do not use it for leader selection. |
TransactionClosureState | {history:string|null,nextSequence,closedThrough,pending,blockedReason:string|null,deletedRecords}. pending is null or {through,participants,acknowledged}; target lists use TransactionClosureTarget. deletedRecords counts only physical records removed by this operation. An incomplete prefix, an admissible aborted request, unavailable peer or exhausted work budget can prevent progress. |
TransactionClosureAction | {operation:"close",through?:number,maxBytes?:number} or {operation:"collect",maxBytes?:number}. through is an optional nonnegative safe sequence limit; omission selects the available completed prefix. maxBytes is a positive work budget, defaulting to the node's transaction byte budget. |
client.transactionClosureStatus(options?) | Fresh operator QueryResult<TransactionClosureState> from POST /admin/transactions. Works at the root or a named-partition URL. |
client.controlTransactionClosure(action, options?) | Returns QueryResult<TransactionClosureState>. close durably asks every participant to reject delayed protocol messages through a completed prefix before advancing the coordinator floor. collect incrementally deletes detail behind durable floors; run it on coordinators and participants. Operations are monotonic and safe to repeat after uncertain responses. Inspect pending and blockedReason, not just HTTP success. |
Closure never releases an undecided prepared transaction on timeout. Committed client receipts remain governed by their retry contract. An aborted coordinator record can close only after the original request ID is provably inadmissible; legacy aborts can block a prefix until retention is initialized and rejects unscoped IDs. Participant floors survive deletion, snapshots, restart and movement. This protocol does not make independently restored old participants safe.
Operational limits are part of the API.
Each logical database has an ordered writer; readers run independently, and named partitions have separate preparation lanes while sharing their physical group’s ordered Raft log. Durable acknowledgements wait for quorum commitment and application. Adaptive group commit reduces storage overhead but trades queue latency against throughput. Independent candidates can prepare in parallel; their dependencies are validated in order, and conflicting candidates rerun before commitment. A 503 queue rejection is backpressure, not extra capacity.
| API | Contract |
|---|---|
| Execution budgets | Default evaluation deadline 5 seconds; source bundle 2 MiB; result/exchange 16 MiB; aggregate live QuickJS/Wasm memory 128 MiB; Rust overlay/bookkeeping allowance 128 MiB. Structural JSON/graph depth and stack guards remain. Budgets are configurable at node startup and do not promise a process-RSS cap. |
| Node resource admission | Heavy queries, calls and watch refreshes reserve shared node capacity before capturing execution snapshots. FIFO logical-partition lanes rotate fairly; writers share preparation capacity while preserving lane order. Separate control reserves serve operators/maintenance. FLOWER_PREPARATION_WORKERS and memory reservations bound this shared pool. FLOWER_QUERY_WORKERS bounds authorization callbacks and public cache probes, and supplies the preparation pool’s default; it is not a separate heavy query pool. Queued bytes, active reservations and retained watch values are configurable; exhaustion returns 503 ADMISSION_OVERLOADED. GET /admin/resources requires the operator bearer token and reports active/queued work, bytes, lane counts, oldest age and rejection/cancellation counters. Cache-probe/output leases count toward retained bytes, not active preparation slots. Reservations are estimates, not RSS measurements; fresh-barrier waits retain their slot. See the linked reference for memory budgets and control reserves. |
| Public HTTP cache probes | Eligible query hits use already-encoded results without acquiring a heavy preparation slot. A probe reserves input/key scratch bytes, immediately tries a lookup slot, then checks the current partition gate, HTTP registry, declared consistency, transaction lock and dependency certificate. Fresh reads still need a quorum fence. Applications with authorization hooks or managed keys keep the full admitted path. Misses or busy probes release temporary snapshots and reservations before ordinary fair admission and fresh snapshot capture; the probe itself has no separate fairness guarantee. Output bytes are reserved before allocation and remain charged to FLOWER_QUEUED_INPUT_BYTES until transport releases them, independently of cache eviction. The response retains no snapshot or worker slot. Watches keep their existing admission and authorization path. |
| Wasm reuse | Compiled modules, initialized images and instance slots are reused. Every callback starts from the same pristine logical state. A synchronous serial mutation batch may retain a Store on its worker thread after restoring memory and every mutable Wasm global; host rights/caches are replaced and memory is charged anew. Nested callbacks use distinct active instances; traps, failures and growth discard them. Linux/macOS track the exact pages ever written and copy all of them on reset; untouched pages remain read-only. Other platforms or FLOWER_WASM_DIRTY_PAGES=0 use full copies. FLOWER_WASM_RECYCLE=0 disables resident reuse. FLOWER_WASM_RECYCLE_BYTES bounds idle linear memory per process (96 MiB by default); zero or larger images bypass retention without rejecting work. Idle instances also use at most half the process’s slots and are released at batch exit; small pools can have less active capacity. GC and execution/memory budgets stay active. Each slot reserves 4 GiB of virtual address space on 64-bit hosts, without committing that amount of RAM. See the operator reference for platform and memory tradeoffs. |
| Durability and history | Records, receipts, indexes, and snapshots use redb. Acknowledgements wait for quorum-durable log commitment and local atomic application. Application checkpoint transactions defer their own fsync; subsequent immediate log, vote, snapshot, or purge transactions also persist earlier checkpoints. Crash recovery reconstructs missing committed changes and receipts from Raft without rerunning callbacks. Backups must preserve node identity/data and the semantics of all involved groups. Receipt, completed job, timer-failure, and transaction history can grow without bound unless application/operator policy reclaims what is safe; blindly deleting fencing or transaction metadata breaks guarantees. |
| Security boundary | Only deployed aliases are public. Use define({authorize}) to establish a principal before callbacks, replay, caching and watch delivery; Flower does not supply an identity provider. FLOWER_ADMIN_TOKEN protects operator routes; a distinct FLOWER_PEER_TOKEN separates internal routes (omission falls back to admin). Set FLOWER_TLS_CERT_FILE/KEY_FILE/CA_FILE together for native TLS with verified peers and h2/HTTP1 ALPN. Cleartext h2c remains the default. Trusted peers still have cluster authority. See the linked TLS operating contract. |
| Recovery and upgrades | Membership adds a caught-up learner before voting promotion and uses joint consensus. Compatible builds can be rolled one member at a time after compatibility preflight. Unreleased incompatible wire/state/guest contracts cannot mix. The deferred-checkpoint recovery policy has a new state-machine contract: pre-change and current nodes require a coordinated upgrade, and forward metadata migration makes directory downgrades unsupported. Restart with the same node ID and directory; do not initialize again. If durable logs extend beyond the recovered applied position, application reads wait for a quorum-confirmed startup recovery fence; diagnostics and peer RPCs remain available. |
| Shutdown | SIGINT/SIGTERM stop acceptance and attempt a bounded graceful drain; long-lived SSE streams are closed by the deadline. Interrupted calls can have uncertain outcomes; retry with the same request ID. |
| Performance measurements | Published reports label consistency, mix, topology, latency, resources, failover, and audit. All measured replicas and generators share one host. Closed-loop customer throughput is not an open-loop latency guarantee, a capacity promise for another application, or evidence of cross-group transaction scaling. |
For every environment variable and cross-budget rule, use the complete operator settings reference. The operating walkthrough, membership and rolling-upgrade procedure, HTTP/storage protocol, and latest measured report provide the next level of detail. Failure recovery is tested; deployments still need validation against their own workloads and failure scenarios.