Decoupling UI from Auth with Ory Kratos Self-Service Flows
Use Ory Kratos headless self-service flows to keep authentication state in Kratos while rendering registration and login UI in your own application.
15 Feb 2026, 08:04 UTC

Problem: Auth UI tightly coupled to product code slows iteration
When authentication screens live in the same repo as business logic, a label change or new field requires a deploy to the auth layer. Teams end up batching UX tweaks with backend releases and risking regressions in login and registration.
Thesis: Let Kratos drive the flow state, you own the rendering
Ory Kratos is headless. It exposes a REST API for identity management and returns a flow ID plus a description of the current step. Your application renders the form from that description, posts data back, and handles redirects. Identity storage stays in Kratos, UI stays in your app.
How self-service flows are structured
Kratos models registration, login, recovery and settings as state machines. Starting a flow returns an identifier and UI hints for the current step. The API does not render HTML.
- Start a flow. Call the browser endpoint for the desired action, e.g. registration. The response contains a flow ID and a UI description.
- Render from ui.nodes. Each node describes a field name, type and validation hints derived from the active JSON Schema.
- Submit the flow. POST the collected values to the method endpoint with the flow ID. Kratos validates against the schema and returns either errors in ui.messages or advances the flow.
- Session and redirect. On success Kratos can set a session cookie and redirect to the after URL you configured. Your app remains stateless about auth logic.
Worked example: Local registration flow setup
The steps below outline a local setup. Replace placeholders with your values. Behavior varies by Kratos version; verify with your deployed version.
- Run Kratos via Docker. Requires Docker daemon access and port 4433 free.
- Create an identity schema with the Ory CLI. Requires Ory CLI installed and access to the Kratos admin API.
- Start a registration flow from a machine that can reach Kratos.
- Submit the flow.
- Verify session.
docker run -d --name kratos \
-p 4433:4433 \
-e DSN=memory \
-e SERVE_PUBLIC_API=true \
oryd/kratos:latest serve --dev
Risk: memory DSN is non-persistent. Do not use in production.
ory create schema --name registration --format jsonschema
Define traits in JSON Schema. Schema changes affect validation without database migrations.
curl -X POST http://localhost:4433/self-service/registration/browser -H 'Content-Type: application/json' -d '{}'
Inspect the JSON response for flow id and ui.nodes. Do not assume field names; they follow the active schema.
curl -X POST 'http://localhost:4433/self-service/registration/methods?flow=FLOW_ID' \
-H 'Content-Type: application/json' \
-d '{"traits":{"email":"[contact removed]"}}'
Replace FLOW_ID with the value from step 3 and traits with schema-required fields. Check for Set-Cookie and Location headers on success.
Use the session cookie to call a protected resource you control. Verify the response reflects an authenticated identity. Exact endpoint depends on your application.
Trade-off: Boilerplate and redirect safety
Headless control comes with integration cost. You must map Kratos validation errors to your form UI, handle each flow type separately, and manage redirect URIs.
- Error mapping requires translating ui.messages to user-facing text.
- Incorrect selfservice.registration.after or redirect URIs can cause loops or open redirect risks.
- Schema-driven forms reduce DB migrations but increase upfront schema design work.
Actionable closing
Start with one flow, e.g., registration, and implement error mapping first. Centralize redirect URL validation and log flow errors at debug level during development. Keep schemas versioned and test changes against the API before updating the UI.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.