Flower / A reactive TypeScript database

A little logic.
A lot of bloom.

Flower is a database you program in TypeScript. You write the methods and the values derived from your data; Flower keeps those values up to date and copies every change to three servers with Raft.

Pink, golden, and lavender flowers grow from connected green stems.sourcederivewatch
One change. Fresh blooms.
Your methods are the APIDerived values stay currentSurvives a server failureRead from any replica

A tiny multiplayer garden.

A complete app in about 50 lines: plant seeds, watch them bloom, share the view live.

Complete app ↓
01

Define the data.

Each garden has twelve spots. flowers stores the seeds under [garden, id] keys; garden is a derived view that counts blooms and free spots.

Flower recomputes the view whenever a flower in that garden changes.
docs/terrarium.ts · start here
import { canonicalJson, collection, define, derive, fail, mutation, query, v } from "@flower-js/sdk";
import { scheduler } from "@flower-js/sdk/scheduler";

const GARDEN_SIZE = 12;
const name = v.string({ min: 1, max: 64 });
const flowers = collection<{ garden: string; id: string; plantedAt: number; bloomed: boolean }>("flowers")
  .key(v.tuple([name, name]))
  .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", { args: name, consistency: "replica-local" }, (ctx, id) => ctx.get(garden, id));
02

Add the methods.

plant checks its arguments against a schema, makes sure there is room, saves the seed and schedules two timers: bloom after 5 seconds, remove after 35.

Mutations run one at a time, so two gardeners can never overfill a garden. fail() tells callers why, with a code like GARDEN_FULL.

Only garden.plant and garden.view are public. Timers mean “not before”: an outage can delay them.
docs/terrarium.ts · continue the same file
const season = v.object({ key: v.tuple([name, name]), plantedAt: v.int() });
const bloom = mutation("internal.bloom", { args: season }, (ctx, event) => {
  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", { args: season }, (ctx, event) => {
  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", { args: v.object({ garden: name, id: name }) }, (ctx, seed) => {
  const key: [string, string] = [seed.garden, seed.id];
  if (ctx.get(flowers, key)) fail("SPOT_TAKEN", "That spot is already planted.");
  if (ctx.query(flowers.by("garden").eq(seed.garden)).length >= GARDEN_SIZE) {
    fail("GARDEN_FULL", "Garden full! Wait for a flower to make room.");
  }
  const plantedAt = ctx.now();
  ctx.set(flowers, key, { ...seed, plantedAt, bloomed: false });
  const event = { key, plantedAt }; // An old timer cannot affect a replacement.
  seasons.at(ctx, `bloom:${canonicalJson(key)}`, plantedAt + 5_000, "bloom", event);
  seasons.at(ctx, `perish:${canonicalJson(key)}`, plantedAt + 35_000, "perish", event);
  ctx.materialize(garden, seed.garden);
  return { planted: seed.id };
});

const app = define({
  uses: [seasons],
  collections: [flowers],
  definitions: [garden],
  http: { "garden.plant": plant, "garden.view": view },
});
export default app;
03

Call it and watch.

Plant a seed, then subscribe to the garden. Every viewer sees the same garden, and TypeScript checks every alias and argument against the app's types.

This view may lag slightly behind; drop replica-local for always-fresh reads. Anyone can plant here: add access control for private gardens.

Subscriptions show the latest state, may skip steps and reconnect on their own. Retries reuse the request ID, so a write lands once.
docs/terrarium-client.ts · outside the database
import { FlowerClient } from "@flower-js/sdk";
import type terrarium from "./terrarium.ts";

const client = new FlowerClient<typeof terrarium>("http://127.0.0.1:7101");
await client.mutate("garden.plant", { garden: "moon-garden", id: "luna" }, {
  requestId: "plant-moon-garden-luna", retry: true, // Retries reuse this ID.
});

