Without auth, every exposed method is public. With it, each call passes one check before it runs: the credentials become a principal, then the method's access rule decides.
Protect an app
This notes app accepts JWTs signed with a managed key. Everyone must sign in except for health, and only a note's owner can read it.
import { collection, define, fail, jwtBearer, key, mutation, query, v } from "@flower-js/sdk";
const sessions = key("sessions", { algorithm: "Ed25519", usages: ["verify"] });
const notes = collection("notes", v.object({ owner: v.string(), text: v.string({ max: 10_000 }) }));
const noteId = v.string({ min: 1, max: 128 });
const read = query("note.read", {
args: noteId,
access: (ctx, principal, id) => principal !== null && ctx.get(notes, id)?.owner === principal.subject,
}, (ctx, id) => ctx.get(notes, id));
const write = mutation("note.write", {
args: v.object({ id: noteId, text: v.string({ max: 10_000 }) }),
}, (ctx, { id, text }) => {
const owner = ctx.principal()!.subject;
if ((ctx.get(notes, id)?.owner ?? owner) !== owner) fail("NOT_OWNER", "Someone else owns this note");
ctx.set(notes, id, { owner, text });
return null;
});
const health = query("health", { access: "public" }, () => "ok");
const app = define({
auth: { authenticate: jwtBearer({ key: sessions, issuer: "notes", audience: ["notes-api"] }) },
http: { "note.read": read, "note.write": write, health },
});
export default app;jwtBearerverifies tokens natively. Callers send the token ascredentials: the token itself,"Bearer …", or{ token }.- No credentials means an anonymous caller. A bad or expired token is rejected with
UNAUTHENTICATED. - The principal is the token's
sub, itstenantclaim if any, and all its claims. Passprincipal: (claims) => …to map claims yourself. - A managed key is added to the app's keys for you. A raw key needs
algorithms. - From the terminal, pass
--credentialsor setFLOWER_CREDENTIALS.
Access per method
| Access | Who may call |
|---|---|
"public" | Anyone, including anonymous callers. |
"authenticated" | Any principal. Anonymous callers get 403 with UNAUTHENTICATED. |
(ctx, principal, args) => boolean | Whoever the predicate allows; false is 403 with FORBIDDEN. args have already passed the method's schema, and principal is null for anonymous callers. |
- Methods without
accessfollowauth.default:"authenticated"when there is anauthenticate, otherwise"public". auth.sessionssets access to retry sessions; it defaults toauth.default.- Inside a method,
ctx.principal()returns the caller, ornullfor anonymous callers. - The check reads a fresh snapshot, but it runs before the mutation. Recheck anything that can change in between inside the mutation, like
note.writedoes.
Authenticate your own way
authenticate can be any function of the credentials. Return a principal { subject, tenant?, claims? }, return null for an anonymous caller, or fail() to reject the call.
const sessions = collection("sessions", v.object({ subject: v.string(), expiresAt: v.int() }));
const app = define({
collections: [sessions],
auth: {
authenticate: (ctx, credentials) => {
if (credentials === null) return null;
const session = typeof credentials === "string" ? ctx.get(sessions, credentials) : null;
if (!session || session.expiresAt <= ctx.now()) fail("UNAUTHENTICATED", "Sign in again");
return { subject: session.subject };
},
},
http: { "note.read": read, "note.write": write, health },
});A sign-in mutation creates session rows. Deleting a row revokes the session at the next call, retry or watch refresh.
How the check runs
- It runs before every call, retry replay, cached read and watch refresh. Watches close when a refresh is denied; data already sent can't be taken back.
- Credentials are separate from arguments. A retry with refreshed credentials for the same subject and tenant gets the original result; a different caller can't.
- Flower adds the check only when a call could be refused: with
authenticateordelegation, or a method whose access is a predicate.access: "public"alone adds nothing. - The check has a cost: replica-local reads become fresh, cached reads wait for full admission, and some write batching is skipped.
- In a named partition, the principal's tenant must be the partition's name. Anonymous callers are admitted there with that tenant.
- Transaction participants see the coordinator's principal, with the participant partition as its tenant.
auth.delegation(ctx, coordinator, principal)can refuse a coordinator; by default, cluster peers are trusted. - Authorization doesn't secure peer or operator traffic. Protect the network and the operator token too.
Every option is in the authorization reference. The test database runs your access rules, but can't verify JWTs.