Getting Started
From zero to a published, discoverable OAuth provider — on whatever backend you run — and a safe integration with someone else's.
Page 01 covered the theory: flows, tokens, OIDC, and where fine-grained authorization decides what a token may do. This page is hands-on. You'll scaffold a project with the CLI, meet the three SDK legs, publish to the discovery registry, connect to another provider, and wire up authorization. Frauthy is a general wrapper: it speaks to Better Auth and Ory through a common adapter, and integrates with anything that speaks OIDC.
Three things, two of them you already have.
Approved private-beta participants receive the supported toolchain and setup procedure with their invitation. Fine-grained authorization can be introduced when you reach Step 05; this public guide focuses on the architecture and workflow rather than executable setup.
terminal — check your toolchain
# Approved participant setup [PRIVATE_BETA_INSTALL_INSTRUCTIONS_REDACTED]
frauthy init is an interactive flow. It asks a handful of questions and writes a working project — no boilerplate to copy.
frauthy init
┌─────────────────────────────┐ │ ╔═╗ Ⓕ F R A U T H Y │ │ ╚═╝ trust, in transit │ └─────────────────────────────┘ ◇ What is your provider called? │ Alice Identity │ ◇ Registry handle (slug) │ alice │ ◇ Provider domain (issuer URL) │ https://auth.alice.com │ ◇ Which provider backend does Frauthy wrap? │ ● Ory — Hydra + Kratos, self-host or Ory Network │ ◇ Discovery visibility │ ● Unlisted — hidden, resolvable by exact handle │ ◇ Which SDK legs do you need? │ ◼ provider ◼ connect ◼ authz │ ◇ Which authorization backend? │ ● Ory Keto — Zanzibar, pairs with Ory │ ◆ Created 7 files │ └ Frauthy ready. ory provider, unlisted in discovery.frauthy.dev
The result is a small, legible project. Each module you picked drops in one example file you own and edit:
alice/ — scaffolded project
alice/ ├─ frauthy.config.ts ← single source of truth ├─ package.json ├─ .env.example ├─ README.md └─ src/ ├─ provider.ts ← wraps your backend ├─ connect.ts ← connect └─ authz.ts ← authz
Your whole configuration lives in one typed file. The CLI and every SDK read from it, so there's one place to change a domain, a registry, or your visibility.
frauthy.config.ts
import { defineConfig } from "[PRIVATE_BETA_PACKAGE_COORDINATE_REDACTED]"; export default defineConfig({ service: { slug: "alice", name: "Alice Identity", issuer: "https://auth.alice.com", visibility: "unlisted", }, // swap kind for "better-auth" or "oidc" — nothing else changes provider: { kind: "ory", sdkUrl: process.env.ORY_SDK_URL!, apiKey: process.env.ORY_API_KEY }, registry: { url: process.env.FRAUTHY_REGISTRY_URL!, apiKey: process.env.FRAUTHY_API_KEY }, authz: { kind: "keto", readUrl: process.env.KETO_READ_URL!, writeUrl: process.env.KETO_WRITE_URL! }, modules: ["provider", "connect", "authz"], });
Frauthy's SDK is split by which side of a connection you're on. Approved participants enable only the capabilities they need.
Register OAuth clients, manage user sessions, and emit lifecycle events — through an adapter for Better Auth or Ory.
Discover a provider, register a client dynamically, and run PKCE, refresh, and client-credentials flows. Pure OIDC, so it works with any backend.
Object-level check()s and delegation against SpiceDB or Ory Keto — one interface, both Zanzibar engines.
frauthy.ts — create the application SDK contract
import { createFrauthy } from "[PRIVATE_BETA_PACKAGE_COORDINATE_REDACTED]"; export const frauthy = createFrauthy({ provider: { issuer: "https://auth.alice.com", audience: "alice-web" }, store: { kind: "spicedb", endpoint: process.env.SPICEDB_ENDPOINT!, token: process.env.SPICEDB_TOKEN!, }, });
route.ts — verify a session and decide access
import { resource } from "[PRIVATE_BETA_PACKAGE_COORDINATE_REDACTED]"; import { frauthy } from "./frauthy"; const session = await frauthy.session(request); const portal = resource("portal", "alice"); if (!(await session.can("access", portal))) { return new Response("Forbidden", { status: 403 }); }
Site UI SDK — documentation bridge
import { DocumentationBridge } from "[PRIVATE_BETA_PACKAGE_COORDINATE_REDACTED]"; <DocumentationBridge heading="Go from example to contract" sdk={sdkReference} ui={uiCatalog} note="Runtime SDKs and UI SDKs solve different problems." />
One adapter contract sits under the provider leg. Pick a backend; everything above it — publishing, lifecycle, connect — is identical.
The OIDC plugin issues tokens; Frauthy registers clients and manages sessions through it.
Clients map to Hydra's /admin/clients; identities and sessions to Kratos. Self-host or Ory Network.
Anything that serves a well-known document can be published and connected to — admin ops stay with that system.
src/provider.ts
import { createProvider, oryAdapter } from "[PRIVATE_BETA_PACKAGE_COORDINATE_REDACTED]"; import config from "../frauthy.config"; // One surface, any backend. Swap the adapter and nothing below changes: // betterAuthAdapter(auth, { issuer }) ← Better Auth const provider = createProvider(oryAdapter({ issuer: config.service.issuer, sdkUrl: process.env.ORY_SDK_URL!, // Hydra admin → /admin/clients apiKey: process.env.ORY_API_KEY, })); // Register an app that will sign users in through you const client = await provider.clients.register({ name: "Alice Web", redirectUris: ["https://app.alice.com/callback"], scopes: ["openid", "profile", "email"], }); // React to the whole auth lifecycle (audit, webhooks, authz sync) provider.lifecycle.on((e) => log.info(e.type));
Publishing reads your provider's live OIDC metadata and registers a descriptor so others can find and integrate with you — at the visibility you choose.
Anyone can find and integrate with you. For open platforms and marketplaces.
Resolvable only if someone knows your exact handle or issuer. The quiet default.
Resolvable solely with an authorized token. For internal or invite-gated providers.
Approved publication workflow
[PRIVATE_BETA_INSTALL_INSTRUCTIONS_REDACTED]Approved participants can change visibility through the publication workflow or programmatically through publishService(config, { visibility }). The descriptor is rebuilt from the discovery document every time, so it never drifts from reality.
This is the part Frauthy exists for. Resolve another provider by handle, register a client against it automatically, and you have a fully wired connection — no secrets traded over chat.
src/connect.ts
import { discover, connectTo } from "[PRIVATE_BETA_PACKAGE_COORDINATE_REDACTED]"; import config from "../frauthy.config"; // 1 — resolve Bob from the registry (or a direct issuer URL) const bob = await discover("bob", { registry: config.registry.url }); // 2 — register a client against Bob via dynamic client registration const client = await connectTo(bob, { redirectUri: "https://app.alice.com/cb/bob", scopes: ["openid", "read:files"], }); // 3 — send the user through Code + PKCE (keep the verifier for the callback) const { url, codeVerifier } = await client.authorizationUrl(); // ...on callback: const tokens = await client.exchange(code, codeVerifier);
Prefer the terminal? frauthy connect bob resolves the provider and prints exactly this wiring for you to drop in.
A valid token is not a yes. Gate each call on token scope and an object-level check — against SpiceDB or Ory Keto, your choice. The two-gate pattern from Page 01.
src/authz.ts
import { createAuthz, FRAUTHY_BASE_SCHEMA } from "[PRIVATE_BETA_PACKAGE_COORDINATE_REDACTED]"; import config from "../frauthy.config"; // kind: "spicedb" | "keto" — picked from config, same surface either way const { backend, delegation } = createAuthz(config.authz!); if (backend.supportsSchemaWrite) await backend.writeSchema(FRAUTHY_BASE_SCHEMA); // (Keto's model is OPL, uploaded with the Ory CLI — see FRAUTHY_KETO_OPL) // Gate 2: may THIS user edit THIS document? const { allowed } = await backend.check({ resource: { type: "document", id: "q3-plan" }, permission: "edit", subject: { type: "user", id: "alice" }, }); if (!allowed) throw forbidden(); → 403 // Delegate read-only to a service — caveat-boxed on SpiceDB, app-boxed on Keto await delegation.grant({ resource: { type: "document", id: "q3-plan" }, service: "alice-bot", mode: "read", expiresAt: new Date(Date.now() + 3_600_000), });
Three processes during development. In production each scales on its own.
terminal — three tabs
# 1 · your provider (Ory — or `pnpm dev` for Better Auth) $ ory tunnel http://localhost:3000 # 2 · local discovery registry $ pnpm discovery ▸ Frauthy Discovery listening on http://localhost:8788 # 3 · authz engine — Ory Keto (or `spicedb serve --http-enabled`) $ keto serve -c keto.yml # publication access is provided to approved participants [PRIVATE_BETA_INSTALL_INSTRUCTIONS_REDACTED]
That's the whole loop: init → publish → connect → check. Your provider is live on your domain, listed exactly as visibly as you want, integrating with others through standard OIDC — with object-level authorization on every call.
You've now seen both halves of Frauthy — the theory of delegated access and the tools that make operating it a matter of minutes. Keep them side by side.