The data model
Resources are declared, not wired. db / kv / blob ride an ambient scope — never ctx, which is identity only. Bring Juno or your own.
Declared, not wired
Data is a declaration. Naming a resource in june.config.ts enables it — omit
it and it doesn't exist; an unused one compiles away. Each gets a zero-config
local default in june dev and a deploy binding on each target.
// june.config.ts
import { defineJune } from "@junejs/core/config";
import { sqlite } from "@junejs/server/db";
export default defineJune({
resources: {
db: sqlite(), // dev: ./.june/dev.sqlite · deploy: D1 / Turso
},
});
db is ambient — never on ctx
You reach a resource with an ambient handle. There is no request object to thread:
import { db } from "@junejs/db";
// the SAME import works in a loader, a view, a defineAction(), or a plain
// model file three calls deep — nothing to pass down.
const users = await db.query("select id, name from users order by id");
Keeping db off ctx is deliberate — it's the line that makes the whole model
coherent:
ctxis identity;db/kv/blobare capability.ctxanswers who is calling (user, session, url, params) — what authorization needs. The resources answer what tools exist. Mixing them onto one object forces every helper to threadctxjust to touch the database (the Expressreq.dbanti-pattern). Instead the host runs each request inside a scope that holds the opened resources, anddb/kv/blobread it throughAsyncLocalStorage— so domain code never sees the request, and stays edge-safe (the async context loads lazily; nothing pulls a staticnode:*into the worker).
This is why an agent and a human run identical data code: there is no ctx to
thread or mock, and the authorization that matters lives in one place —
run(input, ctx) (see Auth & the scoped principal).
Bring your own
The default layer is Juno, but the magic — auto-batched reads and
auto-invalidated cache (see Queries & caching) —
is a property of a small public trace contract (recordTableRead /
recordTableWrite), not of Juno:
- Tier 1 — bring Prisma or Drizzle untouched.
- Tier 2 — run them over the same connection to share config.
- Tier 3 — add a thin shim that names the table read or written, and get the same auto-cache behavior.
Why it matters
The data layer is the part of an app an agent writes most — and the part where a
threaded request object or a forgotten revalidate() does the most damage.
Making db ambient (nothing to thread) and invalidation automatic (nothing to
remember) removes both failure modes by construction, for humans and agents
alike.
Status: Changing — the model above is settled; the query/resource surface is still being refined. See Stability.