ElementsGate

Authentication

Enable gate.auth so sessions and API keys fill fx.auth — then protect Flows with policy gates.

Authentication is configured as oke({ gate: { auth } }). That bag issues /auth/* Flows (unless http: false), fills fx.auth, and leaves permission to policy gates — there is no gate.auth handle for .gate(...).

For developers shipping signed-in APIs on okengine — turn on auth, plug a method, attach policies.

The one rule

Turn on gate.auth for identity. Attach gate.policy / gate.scope (or .public()) for permission. Boot fails if an HTTP trigger has neither.

Smallest Example

Enable auth on the app

src/app.ts
import { oke } from "okengine";
import { username } from "okengine/plugins";

export const app = oke({
  name: "notes",
  env: "dev",
  gate: {
    auth: {
      // secret required in prod; minted in dev when omitted
      // basePath defaults to "/auth"
    },
  },
}).plug(username());

Declare a signed-in policy and attach it

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

export const member = gate.policy("member", {
  description: "Signed-in user",
  check: ({ auth }) => !!auth.verified,
});
src/flows/profile/get.ts
import { on, flow, http } from "okengine";
import { member } from "@/core/gate";

export const get = on(
  http.get().gate(member),
  flow({
    do: async (_, fx) => ({ userId: fx.auth.userId }),
  }),
);

Sign in and call

# Method routes live under basePath (default /auth) — see your plugged method docs
curl -X GET http://localhost:6530/profile \
  -H "accept: application/json" \
  -H "authorization: Bearer …"

Authenticated callers reach do with fx.auth.userId and fx.auth.scopes set. Anonymous callers fail the policy → typed Unauthorized.

Progressive Patterns

From Bearer-only identity to cookies, API keys, and method plugins:

Default transport is Authorization: Bearer <access>. The pipeline verifies the token into fx.auth before gate evaluation:

export const app = oke({
  name: "notes",
  env: "dev",
  gate: { auth: {} },
});

In production, set gate.auth.secret (or OKE_AUTH_SECRET). Omitting it in prod throws: gate.auth: secret is required in production (set gate.auth.secret or OKE_AUTH_SECRET).

Forged, expired, or revoked access tokens map to typed Unauthorized — they never become a principal.

Options

OptionTypeDefaultMeaning
secretstringminted in non-prodHMAC for access tokens; required in prod
basePathstring"/auth"HTTP prefix for auth Flows
httpbooleantruefalse skips /auth/* bindings (secret + tables only)
audiencestring"oke-app"Access-token audience claim
emailAndPassword.enabledbooleanfalseCredential method knobs
emailAndPassword.requireEmailVerificationbooleanfalseBlock sign-in until verified
session.accessTtlMsnumber14mAccess token lifetime
session.refreshTtlMsnumber30dRefresh token lifetime
session.freshAgeMsnumber24hMax age for "fresh" step-up policies
session.idleTtlMsnumberoffIdle timeout from last activity
session.absoluteTtlMsnumberoffAbsolute lifetime from creation
session.singleSessionPerUserbooleanfalseOne live family per user
cookiesbagoffHttpOnly cookie mirror
secondaryStoragebagoffHot-path KV cache (prefix default "auth:")
tenanttrue | bagoffMulti-tenancy — see Tenancy

What fx.auth carries

FieldMeaning
userIdPrincipal id, or null when anonymous
scopesReadonlySet<string> used by gate.scope (may include tenant-role union)
sessionScopesSession / JWT scopes before tenant-role union
verifiedSession / credential passed verification
apiKeyIdPresent when the principal is an API key

Inside do, read identity from fx.auth — not fx.user. World access stays on fx.

Sessions & Cookies

Detailed section

Defaults match a short-lived access token plus a long-lived refresh family. Override only what your product needs.

src/app.ts
export const app = oke({
  name: "notes",
  env: "prod",
  gate: {
    auth: {
      secret: process.env.OKE_AUTH_SECRET!,
      session: {
        accessTtlMs: 14 * 60 * 1000,
        refreshTtlMs: 30 * 24 * 60 * 60 * 1000,
        freshAgeMs: 24 * 60 * 60 * 1000,
        // idleTtlMs / absoluteTtlMs / singleSessionPerUser when needed
      },
      cookies: {
        enabled: true,
        prefix: "oke",
        sameSite: "lax",
        secure: true,
        path: "/",
      },
    },
  },
});
Cookie optionDefaultMeaning
enabledfalseOpt-in HttpOnly mirror
prefix"oke"Cookie name prefix
securetrueHTTPS-only
sameSite"lax""strict" | "lax" | "none"
path"/"Cookie path
crossSubdomainfalseShare across subdomains
domainExplicit cookie domain

Freshness: policies that require a recent sign-in should compare session age against session.freshAgeMs (default 24h). Step-up plugins (e.g. two-factor) build on the same window.

API Keys

Public routes

Health checks and login endpoints must declare open posture explicitly:

http.get().public();
// equivalent: http.get().gate(gate.public)

Auth method Flows under basePath register their own posture; your app routes still need .gate(...) or .public().

Set gate.auth.http: false when you want tables + Bearer verify without materializing /auth/* HTTP bindings (embedding / Console-style hosts).

Troubleshooting

Learn more

Next

On this page