Reference · 12

Partitions and transactions.

Move tenants between Raft groups, and commit changes across groups at once.

Partitions and resizing

A named partition is a separate database (its own bundle, data, timers and retry receipts) that you can move between Raft groups. Several partitions share one group.

  • Choose partitions when you create a database. Data in the default database can't be turned into a partition later.
  • A partition can't read another partition's data directly. A transaction can call exposed methods in several partitions at once.
Operator · existing, initialized groups
import { FlowerAdmin, FlowerClient } from "@flower-js/sdk/client";
import { buildBundle } from "@flower-js/sdk/bundle";

const admin = new FlowerAdmin("http://catalog-1:7101", { adminToken: process.env.FLOWER_ADMIN_TOKEN });
await admin.registerGroup({ id: "west", addresses: ["west-1:7101", "west-2:7101", "west-3:7101"] });
await admin.registerGroup({ id: "east", addresses: ["east-1:7101", "east-2:7101", "east-3:7101"] });
await admin.createPartition("tenant-a", "west", { requestId: "create-tenant-a" });
await admin.waitForPartition("tenant-a");
await admin.partition("tenant-a").deploy(await buildBundle("app.ts"), { requestId: "deploy-tenant-a" });
const tenant = new FlowerClient("http://catalog-1:7101").partition("tenant-a"); // Calls its exposed methods.
await admin.resize(["west", "east"], { requestId: "grow-two-groups" });
console.log(await admin.layout()); // Durable progress; resize continues after this client exits.
APIContract
client.partition(name): FlowerClient<App>A client for /partitions/{name}, keeping the same transport, credentials, retry policy and types. Works wherever the partition lives. Isolation only; it doesn't authenticate tenants.
admin.partition(name): FlowerAdminOperator calls against one named database: deploys, staged deployment, keys, retention and transaction closure.
admin.layout(options?): Promise<ClusterLayout>Current groups, placements, moves and rebalance plan.
admin.registerGroup(group, options?): Promise<ClusterGroup>Register a group that is already running. Safe to repeat. Doesn't start servers.
admin.removeGroup(id, options?): Promise<{removed: string}>Unregister an empty, unused group (never the catalog). Doesn't stop servers or delete files.
admin.createPartition(name, group, options?): Promise<PartitionPlacement>Create an empty partition. Wait until active, then deploy to it.
admin.movePartition(name, destination, options?): Promise<PartitionMove>Start moving to another group. Returns before the move finishes; the move continues if the client goes away.
admin.resize(groupIds, options?): Promise<RebalancePlan>Spread partitions evenly by count across these groups, one move at a time; groups left out are drained. Doesn't balance by size or load. Register new groups first; remove drained ones after. Rejected while another plan or move is running.
admin.partitionStatus(name, options?): Promise<PartitionPlacement>Current phase, owner and epoch.
admin.waitForPartition(name, options?): Promise<PartitionPlacement>Poll until active. Defaults: timeoutMs 30,000, intervalMs 100. Throws PARTITION_WAIT_TIMEOUT on timeout; abort only stops waiting.
ControlOptionsOptional signal and requestId. A UUID is generated if omitted; set your own so you can retry safely.
PartitionWaitOptionsOptional signal, timeoutMs and intervalMs.
ClusterGroup{id: string, addresses: string[]}, with distinct host:port entries. Addresses are seeds and can't be changed while the group is registered; keep at least one reachable.
PartitionPlacement{partition, epoch, owner: ClusterGroup, status: "creating" | "active" | "moving", operation, movement: PartitionMove | null}.
PartitionMove{operation, partition, source: ClusterGroup, destination: ClusterGroup, source_epoch, epoch, phase}. The epoch changes on each move; revision and receipts carry over.
PartitionMovePhase"copying" | "freezing" | "importing" | "activating" | "retiring" | "complete". Moves always finish; there is no cancel.
RebalanceMove{partition, source, destination, operation}, with group IDs.
RebalancePlan{operation, groups: ClusterGroup[], moves: RebalanceMove[], next: number, complete: boolean}. next is the index of the next move.
ClusterLayout{groups: ClusterGroup[], partitions: PartitionPlacement[], moves: PartitionMove[], rebalance: RebalancePlan | null}. Move history is kept indefinitely.