for await (const { value } of client.subscribe("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.
}

Run it yourself.

Start a local Flower node, then run these two commands from the repository.

Change the seed ID to plant another flower. Change the garden name to start a new garden.

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

LATEST MEASURED RUN ·

A busy day in the garden.

Global customer calls / s
62,061
Independent Raft groups
8 × 3 replicas

Reads2,611,354 calls

Customer reads: 2,611,354 calls; p50 1.01 ms, p99 15.4 ms, max 2.01 s. Log axis from 0.043 ms to 792 ms; 0.1% of calls fall outside it.0.1 ms1 ms10 ms100 ms0.043 ms–0.053 ms: 0.061% of calls (1,601)0.053 ms–0.0653 ms: 0.3% of calls (6,543)0.0653 ms–0.0805 ms: 0.5% of calls (13,341)0.0805 ms–0.0992 ms: 0.8% of calls (21,993)0.0992 ms–0.122 ms: 1.3% of calls (34,873)0.122 ms–0.151 ms: 2.0% of calls (51,365)0.151 ms–0.186 ms: 2.6% of calls (68,728)0.186 ms–0.229 ms: 3.3% of calls (86,159)0.229 ms–0.282 ms: 3.8% of calls (100,506)0.282 ms–0.348 ms: 4.4% of calls (113,987)0.348 ms–0.428 ms: 4.9% of calls (128,873)0.428 ms–0.528 ms: 5.6% of calls (145,762)0.528 ms–0.65 ms: 6.3% of calls (163,989)0.65 ms–0.802 ms: 6.7% of calls (174,942)0.802 ms–0.988 ms: 6.9% of calls (180,542)0.988 ms–1.22 ms: 7.0% of calls (181,556)1.22 ms–1.5 ms: 6.9% of calls (180,561)1.5 ms–1.85 ms: 6.8% of calls (177,887)1.85 ms–2.28 ms: 6.6% of calls (171,516)2.28 ms–2.81 ms: 6.0% of calls (156,215)2.81 ms–3.46 ms: 5.0% of calls (129,894)3.46 ms–4.27 ms: 3.7% of calls (96,899)4.27 ms–5.26 ms: 2.6% of calls (66,935)5.26 ms–6.48 ms: 1.7% of calls (45,684)6.48 ms–7.98 ms: 1.3% of calls (32,973)7.98 ms–9.84 ms: 0.9% of calls (22,264)9.84 ms–12.1 ms: 0.6% of calls (16,596)12.1 ms–14.9 ms: 0.5% of calls (11,921)14.9 ms–18.4 ms: 0.3% of calls (9,069)18.4 ms–22.7 ms: 0.2% of calls (5,802)22.7 ms–28 ms: 0.1% of calls (3,808)28 ms–34.5 ms: 0.1% of calls (2,643)34.5 ms–42.5 ms: 0.058% of calls (1,512)42.5 ms–52.4 ms: 0.019% of calls (506)52.4 ms–64.5 ms: 0.010% of calls (274)64.5 ms–79.5 ms: 0.0036% of calls (94)79.5 ms–98 ms: 0.0035% of calls (92)98 ms–121 ms: 0.00096% of calls (25)121 ms–149 ms: 0.0026% of calls (69)149 ms–183 ms: 0.0049% of calls (127)183 ms–226 ms: 0.0033% of calls (85)226 ms–279 ms: 0.0033% of calls (87)279 ms–343 ms: 0.0010% of calls (26)343 ms–423 ms: 0.0035% of calls (91)423 ms–521 ms: 0.0033% of calls (85)521 ms–643 ms: 0.0051% of calls (133)643 ms–792 ms: 0.0036% of calls (95)p50 1.01 msp99 15.4 ms

Writes1,120,588 calls

Customer writes: 1,120,588 calls; p50 84.4 ms, p99 207 ms, max 977 ms. Log axis from 30 ms to 910 ms; 0.052% of calls fall outside it.50 ms100 ms200 ms500 ms30 ms–32.2 ms: 0.045% of calls (509)32.2 ms–34.5 ms: 0.081% of calls (910)34.5 ms–37 ms: 0.2% of calls (1,928)37 ms–39.6 ms: 0.4% of calls (4,036)39.6 ms–42.5 ms: 0.8% of calls (8,435)42.5 ms–45.5 ms: 1.3% of calls (14,154)45.5 ms–48.8 ms: 2.1% of calls (23,959)48.8 ms–52.4 ms: 3.1% of calls (34,853)52.4 ms–56.1 ms: 4.4% of calls (49,036)56.1 ms–60.2 ms: 5.4% of calls (60,789)60.2 ms–64.5 ms: 6.3% of calls (71,006)64.5 ms–69.2 ms: 6.8% of calls (76,451)69.2 ms–74.2 ms: 6.8% of calls (76,603)74.2 ms–79.5 ms: 6.9% of calls (77,598)79.5 ms–85.2 ms: 7.1% of calls (79,005)85.2 ms–91.4 ms: 7.1% of calls (79,026)91.4 ms–98 ms: 6.8% of calls (76,363)98 ms–105 ms: 6.3% of calls (70,386)105 ms–113 ms: 6.0% of calls (67,597)113 ms–121 ms: 5.6% of calls (63,278)121 ms–129 ms: 4.9% of calls (54,884)129 ms–139 ms: 3.6% of calls (40,574)139 ms–149 ms: 2.4% of calls (27,416)149 ms–160 ms: 1.7% of calls (19,327)160 ms–171 ms: 1.3% of calls (14,262)171 ms–183 ms: 0.7% of calls (7,823)183 ms–197 ms: 0.5% of calls (5,990)197 ms–211 ms: 0.4% of calls (4,293)211 ms–226 ms: 0.3% of calls (2,837)226 ms–242 ms: 0.2% of calls (2,147)242 ms–260 ms: 0.1% of calls (1,159)260 ms–279 ms: 0.098% of calls (1,102)279 ms–299 ms: 0.0093% of calls (104)299 ms–320 ms: 0.030% of calls (331)343 ms–368 ms: 0.0054% of calls (61)368 ms–395 ms: 0.0037% of calls (42)395 ms–423 ms: 0.011% of calls (123)643 ms–689 ms: 0.00027% of calls (3)689 ms–739 ms: 0.0087% of calls (98)739 ms–792 ms: 0.018% of calls (202)792 ms–849 ms: 0.038% of calls (429)849 ms–910 ms: 0.079% of calls (880)p50 84.4 msp99 207 ms
Customer call latency, merged across groups. Each log time axis spans p0.1 to p99.9 of its calls; bar height is a bin's share of calls, and lines mark p50 and p99.
Show as a table
Customer reads, all groups
LatencyCallsShareCumulative
0.0283 ms–0.0349 ms1<0.01%0.00%
0.0349 ms–0.043 ms1390.01%0.01%
0.043 ms–0.053 ms1,6010.06%0.07%
0.053 ms–0.0653 ms6,5430.25%0.32%
0.0653 ms–0.0805 ms13,3410.51%0.83%
0.0805 ms–0.0992 ms21,9930.84%1.67%
0.0992 ms–0.122 ms34,8731.34%3.01%
0.122 ms–0.151 ms51,3651.97%4.97%
0.151 ms–0.186 ms68,7282.63%7.60%
0.186 ms–0.229 ms86,1593.30%10.90%
0.229 ms–0.282 ms100,5063.85%14.75%
0.282 ms–0.348 ms113,9874.37%19.12%
0.348 ms–0.428 ms128,8734.94%24.05%
0.428 ms–0.528 ms145,7625.58%29.63%
0.528 ms–0.65 ms163,9896.28%35.91%
0.65 ms–0.802 ms174,9426.70%42.61%
0.802 ms–0.988 ms180,5426.91%49.53%
0.988 ms–1.22 ms181,5566.95%56.48%
1.22 ms–1.5 ms180,5616.91%63.39%
1.5 ms–1.85 ms177,8876.81%70.21%
1.85 ms–2.28 ms171,5166.57%76.77%
2.28 ms–2.81 ms156,2155.98%82.76%
2.81 ms–3.46 ms129,8944.97%87.73%
3.46 ms–4.27 ms96,8993.71%91.44%
4.27 ms–5.26 ms66,9352.56%94.01%
5.26 ms–6.48 ms45,6841.75%95.75%
6.48 ms–7.98 ms32,9731.26%97.02%
7.98 ms–9.84 ms22,2640.85%97.87%
9.84 ms–12.1 ms16,5960.64%98.51%
12.1 ms–14.9 ms11,9210.46%98.96%
14.9 ms–18.4 ms9,0690.35%99.31%
18.4 ms–22.7 ms5,8020.22%99.53%
22.7 ms–28 ms3,8080.15%99.68%
28 ms–34.5 ms2,6430.10%99.78%
34.5 ms–42.5 ms1,5120.06%99.84%
42.5 ms–52.4 ms5060.02%99.86%
52.4 ms–64.5 ms2740.01%99.87%
64.5 ms–79.5 ms94<0.01%99.87%
79.5 ms–98 ms92<0.01%99.87%
98 ms–121 ms25<0.01%99.87%
121 ms–149 ms69<0.01%99.88%
149 ms–183 ms127<0.01%99.88%
183 ms–226 ms85<0.01%99.89%
226 ms–279 ms87<0.01%99.89%
279 ms–343 ms26<0.01%99.89%
343 ms–423 ms91<0.01%99.89%
423 ms–521 ms85<0.01%99.90%
521 ms–643 ms1330.01%99.90%
643 ms–792 ms95<0.01%99.90%
792 ms–976 ms87<0.01%99.91%
976 ms–1.2 s95<0.01%99.91%
1.2 s–1.48 s5<0.01%99.91%
1.48 s–1.83 s82<0.01%99.92%
1.83 s–2.25 s2,2170.08%100%
Customer writes, all groups
LatencyCallsShareCumulative
13.9 ms–14.9 ms1<0.01%0.00%
14.9 ms–16 ms2<0.01%0.00%
16 ms–17.2 ms1<0.01%0.00%
17.2 ms–18.4 ms3<0.01%0.00%
18.4 ms–19.7 ms2<0.01%0.00%
19.7 ms–21.2 ms11<0.01%0.00%
21.2 ms–22.7 ms18<0.01%0.00%
22.7 ms–24.3 ms28<0.01%0.01%
24.3 ms–26.1 ms43<0.01%0.01%
26.1 ms–28 ms730.01%0.02%
28 ms–30 ms1580.01%0.03%
30 ms–32.2 ms5090.05%0.08%
32.2 ms–34.5 ms9100.08%0.16%
34.5 ms–37 ms1,9280.17%0.33%
37 ms–39.6 ms4,0360.36%0.69%
39.6 ms–42.5 ms8,4350.75%1.44%
42.5 ms–45.5 ms14,1541.26%2.71%
45.5 ms–48.8 ms23,9592.14%4.84%
48.8 ms–52.4 ms34,8533.11%7.95%
52.4 ms–56.1 ms49,0364.38%12.33%
56.1 ms–60.2 ms60,7895.42%17.75%
60.2 ms–64.5 ms71,0066.34%24.09%
64.5 ms–69.2 ms76,4516.82%30.91%
69.2 ms–74.2 ms76,6036.84%37.75%
74.2 ms–79.5 ms77,5986.92%44.67%
79.5 ms–85.2 ms79,0057.05%51.72%
85.2 ms–91.4 ms79,0267.05%58.78%
91.4 ms–98 ms76,3636.81%65.59%
98 ms–105 ms70,3866.28%71.87%
105 ms–113 ms67,5976.03%77.90%
113 ms–121 ms63,2785.65%83.55%
121 ms–129 ms54,8844.90%88.45%
129 ms–139 ms40,5743.62%92.07%
139 ms–149 ms27,4162.45%94.52%
149 ms–160 ms19,3271.72%96.24%
160 ms–171 ms14,2621.27%97.51%
171 ms–183 ms7,8230.70%98.21%
183 ms–197 ms5,9900.53%98.75%
197 ms–211 ms4,2930.38%99.13%
211 ms–226 ms2,8370.25%99.38%
226 ms–242 ms2,1470.19%99.57%
242 ms–260 ms1,1590.10%99.68%
260 ms–279 ms1,1020.10%99.78%
279 ms–299 ms1040.01%99.79%
299 ms–320 ms3310.03%99.81%
343 ms–368 ms610.01%99.82%
368 ms–395 ms42<0.01%99.82%
395 ms–423 ms1230.01%99.83%
643 ms–689 ms3<0.01%99.84%
689 ms–739 ms980.01%99.84%
739 ms–792 ms2020.02%99.86%
792 ms–849 ms4290.04%99.90%
849 ms–910 ms8800.08%99.98%
910 ms–976 ms2290.02%99.99%
976 ms–1.05 s10<0.01%100%

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

Run passed. 8/8 group audits passed. 8 injected leader failures; quorum recovery 565–776 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.

Do slow work outside.

Call APIs, send email or render previews from ordinary worker processes. Flower tracks what needs doing and who is doing it.

External workers →

Read from any replica.

Spread reads across servers. They are fresh by default; opt into faster, possibly stale reads per query.

Reads & live queries →

Survive a failure.

Run three servers and lose any one. A write is confirmed only once a majority has saved it to disk.

Run a cluster →