Flower / A reactive TypeScript database

A little logic.
A lot of bloom.

Write methods and values that depend on each other. Flower keeps them in sync, with every change rooted in Raft. Plant a function. Watch it grow.

Pink, golden, and lavender flowers grow from connected green stems.sourcederivewatch
One change. Fresh blooms.
Your methods are the APIReactive values, atomic updatesRust + QuickJS in WasmFresh reads on any replica

A tiny multiplayer terrarium.

Twelve spots. New blooms. Room for the next generation.

Complete app ↓
01

Give your garden some room.

Each garden gets twelve spots and its own flower names. An indexed, reactive view counts blooms and free spaces; stable IDs keep live patches small.

Define the records and view first. Next, give them a life cycle.
docs/terrarium.ts · start here
import { canonicalJson, collection, define, derive, mutation, query } from "@flower-js/sdk";
import { scheduler } from "@flower-js/sdk/scheduler";

type Seed = { garden: string; id: string };
const GARDEN_SIZE = 12;
const flowers = collection<Seed & { plantedAt: number; bloomed: boolean }>("flowers")
  .index("garden", ["garden"]);

const garden = derive("garden", (ctx, id: string) => {
  const rows = ctx.query(flowers.by("garden").eq(id));
  return {
    spacesLeft: GARDEN_SIZE - rows.length,
    blooming: rows.filter(flower => flower.bloomed).length,
    flowers: Object.fromEntries(rows.map(flower =>
      [flower.id, flower.bloomed ? "🌼" : "🌱"])),
  };
});
const view = query("view", (ctx, id: string) => ctx.get(garden, id),
  { consistency: "replica-local" });
02

Plant, bloom, make room.

Planting checks capacity inside the mutation, so simultaneous gardeners cannot overfill it. Each seed blooms after five seconds and perishes after thirty-five, freeing its spot.

Two private callbacks handle the seasons. Their deadlines and the plant commit together; the planting time protects replacements from old callbacks.

Only plant and view are public. Deadlines mean “not before”; outages can delay both bloom and farewell.
docs/terrarium.ts · continue the same file
type Season = { key: string; plantedAt: number };
const bloom = mutation("internal.bloom", (ctx, event: Season) => {
  const flower = ctx.get(flowers, event.key);
  if (flower?.plantedAt === event.plantedAt) {
    ctx.set(flowers, event.key, { ...flower, bloomed: true });
  }
  return null;
});
const perish = mutation("internal.perish", (ctx, event: Season) => {
  if (ctx.get(flowers, event.key)?.plantedAt === event.plantedAt) {
    ctx.delete(flowers, event.key);
  }
  return null;
});
const seasons = scheduler("seasons", { bloom, perish });

const plant = mutation("plant", (ctx, seed: Seed) => {
  if (!seed || ![seed.garden, seed.id].every(v => typeof v === "string" && v)) {
    throw new Error("Give your garden and seed a name.");
  }
  const key = canonicalJson([seed.garden, seed.id]);
  if (ctx.get(flowers, key)) throw new Error("That spot is already planted.");
  if (ctx.query(flowers.by("garden").eq(seed.garden)).length >= GARDEN_SIZE) {
    throw new Error("Garden full! Wait for a flower to make room.");
  }
  const plantedAt = ctx.now();
  ctx.set(flowers, key, { garden: seed.garden, id: seed.id, plantedAt, bloomed: false });
  const event = { key, plantedAt }; // An old timer cannot affect a replacement.
  seasons.at(ctx, `bloom:${key}`, plantedAt + 5_000, "bloom", event);
  seasons.at(ctx, `perish:${key}`, plantedAt + 35_000, "perish", event);
  ctx.materialize(garden, seed.garden);
  return { planted: seed.id };
});

export default define({
  collections: [flowers, seasons.records],
  definitions: [garden, bloom, perish],
  maintenance: seasons.maintenance,
  http: { "garden.plant": plant, "garden.view": view },
});
03

Share one living view.

Plant with a friend, then watch seeds appear, bloom, and leave empty spots. One SSE watch carries the whole garden; the SDK applies each patch.

This view permits replica lag. Omit replica-local for fresh reads. These gardens are public; add authorization for private worlds.

Watches show current state, not every event. Keep the request ID when retrying a write.
docs/terrarium-client.ts · outside the database
import { FlowerClient } from "@flower-js/sdk";

const terrarium = new FlowerClient("http://127.0.0.1:7101");
await terrarium.mutate("garden.plant", {
  garden: "moon-garden", id: "luna",
}, { requestId: "plant-moon-garden-luna" }); // Reuse this ID on retries.

for await (const { value } of terrarium.watch("garden.view", "moon-garden")) {
  console.log(value);
  // { spacesLeft: 11, blooming: 0, flowers: { luna: "🌱" } }
  // After 5s: a bloom. After 35s: an empty spot, ready for another seed.
}

Grow it on your machine.

Build and start a local Flower node, then run these commands from the checkout. The server is Rust; TypeScript tooling uses Node.

Use another seed ID to fill a free spot. After a flower perishes, replant its name with a new request ID. Another garden gets its own twelve spots.

Download the client ↓
Terminal · repository root
node sdk/cli.ts deploy docs/terrarium.ts --initialization static
node docs/terrarium-client.ts

LATEST MEASURED RUN ·

A busy day in the garden.

Global customer calls / s
85,508
Customer p99
270.4 ms
Independent Raft groups
8 × 3 replicas

Replica-local reads: lag is allowed. 70% reads / 30% mutations · HTTP/2 · 60.3 measured seconds.

Run passed. 8/8 group audits passed. 8 injected leader failures; quorum recovery 592–736 ms.

Apple M5 Pro; all replicas and load generators share one machine. Completed customer calls use the union measurement window; retries, worker traffic, and explicit replays do not inflate throughput. Reads may be stale; mutations and audits retain fresh checks.

Business logic, a little later.

Schedule a mutation after an update. Let workers claim jobs with expiring leases. Timers and lease policy are ordinary TypeScript.

Timers, workers & expiry →

More readers. More replicas.

Spread queries and watches with queryUrls. Reads stay fresh; application code can opt into replica-local when lag is okay.

Read policies & live queries →

Rooted in real Raft.

Three nodes tolerate one failure. Writes wait for durable commitment. Each callback gets a fresh QuickJS/Wasm instance with reusable snapshots.

Run it, recover it, know its limits →