Auth & the scoped principal
One authorization gate for both audiences — run(input, ctx). ctx is the principal; an agent at /mcp is just another caller through the same gate.
One gate, both audiences#
June has exactly one authorization gate, and both your UI and an agent pass
through it: a defineAction()'s run(input, ctx).
export const deletePost = defineAction({
id: "deletePost",
input: { id: "string" },
run: async (input, ctx) => {
const post = await db.posts.find(input.id);
if (post.authorId !== ctx.user?.id) throw new Error("forbidden");
return db.posts.delete(input.id);
},
});
There is no "expose to agents" step and no second permission system. The action is a server action and an MCP tool; the check you write once protects both.
ctx is the principal#
ctx answers who is calling — ctx.user, ctx.session, and the request
(url, params). That is identity, and identity is all ctx carries: data
lives on the ambient db instead (see the data model), so
the only reason to reach for ctx is authorization.
An agent is just another caller#
When an agent calls a tool at /mcp, it carries the caller's credential, so the
SAME run(input, ctx) sees the SAME ctx it would for a UI request — and runs
the SAME check. The agent is a scoped principal: it can do exactly what that
user can do, no more. Nothing is re-declared; no tool gets a parallel ACL.
UI button ─┐
├─ run(input, ctx) ← one gate, one ctx, one check
agent /mcp ─┘
Better Auth by default#
Authentication itself — sessions, providers, the login flow — is Better
Auth, June's blessed default. A first-class integration (wiring a Better Auth
session straight into ctx as the principal, and into /mcp scoping) is
coming soon. Until then you can wire it by hand, and bring-your-own auth
works today — June only needs you to populate ctx.user / ctx.session; the
gate doesn't care who issued the session.
Why it matters#
The expensive bug in an agent-ready app is an authorization gap between what the UI enforces and what the tool endpoint allows. June closes it by construction: there is only one gate, so there is nothing to keep in sync.
Status: Changing — the model (one gate,
ctx-as-principal) is settled; the Better Auth integration is in progress. See Stability.