| name | ory-auth-setup |
| description | Set up a complete authentication system using Ory Network with Ory Elements as the UI layer. Use whenever the user wants to add login, registration, account recovery, email verification, account settings, or session management to a web app — even if they only mention "auth", "sign-in", or "users". Ory Elements is the default UI; only fall back to custom rendering when Ory Elements cannot be used. |
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.nodes to 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 — never https://<slug>.projects.oryapis.com
directly:
- Local dev against Ory Network — run
ory tunnel and 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 to localhost). Use http://localhost,
not 127.0.0.1.
- Production — serve Ory from your own domain via an Ory Network custom
domain (CNAME) or the
@ory/nextjs proxy, and set the SDK URL to that.
- 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/login but 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/nextjs UI-path config, the project's selfservice.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
FrontendApi built with credentials: "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/nextjs
server 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 with undefined. 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
ory-local-dev skill. It manages a Dockerized
Kratos/Keto/Hydra stack via ory-local-up and ory-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:
- Ory Network account — run
ory auth. If not authenticated,
tell the user to run ! ory auth to 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.",
);
}
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:
NEXT_PUBLIC_ORY_SDK_URL=http://localhost:4000
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 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 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 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();
if (!session) {
}
-
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/nextjs server 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.tsx rendering 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 in ory.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's ui_urls.
-
React SPA / no @ory/nextjs proxy: 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:4000 that serves Ory first-party to
localhost. Point NEXT_PUBLIC_ORY_SDK_URL at the tunnel
(http://localhost:4000) — not at https://<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 ory-local-dev).
-
Production — serve Ory from your own domain via an Ory Network custom
domain (CNAME) or the @ory/nextjs proxy, 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 should Set-Cookie a 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 200 with a Set-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/error directly. Each must return 200, not
404. A 404 here means a route/ui_url mismatch (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. A 404/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 trace spans. It
never serves your app's HTTP routes. Diagnose it with
npx -y -p @ory/codex ory-codex status and 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/codex ory-codex status reports a problem.
Customization
After the base setup is working, the user can:
- Add social login providers — use
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.