Decoupling Identity: Implementing Self-Service Registration with Ory Kratos
Learn how to implement self-service registration using Ory Kratos' headless architecture, utilizing JSON schemas and state-driven flows to decouple identity logic from your UI.
09 Jun 2026, 14:50 UTC

The Friction of Custom Identity UIs
Most identity providers force a trade-off: use a hosted login page that looks like a generic template, or spend weeks building a custom authentication system from scratch to maintain brand consistency. The problem isn't the UI itself, but the state machine behind it—handling password validation, email verification, and session handoffs without introducing security holes.
Ory Kratos solves this by using a headless architecture. It manages the identity logic, validation, and storage via a REST API, while leaving the presentation layer entirely to you. The takeaway is simple: you don't build a registration system; you build a frontend that talks to a state-driven identity API.
How the Headless Registration Flow Works
In Kratos, registration is not a single POST request. It is a Flow. A flow is a stateful process identified by a unique Flow ID. When a user hits your "Sign Up" page, your application requests a registration flow from Kratos. Kratos returns a Flow ID and a set of requirements based on your identity schema.
This decoupled approach means the backend doesn't care if your UI is a React SPA, a mobile app, or a server-side rendered page. It only cares that the Flow ID is maintained and that the submitted data matches the schema.
Defining Your Identity Schema
Before a user can register, you must define what an "identity" looks like. Kratos uses JSON Schema to enforce these rules. This prevents the common issue of "schema drift," where the database allows fields that the application doesn't validate.
For example, if you need a user's birthdate and a specific set of preferences, you define them in the identity schema. Kratos will then automatically generate validation errors if a user submits a date in the wrong format, removing the need for redundant validation logic in your frontend code.
Worked Example: Initiating a Registration Flow
To start the process, your frontend must request a flow. This is typically done via a GET request to the Kratos public API. Run this command from a terminal with access to your Kratos instance (assuming default ports and local deployment):
# Request a new registration flow
curl -X GET "http://localhost:4433/self-service/registration?return_to=http://localhost:3000/dashboard"Expected Result: The API will respond with a 303 See Other redirect or a JSON body containing a flow_id. This ID must be passed back to Kratos in all subsequent registration steps.
Required Permissions: The API endpoint is public, but the return_to parameter must be a URL previously whitelisted in the Kratos configuration to prevent open-redirect vulnerabilities.
Submitting the Registration Data
Once you have the flow_id, you submit the user's data (email, password, etc.) to the flow endpoint:
# Submit registration data
curl -X PATCH "http://localhost:4433/self-service/registration?flow=FLOW_ID_HERE" \\
-H "Content-Type: application/json" \\
-d '{"traits": {"email": "[contact removed]"}, "password": "secure-password-123"}'Risk: If the JSON schema requires a field (e.g., username) that you omit in the traits object, Kratos will return a 400 Bad Request with a detailed list of validation errors. You must map these errors to your UI fields to guide the user.
The Trade-off: UI Boilerplate
The primary limitation of the headless approach is the implementation overhead. Because Kratos does not provide a default UI, you are responsible for:
- Handling the redirect loops between your app and the Kratos API.
- Managing the session cookies or tokens returned after a successful flow.
- Building the forms to display the dynamic errors returned by the JSON schema validation.
This is a conscious architectural decision: you trade initial development speed for total control over the user experience and security logic.
Verifying the Implementation
To ensure your registration flow is working correctly, do not rely solely on the UI. Check the underlying state:
- Database Check: Query your identity store (e.g., PostgreSQL) to ensure a new record exists in the
identitiestable after the flow completes. - Flow State: Use the Kratos Admin API to inspect the status of a flow ID to see if it is
completedor still awaiting verification. - Verification Loop: Trigger a registration and verify that the user is placed in a "verification required" state if email verification is enabled, preventing them from accessing the dashboard until the email link is clicked.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.