Running partitions

Configure every server with:

  • FLOWER_GROUP: this server's group.
  • FLOWER_CATALOG_GROUP: the group that stores placements.
  • FLOWER_GROUPS: JSON map of group IDs to host:port lists, including this group and the catalog.
  • The same peer secret everywhere, and a separate operator token.

Initialize each group on its own. Put applications in named partitions, not the catalog's default database.

During a move

  • The source keeps serving while it copies, then briefly freezes to send the final changes. Writes either land before the freeze or fail; none are lost.
  • The freeze can take longer for large partitions or slow networks. It is not guaranteed to be milliseconds.
  • If a group loses quorum after the freeze, the move pauses until it recovers.
  • Pending cross-group transactions delay the freeze.
  • Live watches end with an error when the owner changes. Reconnect through the partition client; there's no automatic replay.
  • Other partitions on the same group keep working but share CPU, disk and the Raft log.
SettingEffect
FLOWER_ROUTE_CACHE_MSHow long a server trusts a cached owner. Default 1,000 ms. If the catalog is down when an entry expires, requests to that partition fail.
FLOWER_PARTITION_TAIL_MAX_BYTESIf the final change set is bigger than this, send a full copy instead. Unset by default.
FLOWER_PARTITION_BASE_MAX_BYTESMaximum size of the initial copy. Unset by default.

Partition URLs are not a security boundary. For public traffic, put a trusted gateway in front that limits each caller to its partitions. Retry uncertain mutations with the same request ID and body.

Cross-group transactions

A transaction method calls exposed methods in several partitions or groups and commits them all or none. The methods guide has a complete example.

APIContract
transaction(name, plan)
transaction(name, spec: MethodSpec, plan)
Returns a TransactionMethod<A, V>. plan(args) returns a TransactionPlan; it sees only its arguments and can’t read data. spec.args checks the arguments and spec.access guards the call. Expose it as an alias and call it with client.call.
participant<Methods>(target).call(alias, args?): TransactionCallBuilds one call. Methods, an http map or typeof app, types the alias and its arguments; transactions can’t be participants. target is { partition } or { group }.
TransactionTargetExactly one of partition: string (a named database) or group: string (a group’s root database).
TransactionCallA TransactionTarget plus method (an exposed query or mutation alias) and optional args.
TransactionPlan<V = Json>calls: readonly TransactionCall[] and optional value: V. The call list is fixed up front; results can’t choose later calls.
TransactionResult<V = Json>What the caller gets: results, one per call in plan order, and the plan’s value, when it returned one.
TransactionMethod<A = Json, V = Json>{kind: "transactionMethod", name, compute}. Create it with transaction; don’t call compute yourself.
  • Check business rules inside the participant methods. Later calls to the same target see earlier ones.
  • If a participant fails, nothing commits and the caller gets 422 TRANSACTION_ABORTED with that participant’s failure. It is final for that request ID; fix the problem and use a new ID.
  • Participants are authorized with the coordinator’s principal; see delegation.
  • While a transaction is prepared, each participating database blocks fresh reads, watches, writes, deploys and maintenance, even for unrelated keys. Other partitions on the same group are unaffected.
  • Replica-local reads may not see the transaction atomically.
  • If the coordinator is unreachable, participants stay blocked until it recovers. Nothing times out on its own.
  • TRANSACTION_PREPARED: wait for recovery and retry with the same ID and content.
  • Completed transaction records are cleaned up only by operator action; see retry retention and transaction closure.
  • Every participant needs FLOWER_GROUP, FLOWER_GROUPS and the shared peer secret; named partitions also need FLOWER_CATALOG_GROUP.

Full protocol: transactions and recovery.