PackagesBindings

@lunora/bindings

The lightweight Cloudflare binding helpers for Lunora in one install — ctx.kv, ctx.images, ctx.analytics, ctx.pipelines, ctx.vectors, ctx.r2sql — with per-binding subpaths and tree-shaking.

@lunora/bindings bundles the thin, zero-dependency Cloudflare binding helpers into a single install, exposed as per-binding subpaths. Each is a typed ctx.* facade over its Cloudflare binding; codegen wires the matching context helper when a lunora/ source imports the subpath (or reads the ctx.* property).

pnpm add @lunora/bindings
SubpathContext helperCloudflare bindingWhere
@lunora/bindings/kvctx.kvWorkers KVQuery / Mutation / Action
@lunora/bindings/imagesctx.imagesCloudflare ImagesAction only
@lunora/bindings/analyticsctx.analyticsAnalytics EngineQuery / Mutation / Action
@lunora/bindings/pipelinesctx.pipelinesPipelines (R2-backed)Action only
@lunora/bindings/vectorsctx.vectorsVectorizeQuery / Mutation / Action
@lunora/bindings/r2sqlctx.r2sqlR2 SQL (Apache Iceberg)Action only

sideEffects: false subpath exports keep tree-shaking per-binding: an app that imports only @lunora/bindings/kv bundles nothing from the other helpers. Heavier add-ons with framework/driver peer deps (@lunora/browser, @lunora/hyperdrive, @lunora/ai, @lunora/payment) stay separate installs.

KV — @lunora/bindings/kv

Typed Workers KV with scoped key helpers, available on every context. Add a kv_namespaces binding (env.KV) to your wrangler.jsonc:

{ "kv_namespaces": [{ "binding": "KV", "id": "<your-kv-namespace-id>" }] }
import { mutation, query } from "@/lunora/_generated/server";
import { v } from "@lunora/values";

export const setFlag = mutation({
    args: { name: v.string(), enabled: v.boolean() },
    handler: async (ctx, { name, enabled }) => ctx.kv.put(`flag:${name}`, { enabled }),
});

export const getFlag = query({
    args: { name: v.string() },
    handler: async (ctx, { name }) => ctx.kv.get(`flag:${name}`),
});

Images — @lunora/bindings/images

Cloudflare Images transforms (resize / format / optimize) plus signed and unsigned delivery URLs. Action-only (non-deterministic compute). Add an images binding (env.IMAGES):

import { action } from "@/lunora/_generated/server";

export const thumbnail = action({
    handler: async (ctx) => {
        // transform(input, transformOptions?, outputOptions?) — sizing and output format are separate args.
        const out = await ctx.images.transform(sourceStream, { width: 128 }, { format: "image/webp" });
        return out;
    },
});

Build delivery URLs without a binding via the helpers:

import { buildImageDeliveryUrl, buildSignedImageUrl } from "@lunora/bindings/images";

Analytics — @lunora/bindings/analytics

Analytics Engine: typed writeDataPoint (and an ergonomic track) plus a SQL-API read client. Fire-and-forget writes ride every context. Add an analytics_engine_datasets binding (env.ANALYTICS):

import { mutation } from "@/lunora/_generated/server";

export const recordSignup = mutation({
    handler: async (ctx) => {
        ctx.analytics.track("signup", { dimensions: { plan: "pro" }, metrics: { mrr: 20 } });
    },
});

The SQL-API read client is a separate import for dashboards/reports:

import { createAnalyticsSqlClient } from "@lunora/bindings/analytics";

Pipelines — @lunora/bindings/pipelines

Cloudflare Pipelines: durable, batched, R2-backed streaming ingestion. Action-only and fire-and-forget — never read a record back in-handler. Add a pipelines binding (env.PIPELINES) created with wrangler pipelines create:

import { action } from "@/lunora/_generated/server";

export const ingest = action({
    handler: async (ctx) => {
        await ctx.pipelines.send({ userId: "u_1", event: "purchase", amount: 19.99 });
    },
});

Vectors — @lunora/bindings/vectors

Cloudflare Vectorize: typed vector indexes and similarity search, wired from defineVectorIndex / inline .vectorize() declarations and surfaced as ctx.vectors.

import { action } from "@/lunora/_generated/server";
import { v } from "@lunora/values";

export const search = action({
    args: { query: v.string() },
    handler: async (ctx, { query }) => ctx.vectors.query("docs", { input: query, topK: 5 }),
});

query(index, { input }) embeds input with the index's configured embedder, then runs the search; pass a precomputed vector instead of input to skip embedding. upsert / upsertMany write vectors back.

R2 SQL — @lunora/bindings/r2sql

A typed, chainable query builder over R2 SQL (serverless queries against Apache Iceberg tables): window functions, DISTINCT, set operations. Action-only — every query is an external HTTPS round-trip and is not tracked by Lunora live queries.

import { action } from "@/lunora/_generated/server";
import { desc } from "@lunora/bindings/r2sql";

export const topEvents = action({
    handler: async (ctx) => ctx.r2sql.from("events").select("type", "COUNT(*) AS total").groupBy("type").orderBy(desc("total")).limit(10),
});

Tag descending order with desc(...) (or asc(...)); a bare string column sorts ascending. Aggregates go in the select list as raw SQL expressions.