Elements

Gate

Permission to act — auth policies and rate limits attached to the trigger, evaluated before any effect runs, denied as typed errors.

Gate is how your app answers "may this happen?" before it happens: is this request from a verified member? has this IP exhausted its minute quota? Gates attach to the trigger — the pipeline evaluates them before a single store write, emit, or channel send runs.

The one rule

Permission sits at the trigger, and denial is a typed error value — never a thrown exception, never an effect that runs halfway and rolls back.

Quick start

Declare your gates

Policies are named predicates; rate limits are declarative budgets. Both live once, app-wide:

src/gates.ts
import { gate } from "okengine";

export const member = gate.policy("member", ({ auth }) => !!auth?.verified);

export const fair = gate.rate({
  strategy: "sliding-window-counter", // the default — near-exact, no boundary bursts
  max: 60,
  per: "1m",
  keyBy: "ip",
});

Attach them to triggers

.gate(...) composes in registration order — evaluated left to right, first denial wins:

src/flows/links/shorten.ts
export const shorten = on(
  http.post("/links").gate(member, fair),
  flow({
    in: NewLink,
    out: LinkCode,
    errors: { Taken },
    do: async ({ url, code }, fx) => {
      /* runs only if both gates passed */
    },
  }),
);

Denials are typed failures

A denied request never reaches do. It returns one of three typed failures, like any entry in errors: { … }:

Gate resultTyped failure
Policy denied, not authenticatedUnauthorized
Policy denied, authenticatedForbidden (with gate name + reason)
Rate limit exceededRateLimited (with retryAfterMs)

Two kinds of gates

DeclarationQuestion it answersEvaluated against
gate.policy(name, check)Is this principal allowed? (ABAC)auth / operator / request metadata
gate.rate(options)Is there budget left for this subject?an atomic counter on the kv driver

The policy context

The predicate receives everything it may decide with — nothing else:

FieldContents
authUser principal: userId, scopes (a Set), verified
operatorOperator principal (Console plane): id
metaRequest metadata for keying decisions: ip, userId, …
export const admin = gate.policy("admin", ({ auth }) => auth.scopes.has("admin"));

A policy name containing : is also a Module:Action permission — the same declaration feeds role-based access reviews in the Console.

Rate options

OptionTypeDefaultMeaning
maxnumber— (required)Takes allowed within per
perstring— (required)Window / refill period ("1m", "60s", …)
keyBystringSubject dimension ("ip", "user", …)
strategyRateStrategy"sliding-window-counter"Algorithm (below)
overridablebooleanfalseAllow the Console to retune max/per live
StrategyBehavior
sliding-window-counterNear-exact, two keys, no boundary bursts — the default
fixed-windowCheapest; allows bursts at window boundaries
sliding-logExact; stores one timestamp per take
token-bucketSteady refill; permits saved-up bursts
leaky-bucketSmooths output to a constant rate

All five run as atomic Lua on the kv driver — correct under concurrency, identical on the memory driver in tests.

Identity comes from auth

Gates don't log users in — they decide about principals that already exist. The built-in auth plugin (okengine/auth) provides the user plane: hybrid sessions, argon2id password hashing, roles, API keys, and invites, with zero-config defaults. Whatever it establishes lands on fx.auth, which is exactly what your policies read.

Every decision is recorded

Pass and deny, every evaluation lands on the run's gates dimension in the telemetry trace. The Console (:6533 → Gates) shows which gates fired on a request and their outcomes — an audit trail of permission decisions you never had to build. Rate gates declared overridable: true can also be retuned (max/per) from the Console without a redeploy; anything else rejects edits by design.

Troubleshooting

Learn more

  • Flow — the trigger pipeline gates plug into
  • Console · Gates — decision audit and live retuning
  • Vault — the credentials your policies protect

Next

On this page