Guide · 06

Decide who can call what.

Authenticate callers once, then give each public method an access rule. Flower checks it before every call, retry and watch refresh.

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.

notes.ts
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;
  • jwtBearer verifies tokens natively. Callers send the token as credentials: 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, its tenant claim if any, and all its claims. Pass principal: (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 --credentials or set FLOWER_CREDENTIALS.

Access per method

Access rules
AccessWho may call
"public"Anyone, including anonymous callers.
"authenticated"Any principal. Anonymous callers get 403 with UNAUTHENTICATED.
(ctx, principal, args) => booleanWhoever 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 access follow auth.default: "authenticated" when there is an authenticate, otherwise "public".
  • auth.sessions sets access to retry sessions; it defaults to auth.default.
  • Inside a method, ctx.principal() returns the caller, or null for 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.write does.

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.

Sessions stored in the database
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 authenticate or delegation, 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.