HighX
Server

Authorization

Declaring who may call each function, and the permission table behind it.

Every exposed function declares who may call it. There is no default — omitting auth fails the build with the declaration site named.

auth: true,                      // any signed-in caller
auth: false,                     // deliberately public
auth: can("notes:delete"),       // a capability — prefer this
auth: canAll("notes:write", "notes:publish"),
auth: roles("admin"),            // a job title — the escape hatch
auth: policy("isOwner", (ctx) => ctx.identity.userId === owner),

Being public is something you write down, not something you forget. The two mistakes do not cost the same, so one of them is a build failure.

Prefer can() over roles()

A function depends on being allowed to do something, not on who the caller is. Naming a role in a descriptor spreads your org chart across the codebase — widening what editor may do then means finding every function that mentioned it. roles() stays for checks that genuinely are about identity; a break-glass admin route reads better as roles("admin") than laundered through a permission.

The permission table

Roles are what your identity provider issues. Permissions are what your code depends on. One table maps between them:

export const permissions = definePermissions({
  member: ["notes:read", "notes:write"],
  admin:  ["notes:read", "notes:write", "notes:delete", "tenant:export"],
});

The compiler checks every can() and roles() against it, so a mistyped permission fails the build instead of denying every caller at runtime for a reason nothing reports.

Wildcards disable that check beneath them. notes:* grants everything under notes: — including notes:delet — so the compiler cannot call a typo a mistake. Enumerate actions where you want them checked.

defineAuth additionally rejects a role that is issued but holds no permissions, or holds permissions but is never issued. Both are silent at runtime and are usually a typo.

When the check runs

Before arguments are validatedA caller who may not invoke a function cannot probe its argument schema
At the storage authorityNot at the edge, so it cannot be routed around
On every recomputationA user whose role is withdrawn stops receiving patches, rather than keeping a live feed

unauthorized vs forbidden

Denials distinguish nobody is signed in (unauthorized — signing in would help) from we know who you are, and the answer is no (forbidden). A client that conflates them sends a signed-in user back to a login page forever.

Row-level checks stay in handlers

The gate answers "may this caller invoke this function", which is a property of the caller. Whether they may touch one particular row depends on that row:

handler: (ctx, args) => {
  const note = ctx.db.table("notes").get(args.id);
  if (note?.["authorId"] !== ctx.identity.userId) {
    throw new HighXError("forbidden", "That note is not yours");
  }
  ctx.db.table("notes").delete(args.id);
},

Auditing it

highx generate prints the policy beside every function and writes the whole surface to .highx/auth.ts, so a test can assert on it:

query    notes.list      can("notes:read")
mutation notes.save      can("notes:write")
mutation notes.purge     roles("admin")

Next

On this page