How Clerk works
This guide provides a deep dive into Clerk's architecture and internal workings. For developers who are simply looking to add authentication to their app, see the quickstart guides.
The frontend API
When you create a new application through the Clerk Dashboard, Clerk provisions a dedicated frontend API (FAPI) instance for your application. It is hosted at https://<slug>.clerk.accounts.dev
in development environments, where <slug>
is a unique identifier generated for your application. You can find your application's FAPI URL in the Domains page of the Clerk Dashboard.
When configuring your Clerk app, you must provide a Publishable Key. The Publishable Key tells your app what your FAPI URL is, enabling your app to locate and communicate with your dedicated FAPI instance.
The Clerk Publishable Key follows a specific format: it consists of your FAPI instance URL encoded in base64, prefixed with an environment identifier (e.g. pk_test_
for development environments, pk_live_
for production environments), and suffixed with a $
delimiter for future extensibility. The base64-encoded URL enables your application to locate and communicate with your dedicated FAPI instance. You can verify this structure by decoding the key yourself:
FAPI manages authentication flows on a per-user basis. For instance, it handles flows for signing up a user, retrieving a user's active sessions, creating an organization on behalf of a user, or fetching a user's organization invites. You can find the complete FAPI documentation here.
FAPI does not handle administrative actions that impact multiple users, such as listing all users, banning users, or impersonating a user. These types of tasks are handled by the backend API.
Some tasks, such as signing up a user, don't require authentication, as that would defeat the purpose of the endpoint. However, endpoints designed for authenticated users, like updating a user's details, require FAPI to first identify the user making the request and then verify their authorization. This ensures that users cannot modify another user's details. Typically, this is achieved by sending a signed token with the request, either as a cookie or a header. You can learn more about Clerk's authentication tokens later in this guide.
While it's possible to build complete authentication flows directly on top of the frontend API, it involves significantly more work. Most users prefer our frontend SDKs, which provide higher-level abstractions like the mountSignIn()
method or the <SignIn />
component (for React-based SDKs). These abstractions offer a well-tested, thoughtfully designed, a11y-optimized, and customizable UI for authentication flows, handling all possible configurations of your authentication preferences out-of-the-box.
Levels of abstraction
FAPI is the lowest level of abstraction that authentication flows can be built on with Clerk. However, there are several other abstraction layers that offer less work and more convenience.
Clerk's prebuilt components
Clerk's prebuilt UI components are Clerk's highest level of abstraction. They are "all in one" components, offering the most complete implementation of authentication with the least amount of effort. While it's strongly recommended to use these components, due to the amount of research we have put into delivering an optimal experience, it's not the only option if you feel that you need more control over your authentication flows.
Customizability: You can modify the CSS for any part of the prebuilt components, but not the HTML structure or the logic/ordering of how the authentication flow works.
Clerk Elements
The next level of abstraction is Clerk Elements, a headless UI library that provides the foundational building blocks used in Clerk's prebuilt components. Similar to established libraries like Radix, Reach UI, and Headless UI, Clerk Elements exposes a set of unstyled React components that handle complex authentication logic while giving you complete control over the presentation layer. Clerk Elements is still in beta, and only supports sign-up and sign-in flows, with more components planned for future releases.
Customizability: You have full control over both the CSS and the HTML structure of the components, but you can't change the logic/ordering of how the authentication flow works.
Custom flows
Finally, if you need complete control over the authentication flow, Clerk provides low-level primitives that directly wrap our API endpoints. These primitives enable you to build fully custom authentication flows from scratch. Clerk refers to these as "custom flows". While this approach offers maximum flexibility, it also requires significant development effort to implement and maintain. Custom flows should only be considered when you have specific requirements that cannot be met using the prebuilt components or Clerk Elements, as you'll need to handle all authentication logic, error states, and edge cases yourself.
Customizability: You have full control over every part of the authentication flow, including HTML structure, CSS, and the logic/ordering of how the authentication flow works.
The backend API
The frontend API (FAPI) is designed for use primarily from the frontend of your application. Its methods focus on signing in users and managing user-related resources and data once they are authenticated. However, as an application developer, you might also need to perform administrative tasks, such as modifying multiple user or organization details, retrieving a list of all users, banning or impersonating a user, and more.
To maintain security, these administrative tasks should only be executed on the server side using a secret key inaccessible to your users or the browser. These operations are handled by a separate API known as the backend API (BAPI). You can find the BAPI documentation here.
Although the administrative features of BAPI are useful for many applications, it's most commonly used to verify a user's authentication state when processing requests from your app's frontend. For instance, if a user submits a request to update some data associated with their account, your server must ensure the user is authenticated and authorized to make this change. Without proper validation, malicious actors could potentially take over user accounts.
Like FAPI, while you can interact directly with BAPI, most developers opt to use Clerk's SDKs for smoother integration with their preferred language or framework. Documentation for Clerk's SDKs is available in the left sidebar of the docs. That being said, FAPI is a much more complex and nuanced API that relies on more custom logic outside of its endpoints to create a functional SDK on top of it. As such, interacting directly with FAPI is not recommended, whereas interacting directly with BAPI is generally reasonable.
Stateful authentication
To understand how authentication works in Clerk, it's important to first understand how the most common implementation of authentication logic works: the traditional "stateful authentication" model, also known as "session token authentication".
A user's process of signing in would work as follows. This example assumes that the user already signed up and their credentials are stored in a database.
- The user initiates authentication by navigating to
example.com/sign-in
, entering their credentials (e.g. username/password), and submitting the form, usually by clicking a "submit" button. This makes a request to the server with the credentials. - The server validates the credentials against a database. This is normally done by hashing the password and comparing it with a stored password hash. Upon successful validation, it creates a new session in the database associated with the user.
- The server responds to the browser's request by setting the session ID in a
Set-Cookie
header in the response, which sets a cookie with this value in the browser. This cookie will be automatically included in future requests from the browser in order to authenticate the user. - The next time the browser sends a request to the server, it automatically includes the session cookie. The server checks the database for the session ID and retrieves the associated user ID and session metadata. If the session is valid and active, the server has verified that the user has authenticated, and can then use the user ID to fetch any required user data and process the request.
This is a perfectly reasonable authentication model and works great for most apps as it's straightforward to understand and implement. Additionally, since it checks the database for every request that requires authentication, sessions can be instantly revoked if needed (by setting the state to revoked
and adding a check in the server logic). However, because it requires every request to query the database, it introduces additional latency and can be difficult to scale as you start to shard out your database.
Stateless authentication
An alternative approach is "stateless" authentication, which addresses the scalability and latency challenges of stateful authentication while introducing different tradeoffs.
The stateless authentication flow operates as follows. This example assumes that the user already signed up and their credentials are stored in a database.
- The user initiates authentication by navigating to
example.com/sign-in
, entering their credentials (e.g. username/password), and submitting the form, usually by clicking a "submit" button. This makes a request to the server with the credentials. - The server validates the credentials against a database. Upon successful validation, it generates a cryptographically signed token containing essential user data like the user ID and any relevant claims.
- The server responds to the browser's request by sending the token in a
Set-Cookie
header in the response. This token serves as a self-contained proof of authentication, and will be included in future requests from the browser in order to authenticate the user. - The next time the browser sends a request to the server, it sends the cookie containing the token. The server verifies the signature of the token to ensure that it's valid, and then uses the user ID within the token to fetch any required user data and process the request.
While more complex to implement, this stateless model offers significant advantages. Because verifying the JWT doesn't require interacting with a database, the latency overhead and scaling challenges caused by database lookups are eliminated, leading to faster request processing.
However, this approach also comes with important technical tradeoffs. The most significant limitation is that JWTs cannot be revoked due to their self-contained nature. Since JWT validation happens locally without consulting a central authority (i.e., they never "phone home"), there's no direct mechanism to invalidate them before their natural expiration.
This creates challenges for session management. To forcibly terminate a user's session, you have two suboptimal choices:
- Wait for the JWT to expire naturally.
- Rotate the signing keys, which invalidates all active sessions across your entire user base, signing out all of your users.
Furthermore, even after rotating keys, the revocation may be delayed if your application caches the public key used for verification - a common practice for performance optimization. The cached key would continue to validate the old tokens until the cache expires.
This limitation poses significant security risks, as it hampers your ability to quickly respond to security incidents that require immediate session termination for specific users.
Clerk's authentication model
Clerk implements a hybrid authentication model that combines the benefits of both stateful and stateless approaches while mitigating their respective drawbacks, at the cost of adding a substantial amount of complexity to the implementation on Clerk's side. However, for a developer implementing Clerk, like you, this is all upside since the complexity is handled internally by Clerk.
The hybrid model incorporates the same flow when signing in as the stateless flow, but with a key change: the session token's expiration is decoupled from the session lifetime. See the following section for more details.
Authentication flow
This example assumes that the user already signed up and their credentials are stored in a database.
-
The user initiates authentication by navigating to
example.com/sign-in
, entering their credentials (e.g. username/password), and submitting the form, usually by clicking a "submit" button. This makes a request to the server with the credentials. -
The server validates the credentials against a database and, upon successful validation:
- Creates a session record in the database (stateful component).
- Generates a signed JWT with its expiration set to the session lifetime (stateless component) - this JWT is stored on FAPI, and is not accessible to the application. Clerk calls this the client token.
- Generates a second signed JWT that expires after 60 seconds which is returned directly to the application and contains user data like the user ID and other claims. Clerk calls this the session token.
-
The server responds to the browser's request by sending the client token in a
Set-Cookie
header in the response, which is set on the FAPI domain. Clerk's client-side SDK then makes a request to FAPI to get a session token and sets it on your app's domain. -
And just like stateless auth, the next time the browser sends a request to the server, it sends the cookie containing the token. The server verifies the signature of the token to ensure that it's valid, and then uses the user ID within the token to fetch any required user data and process the request.
So far, this is the same as stateless auth, with one key distinction: the session token's expiration time. This is because normally, in stateless authentication implementations, the token's expiration is set to match the intended session duration - commonly ranging from one week to one month. But since JWTs can't be revoked, if a token is compromised, the attacker has the entirety of the session lifetime to be able to take over the user's account. This will be several days at least on average, if not several weeks.
Clerk's model mitigates this issue by setting an extremely short session token lifetime of 60 seconds. Normally, a Clerk token will have already expired before an attacker gets the chance to even try to use it. This is great for security, but it does create a complication in the authentication flow, as signing users out every 60 seconds would not be an acceptable user experience. So, for this architecture to work, it must decouple token expiration from session lifetime. To make this happen, Clerk introduces a new "token refresh" mechanism that runs in the background and is responsible for refreshing the token every minute.
Token refresh mechanism
To maintain session continuity despite the 60-second token lifetime, Clerk's frontend SDKs implement an automatic refresh mechanism that:
- Runs on a 50-second interval (allowing 10 seconds for network latency).
- Makes requests to the
/client/sessions/<id>/tokens
endpoint. - Updates the session token before expiration.
If you open the network tab in your browser's developer tools on a Clerk application, you will see this request being sent, and a session token being returned in the response.
This approach provides several security benefits:
- Minimizes the window of opportunity for token misuse
- Maintains the ability to revoke sessions quickly
- Preserves the performance benefits of stateless authentication
If the token lifetime is 60 seconds, how does Clerk know how long your entire session lifetime is?
Clerk's authentication model relies on two distinct tokens - a "client token" and a "session token". Let's break down each of these tokens and how they are configured as cookies.
Client token
- Cookie name:
__client
- Contents: A Clerk-signed JWT containing:
session_id
: Unique session identifierrotating_token
: Anti-session-fixation token that rotates on each sign-in across any device
- Domain: Set on your FAPI domain (
clerk.example.com
or<slug>.accounts.dev
), rather than on your app domain. This is a security measure - for example if your app logs are leaked, they wouldn't contain client token values, since it's scoped to a different domain. - Expiration: Set to your session lifetime, which is 7 days by default. Can be configured in the Clerk Dashboard.
- HttpOnly: Yes
- SameSite: Lax
The client token serves as the source of truth for authentication state. When a user signs in, Clerk either creates a new client token or rotates the existing token's rotating_token
value. A valid client token enables Clerk to generate short-lived session tokens for the application.
Session token
- Cookie name:
__session
- Contents: A Clerk-signed JWT containing a set of default claims. Can be customized in the Clerk Dashboard to include additional claims.
- Domain: Set on your application's domain directly, scoped strictly so it cannot be shared across subdomains. This is done to prevent a different app on a different subdomain from being able to take over your users' accounts. If you need to send the session token value across subdomain boundaries, such as from
example.com
toapi.example.com
, you can put the token in arequest
header instead. - Expiration: 60 seconds
- HttpOnly: No - must be able to be accessed by client-side SDKs
- SameSite: Lax
When your app makes a request from the frontend to your backend, if the backend is on the same origin, the __session
cookie will automatically be sent along with the request. Your backend can then cryptographically verify the session token's signature and extract the user ID and other claims.
The Handshake
The short-lived nature of session tokens introduces a case that requires special handling. Consider this scenario: A user signs in to your application and then closes their browser tab. When they return after five minutes by opening a new tab, their session token will have expired since the refresh mechanism could not run while the tab was closed. At this point, Clerk needs to determine the user's authentication status and potentially issue a new session token.
For client-rendered applications, this process is straightforward. Clerk's frontend SDK makes a direct request to FAPI, which validates the client token. If the token is valid, FAPI issues and returns a new session token. If invalid, the user is redirected to the sign-in flow.
However, server-rendered applications present a unique challenge. Server-to-server requests cannot include browser cookies, as cookies are stored by the browser. This means that, if your app's backend made a request directly to FAPI, the client token would not be available with that request, as the request would not flow through the browser. To solve this problem, Clerk implements a "handshake" flow:
- The server returns a redirect response to the browser
- The browser follows the redirect to FAPI
- FAPI receives the request with the client cookie
- FAPI validates the authentication state and issues a new session token
This server -> browser -> FAPI request includes the client token, so FAPI is able to verify the user's auth state and issue a new session token securely. This handshake ensures secure token renewal while maintaining the benefits of server-side rendering.
Feedback
Last updated on