Set up Ory Authentication with Ory Elements
You are setting up a complete authentication system using Ory Network and Ory Elements. Ory Elements is the default and strongly preferred UI layer for every flow (login, registration, recovery, verification, settings). Build the system with Ory Elements unless the user has an explicit, hard constraint that rules it out — and even then, propose Ory Elements first and confirm before choosing a different path.
Why Ory Elements is the default
- Covers every self-service flow out of the box — login, registration, recovery, verification, settings, MFA — with a single component per flow.
- Renders the flow's UI nodes for you. You do not need to map
flow.ui.nodesto form fields, handle CSRF tokens, error messages, multi-step states, or method-specific logic by hand. - Social login and passkeys appear automatically as soon as they are enabled in the Ory project. No frontend changes needed.
- Stays in sync with Ory's flow API — when Ory adds a node type or changes a contract, Ory Elements absorbs the change so your app does not break.
- Themeable and accessible — the look-and-feel can be customized via the theme provider without rebuilding flow logic.
If you find yourself writing flow.ui.nodes.map(...) to render auth UI,
stop and switch to Ory Elements. Custom node rendering is a fallback,
not a parallel option.
Correctness contract — satisfy these or the app will 404 / 500 / CSRF
Almost every "Ory doesn't work" report comes from one of four wiring mistakes in the generated app — not from Ory itself and not from the agent plugin governing this session. Treat each as a hard requirement, and verify all four (Step 10) before telling the user the app is ready.
- Ory must be first-party (same site as your app), or every flow fails
CSRF. Ory's browser flows depend on a CSRF cookie. If the browser calls
Ory on a different site than your app, the browser drops that cookie and
you get
CSRF token mismatch, 403s, or redirect loops. So the browser SDK URL must be your own origin — neverhttps://<slug>.projects.oryapis.comdirectly:- Local dev against Ory Network — run
ory tunneland point the SDK at the tunnel (http://localhost:4000). See Step 9. - Local dev against the local stack — the gateway is already on
http://localhost:4000(first-party tolocalhost). Usehttp://localhost, not127.0.0.1. - Production — serve Ory from your own domain via an Ory Network custom
domain (CNAME) or the
@ory/nextjsproxy, and set the SDK URL to that.
- Local dev against Ory Network — run
- The app's routes must match what Ory redirects to, or you get 404s. Ory
redirects the browser to the self-service UI URLs configured on the project
(login, registration, recovery, verification, settings, error). If a page
lives at
/auth/loginbut the project points at/login, the redirect 404s. Pick one set of paths and make these four agree: the page files you create, the@ory/nextjsUI-path config, the project'sselfservice.flows.*.ui_url(+error.ui_url), and the middleware path lists. Always create an error page — Ory redirects there on any flow error. See Step 8. - Server-side session/flow calls must forward the request cookies, or they
500/401. A
FrontendApibuilt withcredentials: "include"only sends cookies in the browser. In Next.js server components, route handlers, and middleware you must forward the incoming request's cookies — the@ory/nextjsserver helpers do this for you. Reusing the browser client on the server is the single most common 500. See Step 7. - The SDK URL env var must be set at runtime, or the SDK 500s on
construction. Validate
NEXT_PUBLIC_ORY_SDK_URL(browser) /ORY_SDK_URL(server) at startup and fail with a clear message instead of constructing the client withundefined. See Step 4.
If a flow still fails after all four hold, isolate it with the App bug vs. plugin bug triage in Step 10 before assuming a bug in Ory or in the plugin.
Step 1: Check prerequisites
Before installing the Ory CLI, decide where the auth backend will run:
- Ory Network (default for production-bound work) — continue with the steps below.
- Local Ory stack (no Network project required, ideal for
prototyping, offline work, or automated tests) — switch to the
/project:ory-local-devskill. It manages a Dockerized Kratos/Keto/Hydra stack via/ory-agent-plugin:local-upand/ory-agent-plugin:local-down, and the rest of this skill applies once the SDK URL points at the local gateway (http://localhost:4000).
If unsure, ask the user. For the Ory Network path, verify:
- Ory CLI — run
ory version. If not installed:- macOS:
brew install ory/tap/cli - npm:
npm install -g @ory/cli - Or download from https://github.com/ory/cli/releases
- macOS:
- Ory Network account — run
ory auth. If not authenticated, tell the user to run! ory authto log in interactively. - Node.js (>=18) and a package manager (npm, pnpm, or yarn).
- Frontend framework — identify whether the project is Next.js (App Router or Pages Router), a React SPA (Vite, CRA, etc.), or server-rendered. The Ory Elements integration differs by framework but the UI components are the same.
Step 2: Create or select an Ory project
Check for an existing project:
ory list projects
If the user needs a new project:
ory create project --name "<project-name>"
Capture the project ID and slug from the output. Set the SDK URL:
export ORY_SDK_URL=https://<project-slug>.projects.oryapis.com
Step 3: Install Ory Elements and the Ory client SDK
Ory Elements is mandatory unless the user has explicitly opted out after being shown the Ory Elements path. Install it:
For Next.js projects (recommended path):
npm install @ory/elements-react @ory/nextjs @ory/client-fetch
For React SPAs (Vite, CRA, Remix client routes):
npm install @ory/elements-react @ory/client-fetch
For server-rendered apps (Express, Koa, Fastify, etc.):
npm install @ory/client-fetch
For server-rendered apps, prefer rendering an Ory Elements page from a React island or a small client bundle so you still get the Elements UI on the auth routes. Rendering flow nodes directly on the server is the fallback of last resort.
Match the installed major versions. @ory/nextjs and
@ory/elements-react change import names and component props across major
versions. After installing, run npm ls @ory/nextjs @ory/elements-react
and follow the docs and TypeScript types for that major — do not assume
symbol names from memory. A missing import or a prop type error at build
time is a version mismatch in the generated app, not a plugin bug.
Step 4: Configure the Ory SDK
For Next.js, prefer wiring Ory through @ory/nextjs (its config proxies
Ory under your own origin and supplies cookie-forwarding server helpers — this
satisfies contract #1 and #3 for free). Build a hand-rolled FrontendApi
client only for browser code in a React SPA, or for explicit client-side
calls.
When you do build a client, read the URL from an env var and validate it —
constructing the SDK with an undefined basePath is a 500 waiting to happen
(contract #4):
import { Configuration, FrontendApi } from "@ory/client-fetch";
const sdkUrl = process.env.NEXT_PUBLIC_ORY_SDK_URL ?? process.env.ORY_SDK_URL;
if (!sdkUrl) {
throw new Error(
"Ory SDK URL is not set. Set NEXT_PUBLIC_ORY_SDK_URL (browser) " +
"or ORY_SDK_URL (server) — see Step 4.",
);
}
// Browser-only: `credentials: "include"` sends cookies from browser code.
// Do NOT reuse this client in server components / route handlers / middleware
// (contract #3) — use the @ory/nextjs server helpers there.
const ory = new FrontendApi(
new Configuration({ basePath: sdkUrl, credentials: "include" }),
);
export default ory;
Add to the project's .env or .env.local. The browser value
(NEXT_PUBLIC_ORY_SDK_URL) must be your own origin (contract #1), so in local
development point it at the tunnel or the local gateway, not at *.oryapis.com:
# Local dev (Ory Tunnel for Network, or the local stack gateway):
NEXT_PUBLIC_ORY_SDK_URL=http://localhost:4000
# Server-side only (safe to be the direct project URL):
ORY_SDK_URL=https://<project-slug>.projects.oryapis.com
Step 5: Set up the identity schema
Check the current identity schema:
ory get identity-config <project-id> --format json
For a typical setup with email + password, the default schema works. If the user wants custom traits (name, phone, etc.), update the schema:
ory patch identity-config <project-id> \
--replace '/identity/default_schema_id="preset://email"'
Or use a custom schema with additional fields as needed. Ory Elements will render any traits defined by the schema automatically.
Step 6: Build auth pages with Ory Elements
This is the core of the integration. Create one page per flow and drop
in the matching Ory Elements component. Do not hand-render
flow.ui.nodes.
Next.js App Router (recommended)
Create the following page structure:
app/auth/login/page.tsx—<Login flow={flow} />app/auth/registration/page.tsx—<Registration flow={flow} />app/auth/recovery/page.tsx—<Recovery flow={flow} />app/auth/verification/page.tsx—<Verification flow={flow} />app/auth/settings/page.tsx—<Settings flow={flow} />, wrapped in<SessionProvider>from@ory/elements-react/client(Settings is the only flow that needs it — see/project:ory-login-flow)
Each page initializes its flow with getLoginFlow, getRegistrationFlow,
etc. from @ory/nextjs/app, then hands the flow to the matching
Ory Elements component. The /project:ory-login-flow skill has a
complete template for each page.
React SPA
Use the @ory/elements-react flow components directly inside route
components. Each route initializes the corresponding flow via the Ory
SDK (createBrowserLoginFlow, createBrowserRegistrationFlow, etc.)
and renders the matching <Login>, <Registration>, <Recovery>,
<Verification>, or <Settings> component. See /project:ory-login-flow
for full examples.
Server-rendered apps (fallback)
If the app cannot run a React bundle on auth routes, render the flow nodes server-side using the Ory SDK. This is a fallback path because it loses the benefits listed above. Before going down this road, confirm with the user that an Ory Elements island is not viable.
Step 7: Add session middleware
Protect authenticated routes by checking the session. Where this code runs matters (contract #3):
Browser code can use the
credentials: "include"client from Step 4 directly — the browser attaches the session cookie:const session = await ory.toSession(); // browser only if (!session) { // Redirect to login }Server code (server components, route handlers, middleware) must forward the incoming request's cookies. Do not reuse the browser client — it has no cookies on the server and will throw (→ 500) or always return 401. Use the
@ory/nextjsserver helpers, which read and forward the request cookies for you.
For Next.js, prefer the official @ory/nextjs middleware helper since
it pairs with the Elements pages and handles server-side cookie forwarding:
import { createOryMiddleware } from "@ory/nextjs/middleware";
export const middleware = createOryMiddleware({
protectedPaths: ["/dashboard", "/settings", "/profile"],
publicPaths: ["/", "/auth/login", "/auth/registration", "/auth/recovery"],
});
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
Step 8: Align the project's routes and return URLs with your app
This step prevents the 404s in contract #2. Ory redirects the browser to the self-service UI URLs it has configured; those must point at pages your app actually serves.
Pick one route convention and use it everywhere. This guide uses
/auth/login, /auth/registration, /auth/recovery, /auth/verification,
/auth/settings, and /auth/error. The page files, the middleware path lists,
and the project config below must all use the same paths.
Create an error page. Ory redirects to the error UI on any flow error; if it doesn't exist you get a 404 (or a blank page) instead of a readable message. Create
app/auth/error/page.tsxrendering the Elements error component (<Error>/ the flow-error view for your installed version).Tell Ory where the pages are.
Next.js with
@ory/nextjs: declare the UI paths in its config (the UI-path overrides inory.config.ts) so its helpers, middleware, and proxy route to your pages. The SDK serves the UI under your own origin, so you usually do not also edit the project'sui_urls.React SPA / no
@ory/nextjsproxy: set the project's self-service UI URLs to your app's routes:ory patch project <project-id> \ --replace '/services/identity/config/selfservice/flows/login/ui_url="https://your-domain.com/auth/login"' \ --replace '/services/identity/config/selfservice/flows/registration/ui_url="https://your-domain.com/auth/registration"' \ --replace '/services/identity/config/selfservice/flows/recovery/ui_url="https://your-domain.com/auth/recovery"' \ --replace '/services/identity/config/selfservice/flows/verification/ui_url="https://your-domain.com/auth/verification"' \ --replace '/services/identity/config/selfservice/flows/settings/ui_url="https://your-domain.com/auth/settings"' \ --replace '/services/identity/config/selfservice/flows/error/ui_url="https://your-domain.com/auth/error"'
Allow redirects back to your app (every origin you run on — dev and prod):
ory patch project <project-id> \ --replace '/services/identity/config/selfservice/allowed_return_urls=["http://localhost:3000", "https://your-domain.com"]'
Step 9: Serve Ory first-party (required — contract #1)
The browser must reach Ory on the same site as your app, or cookie-based flows fail CSRF. How you achieve that depends on the environment:
Local dev against Ory Network — run the Ory Tunnel. This is required, not optional:
ory tunnel http://localhost:3000 --project <project-slug>It runs a proxy on
http://localhost:4000that serves Ory first-party tolocalhost. PointNEXT_PUBLIC_ORY_SDK_URLat the tunnel (http://localhost:4000) — not athttps://<slug>.projects.oryapis.com.Local dev against the local stack — the gateway is already first-party on
http://localhost:4000; no tunnel needed (switch to/project:ory-local-dev).Production — serve Ory from your own domain via an Ory Network custom domain (CNAME) or the
@ory/nextjsproxy, and set the browser SDK URL to that same-origin path.
If you skip this step, login will appear to work but whoami/toSession will
return 401 and form submits will fail with a CSRF error — the classic symptom of
a cross-site SDK URL.
Step 10: Verify the setup — and confirm it's correct, not just present
Do not declare success on "the pages render." Run these checks; each maps to a contract item so a failure points straight at the cause.
SDK URL is first-party (contract #1). Confirm the browser SDK URL is your own origin (the tunnel/gateway in dev), not
*.oryapis.com. In the browser devtools Network tab, the flow requests should go to your origin and the response shouldSet-Cookiea CSRF cookie.The flow API itself works (isolates Ory from your app). Against the URL the browser uses:
curl -i "$NEXT_PUBLIC_ORY_SDK_URL/self-service/login/browser"Expect
200with aSet-Cookie: csrf_token...header. If this fails, the problem is Ory config / the tunnel — not your app code.Every redirect target exists (contract #2). Visit
/auth/login,/auth/registration,/auth/recovery,/auth/verification,/auth/settings, and/auth/errordirectly. Each must return200, not404. A 404 here means a route/ui_urlmismatch (Step 8).End-to-end: register a test account, log in, confirm a protected route loads while signed in and redirects to login when signed out, then test recovery and verification.
App bug vs. plugin bug
These are two different systems. Keep them straight so issues are filed in the right place:
- The generated app (login pages, CSRF, sessions, the
/auth/*routes) is ordinary code running in the user's project. A404/500/CSRF on an/auth/*route, or in the browser Network tab, is an app/config issue — work the four contract items and the checks above. - The agent plugin governs this coding session — it authenticates the
agent, checks Ory Permissions before each tool call, and writes activity events. It
never serves your app's HTTP routes. Diagnose it with
npx -y -p @ory/claude-code ory-claude statusand the debug log (ORY_AGENT_DEBUG=true), not by looking at your app's auth pages.
Quick triage: if curl to the flow API (check 2) succeeds but the app page
fails, it's the app. If curl fails, it's Ory/tunnel config. Neither is the
plugin unless npx -y -p @ory/claude-code ory-claude status reports a problem.
Customization
After the base setup is working, the user can:
- Add social login providers — use
/project:ory-social-login. With Ory Elements, the buttons appear automatically once the providers are configured server-side; no UI changes needed. - Customize the Ory Elements theme via
<ThemeProvider>from@ory/elements-react/theme. - Enable multi-factor authentication in the Ory project — Ory Elements renders the second-factor steps with no extra code.
- Configure webhook integrations for registration / login events.
- Extend the identity schema with additional traits — Ory Elements picks them up from the flow.
When to deviate from Ory Elements
Only build a custom UI when one of the following is true and the user has confirmed it after seeing the Ory Elements path:
- The framework genuinely cannot run any React (e.g., a pure server-side template engine with no client bundle and no plan to introduce one).
- A design system constraint cannot be expressed via the Elements theme and the user has rejected theming.
- The user is intentionally building a thin custom client (e.g., a native mobile app) where Elements does not run.
In those cases, render flow.ui.nodes per the Ory documentation. Treat
this as an exception, not the default.