ElementsStore

Files

Object storage buckets — put/get, putImage variants, LQIP placeholders, and chainable Bun.Image transforms.

The Files facet is a typed object bucket for uploads, exports, and media. Declare with store.files(name), then call fx.store(decl).

For developers storing blobs on okengine — put bytes, optionally derive image variants, serve keys from Flows.

The one rule

Pass the declaration into fx.store(uploads). There is no fx.store.files namespace, no presignPut, and no transform(…) helper — use put / get / putImage / image(…).

Smallest Example

Declare a bucket

src/core.ts
import { store } from "okengine";

export const uploads = store.files("uploads");

Put and get in a Flow

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

export const create = on(
  http.post({
    in: z.object({
      name: z.string().min(1),
      bytes: z.string(), // base64 for the demo
    }),
    out: z.object({ key: z.string() }),
  }),
  flow({
    do: async ({ name, bytes }, fx) => {
      const key = `uploads/${fx.id()}-${name}`;
      const data = Uint8Array.from(atob(bytes), (c) => c.charCodeAt(0));
      await fx.store(uploads).put(key, data);
      return { key };
    },
  }),
);

Call the endpoint

curl -X POST http://localhost:6530/uploads \
  -H "content-type: application/json" \
  -d '{"name":"note.txt","bytes":"aGVsbG8="}'

Response:

{
  "data": { "key": "uploads/…-note.txt" },
  "error": null
}

Effects are inferred

Every fx.store(uploads) touch stamps reads / writes as files:uploads on the Flow. That powers the Manifest, Console, and least privilege — no hand-written effect lists.

Progressive Patterns

From a bare put to list/delete, image variants, and the chainable pipeline:

put accepts Uint8Array or UTF-8 string. get returns bytes or null:

src/flows/exports/create.ts
import { on, flow, http } from "okengine";
import { uploads } from "@/core";

export const create = on(
  http.post(),
  flow({
    do: async (_, fx) => {
      const key = `exports/${fx.id()}.csv`;
      await fx.store(uploads).put(key, "id,title\n1,Ship\n");
      const bytes = await fx.store(uploads).get(key);
      return { key, bytes: bytes?.byteLength ?? 0 };
    },
  }),
);

Method Reference

fx.store(filesDecl):

MethodSignatureMeaning
putput(key, data)Store bytes or UTF-8 string
getget(key)Read bytes, or null if missing
deletedelete(key)Remove; returns whether deleted
listlist(prefix?)List object keys (optional prefix)
imageimage(source, options?)Chainable Bun.Image pipeline
putImageputImage(key, data, opts?)Original + variants + optional LQIP

Handle also exposes ref (files:name) and driverId (memory · fs · s3).

There is no public presignPut, presignGet, copy, head, or transform alias on the fx handle.

Object Keys

Keys are opaque strings the driver stores as-is. Pick a stable prefix + unique suffix; keep SQL rows pointing at the key, not the bytes.

Explicit key — build the path in the Flow:

const key = `avatars/${fx.auth.userId}/${fx.id()}.png`;
await fx.store(uploads).put(key, data);

Content hash — when the same bytes should collapse to one object, hash yourself (SHA-256 hex) and put under that digest. The facet does not auto- dedupe on put.

Driver guards — the fs driver throws on path escape:

Invalid object key: ../secret
RuleWhy
Prefer printable ASCIIS3-compatible signed URLs are fragile with non-ASCII keys
No leading /Absolute paths are rejected on fs
No .. segmentsPath escape is rejected on fs
Stable extensionsMIME / Console kind inference uses the suffix

Blob Operations

Each verb binds through fx.store(decl) inside flow():

Write bytes or a UTF-8 string. Overwrites an existing key:

src/flows/notes/[id]/attachment/create.ts
import { on, flow, http } from "okengine";
import { z } from "zod";
import { uploads } from "@/core";

export const create = on(
  http.post({
    in: z.object({
      id: z.string(),
      name: z.string().min(1),
      bytes: z.string(),
    }),
  }),
  flow({
    do: async ({ id, name, bytes }, fx) => {
      const key = `notes/${id}/${name}`;
      const data = Uint8Array.from(atob(bytes), (c) => c.charCodeAt(0));
      await fx.store(uploads).put(key, data);
      return { key };
    },
  }),
);

putImage

Detailed section

If you only need the original + one thumb, jump to Progressive Patterns → putImage. This section covers options, variant key naming, and decode guards.

putImage(key, data, opts?) writes the original at key, then optional named variants beside it, then optionally returns a ThumbHash LQIP data URL on the result (the placeholder is not stored as an object).

const result = await fx.store(uploads).putImage(key, data, {
  placeholder: true,
  variants: {
    thumb: { resize: [320], webp: { quality: 80 } },
    card: { resize: [800, 600, { fit: "inside" }], jpeg: { quality: 85 } },
  },
  maxPixels: 4096 * 4096,
  autoOrient: true,
});

Image Pipeline

Detailed section

Prefer putImage when you want original + named variants in one call. Use image(…) when you need a one-off transform, metadata probe, or custom out key.

putImage — one write, many keys

{stem}.{variant}.{ext}
putImage
put
  • photos/x.jpg
  • photos/x.thumb.webp
  • photos/x.medium.webp
  • result.placeholder → data:image/…
Original stays at the source key; each variant is {stem}.{variant}.{ext}. Optional placeholder: true returns a ThumbHash LQIP data URL — not a fourth object.

Chain on fx.store(uploads).image(source, options?):

StepMeaning
.resize(w, h?, opts?)Resize (Bun fit options)
.rotate(degrees) · .flip() · .flop()Geometry
.modulate({ brightness?, saturation? })Color
.jpeg / .png / .webp / .heic / .avifEncode
.metadata()Width / height / format (header)
.bytes() · .blob()Materialize
.placeholder()ThumbHash data URL
.put(outKey)Write the transformed bytes

Decode guards (maxPixels, autoOrient) pass as the second argument to image(…), same defaults as putImage.

Declare Options

OptionTypeDefaultMeaning
descriptionstringomittedConsole / Manifest label
export const uploads = store.files("uploads", {
  description: "User uploads and derived image variants",
});

Drivers

DriverRuns asBest for
s3S3-compatible (RustFS in Compose)Dev + prod default
fsLocal filesystem rootSingle-node laptop paths
memoryProcess mapTest default

Defaults: s3 / memory / s3 (dev / test / prod). Image pins (e.g. RustFS) live under images.store.files — the driver id stays s3.

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

export default defineConfig({
  drivers: {
    // omit store.files to use defaults — pin only overrides
    // store: { files: { dev: "fs", test: "memory", prod: "s3" } },
  },
  images: {
    store: {
      files: "rustfs/rustfs:1.0.0-rc.5",
    },
  },
});

For s3, Compose / env typically supply S3_ENDPOINT, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, and optional S3_REGION / S3_SESSION_TOKEN. Without S3_ENDPOINT, CreateBucket is skipped so real AWS is not auto-provisioned.

Troubleshooting

Learn more

  • Store — four facets; driver defaults
  • SQL — relational data beside blob keys
  • HTTP — routes that accept uploads
  • Vault — credentials for S3 when you configure bindings
  • fxfx.store(decl)
  • Configurationdrivers.store.files

Next

On this page