Binary and structured payloads
Two column types carry non-scalar values: s.bytes() for binary data and s.json() for structured
data. Both round-trip through the engine byte- and shape-exact — verified in
package/schema/conformance_test.ts.
Declare the payload columns
// src/schema.ts
import { s } from "@nzip/lofi/schema";
export const app = s.defineApp({
attachments: s.table({
fileName: s.string(),
thumbnail: s.bytes(),
metadata: s.json(),
}),
});
const attachment = await db.insert(app.attachments, {
fileName: "cover.png",
thumbnail: new Uint8Array(await blob.arrayBuffer()),
metadata: { width: 320, height: 180, palette: ["#282828", "#ebdbb2"] },
}).wait({ tier: "global" });
// Reads return the same Uint8Array bytes and the same nested JSON shape.
Choosing between them
s.bytes()stores an opaqueUint8Array: thumbnails, hashes, encrypted blobs, anything the app treats as bytes. One pinned constraint: payloads under 32 bytes are unreliable in the current alpha — pad short values, or store small binary-ish data ass.json()until upstream fixes it.s.json()stores any JSON value and reads back the same nested shape. Reach for it when the data is a document the app consumes whole. When you need to filter on a field, promote that field to its own typed column instead —whereaddresses columns, not JSON internals.s.array(inner)sits between the two: use it when the value is a flat list of one typed element ands.json()when the shape nests.
Rows travel through sync whole, so a payload column is for data sized like a row — icons and metadata, not video. Large media belongs outside the table with a payload column holding the reference (hash, URL, or identifier).