ElementsStore

Overview

Data at rest — SQL, KV, files, and search indexes through one typed fx.store handle.

Store is how your backend holds data at rest. Declare a facet once (store.sql, store.kv, store.files, store.index), then read and write through fx.store(decl) inside Flows.

For developers persisting domain data on okengine — one handle shape, drivers swapped by environment.

The one rule

World access goes through fx.store(decl). Drivers are protocol-named in oke.config.ts (postgres, redis, s3 — never vendors). Application code never imports a driver client directly.

Four facets, one handle

fx.store(db)…
  • sql
    select

    Relational tables

    store.sql(name, opts)

    Single-table session — ops cycle through one row

    Domain tables, relations, constraints

  • kv
    set

    Key-value space

    store.kv(name)

    set → TTL drains → key expires

    Cache, sessions, rate limits

  • files
    put

    Blob bucket

    store.files(name)

    put into the bucket, get the bytes back

    Uploads, exports, attachments, image variants

  • index
    search

    Search index

    store.index(name, opts)

    search fans out — hits ranked by score

    Vector (dims) or full-text

Smallest Example

Declare a SQL store

src/db/schema.decl.ts
import { store, field } from "okengine";

export const notes = store.schema.table("notes", {
  id: field.id().primaryKey(),
  title: field.text().notNull(),
  createdAt: field.timestamp().notNull().now(),
});

export const db = store.sql("app", { schema: { notes } });

Read and write in a Flow

src/flows/notes/create.ts
import { on, flow, http } from "okengine";
import { z } from "zod";
import { db, notes } from "@/schema";

export const create = on(
  http.post({
    in: z.object({ title: z.string().min(1) }),
  }),
  flow({
    do: async ({ title }, fx) => {
      const id = fx.id();
      await fx.store(db).insert(notes).values({ id, title });
      return fx.json.create({ id, title });
    },
  }),
);

Call the endpoint

curl -X POST http://localhost:6530/notes \
  -H "content-type: application/json" \
  -d '{"title":"Ship notes"}'

Response:

{
  "data": { "id": "…", "title": "Ship notes" },
  "error": null
}

Effects are inferred

Every fx.store touch is recorded on the Flow as reads / writes (sql:app, kv:sessions, files:uploads, …). That powers the Manifest, Console, cache invalidation, and least privilege — no hand-written effect lists.

Progressive Patterns

Same fx.store(decl) from a row insert to cache, blobs, and hybrid search:

Declare tables with store.schema.table + field.*, then use the session handle:

src/flows/notes/[id]/get.ts
import { on, flow, http } from "okengine";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db, notes } from "@/schema";

export const get = on(
  http.get({
    in: z.object({ id: z.string() }),
    errors: { NotFound: z.object({ id: z.string() }) },
  }),
  flow({
    do: async ({ id }, fx) => {
      const [note] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
      if (!note) return fx.fail("NotFound", { id });
      return note;
    },
  }),
);

Prefer store.resource when you want five CRUD Flows in one factory.

Facet Reference

FacetDeclarefx.store handleResource refDefault drivers (dev / test / prod)
SQLstore.sql(name, opts?)select / insert / update / page / search / …sql:namepostgres / pglite / postgres
KVstore.kv(name, opts?)get · set · delete · list · ttlMskv:nameredis / memory / redis
Filesstore.files(name, opts?)put · get · delete · list · image · putImagefiles:names3 / memory / s3
Indexstore.index(name, opts?)vector or Meilisearch (by driverId)index:namepin explicitly — no three-env default

Built-in hybrid search (fx.store(db).search) lives on the SQL handle — see Search. store.index is the optional external engine facet.

Per-environment drivers

Standard starters inherit DRIVER_DEFAULTS. Pin only when you diverge; vendor choice lives under images, not driver ids:

oke.config.ts
import { defineConfig } from "okengine/config";

export default defineConfig({
  drivers: {
    // omit store.* to use defaults — pin only overrides
  },
  images: {
    store: {
      sql: "postgres:18-alpine",
      kv: "redis:8-alpine",
      files: "rustfs/rustfs:1.0.0-rc.5",
    },
  },
});
FacetProtocol idsBest for
sqlpostgres · pglite · memoryDomain tables, RLS, live queries, BM25
kvredis · memory (+ durable SQL via oke_kv)Sessions, caches, short-lived locks
filess3 · fs · memoryUploads, exports, image variants
indexmemory · pgvector · meilisearchHosted FTS / ANN outside the primary table

The Capabilities of Store

Troubleshooting

Learn more

Next

On this page