ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β π shadcn/directory/clerk/clerk-docs/guides/how-clerk-works/overview β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β
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.
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:
const publishableKey = 'pk_test_ZXhhbXBsZS5hY2NvdW50cy5kZXYk'
const keyWithoutPrefix = publishableKey.replace('pk_test_', '')
atob(keyWithoutPrefix) // => example.accounts.dev$
[!NOTE] In previous versions of Clerk, the Frontend API URL was exposed directly rather than being encoded within a Publishable Key. This was a source of confusion for users, so we transitioned to encoding it as base64 and making it a key.
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{{ target: '_blank' }}.
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{{ target: '_blank' }}, 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{{ target: '_blank' }}, 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.
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 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.
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.
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 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{{ target: '_blank' }}.
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 sidenav 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.
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.
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.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.
<Video
src="/docs/images/how-clerk-works/stateful-auth.mp4"
width="1400"
height="700"
autoPlay
muted
loop
playsInline
controls
/>[!NOTE] What happens if an attacker gets their hands on your session ID? Generally, the answer here is that you're in trouble. If an attacker gets your session ID, they can sign in as you. Therefore, it's best practice to use HTTPS (ensures attacker can't sniff it) and ensure the cookie is set as
HttpOnly(ensures attacker can't get it via remote JavaScript execution).
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.
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.
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.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.
<Video
src="/docs/images/how-clerk-works/stateless-auth.mp4"
width="1400"
height="700"
autoPlay
muted
loop
playsInline
controls
/>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.
[!QUIZ] How exactly does stateless authentication mitigate database scaling challenges?
When you are first building an application, a single database instance is often sufficient to handle your traffic. As your application grows, you'll need to scale your database to manage increased load. This typically involves creating multiple database copies or splitting the database into multiple instances through a process called sharding.
Sharding involves dividing a database into smaller, more manageable databases (called shards), each handling a subset of the total data. To manage this complexity, a load balancer often serves as an entry point that directs traffic to ensure no single database instance becomes overwhelmed.
Keeping multiple database instances synchronized is a challenging problem that software engineers have grappled with for decades. The potential consequences of unsynchronized instances can be significant. For example, if a user signs in on one database instance and a subsequent request for that user's data is routed to an unsynchronized instance, the user might encounter a confusing authentication error.
Stateless authentication offers an elegant solution. By using signed tokens that contain all necessary authentication information, the server can verify a user's credentials without direct database interaction, effectively bypassing the synchronization complexities that arise in traditional, stateful authentication methods.
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:
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 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.
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:
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.
[!QUIZ] Why doesn't Clerk set a session token using the
Set-Cookieheader when its setting the client token?
This is a great test of your mastery of how cookies work!
Remember, the domain of a cookie can only be set as the domain of the server that set the cookie. In this case, the server returning the request to your app is FAPI. For the
__clientcookie, this is ok, since the__clientcookie needs to be set on FAPI. However, the__sessioncookie needs to be set on your app's domain, not on FAPI. So, FAPI returns the JWT value of the__sessioncookie in its response, and when the Clerk client-side SDK integrated in your app receives the response, it gets the JWT value and uses JavaScript to set the__sessioncookie on your app directly, since the JavaScript is running on your app's domain.
<Video src="/docs/images/how-clerk-works/hybrid-auth.mp4" width="1400" height="700" autoPlay muted loop playsInline controls />
And just like stateless auth, the next time the browser sends a request to the server, it sends the cookie containing the session 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. <Video src="/docs/images/how-clerk-works/hybrid-auth-2.mp4" width="1400" height="700" autoPlay muted loop playsInline controls />
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.
To maintain session continuity despite the 60-second token lifetime, Clerk's frontend SDKs implement an automatic refresh mechanism that:
/client/sessions/<id>/tokens endpoint{{ target: '_blank' }}.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:
[!TIP] To understand Clerk's architecture, it's important to have a solid foundational understanding of how browser cookies work at a detailed level. If you need a refresher on cookie fundamentals, including domain scoping,
SameSitepolicies, andHttpOnlyflags, see the guide on cookies.
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.
The client token is a long-lived token that is stored in the __client cookie, which is set on your FAPI domain.
__clientsession_id: Unique session identifierrotating_token: Anti-session-fixation token that rotates on each sign-in across any deviceclerk.example.com), rather than on your app domain (example.com). It's set on the FAPI domain as 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.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.
The client token is only used in production environments.
<Include src="_partials/clerkdbjwt-vs-client-cookie" />For this reason:
__clerk_db_jwt in your application code, as it will break in production.For more information on the differences between development and production environments, see the dedicated guide on Clerk environments.
The session token is a short-lived token that is stored in the __session cookie, which is set on your app's domain.
__sessionexample.com to api.example.com, you can put the token in a request header instead.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.
[!QUIZ] Why is the
__sessioncookie notHttpOnly? Is this a security issue?
Setting cookies as
HttpOnlyis generally recommended to prevent client-side JavaScript access, reducing the risk of cross-site scripting (XSS) attacks. However, due to Clerk's architecture, this approach wouldn't work.Remember that FAPI is hosted on a different origin than your app. Let's say, in production, your app is
example.com. Then FAPI isclerk.example.com. Since cookies are only sent to the domain that set them (or its subdomains, depending on theDomainattribute), any cookie set by FAPI would be scoped toclerk.example.comand would not be sent toexample.com.To work around this, Clerk returns the session token from FAPI after a user signs in, and the client-side SDK used by your app (e.g., React SDK) sets the
__sessioncookie containing the session token on your app's domain via JavaScript. A benefit of this is that it allows the token's value and session claims to be accessed on the client-side. This is often quite valuable, as it allows developers to send the session token as a custom header in requests and also makes it possible to use a subdomain (likeapi.example.com) for your backend. However, because it's set client-side, it cannot beHttpOnly, making it more vulnerable to XSS attacks.Clerk mitigates this risk substantially by setting the session token's expiration to a very short duration of 60 seconds. For an XSS attack to succeed, the developer would need to ship a vulnerability on their site, and the attacker would need to exfiltrate users' tokens and use them to take over accounts in an average of less than 30 seconds. This is an extremely difficult scenario and extremely unlikely to be an issue for the most common type of XSS attacks, which are broad sweeps across many sites to harvest tokens, typically after a CVE is disclosed, that can take advantage of those who didn't patch their sites in time. Additionally, Clerk's fast-expiring token provides an extra layer of protection against other attacks that
HttpOnlycookies alone wouldn't mitigate.Summary: Clerk's
__sessioncookie is notHttpOnlybecause it needs to be accessible to the client-side SDKs. However, Clerk mitigates the risk of XSS attacks by setting the session token's expiration to a very short duration of 60 seconds.
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 with the __client cookie. If the client token is valid, FAPI issues and returns a new session token. This is secure by default, because only users who are properly authenticated and signed in will have a valid client token in their browser. If the client token is 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:
__client cookieThis 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.
[!QUIZ] Why does handshake do a redirect? Why can't it make a fetch request to FAPI and get a new token back that way? Not needing to redirect would be a better user experience.
First remember that with Clerk, the session token is the short-lived one that refreshes every 60 seconds, and the client token is the long-lived one whose expiration is your session's lifetime.
Now let's get back to the previous scenario: you open your server-rendered application after having closed the tab for a few minutes, and now you have an expired session token. Normally, to refresh your session token, you make a request to FAPI with the
__clientcookie that contains the long-lived client token. FAPI will validate the client token and check if there's an active session. If there is, it will mint a new session token and send it back.But in this scenario, you have only an invalid (expired) session token, and since your app is rendering on the server, you cannot send a request with the client token since it's stored in a cookie in the browser and server-to-server requests don't flow through the browser. So the question becomes this: how do you get a new session token?
- You could send the expired session token, but then any attacker with an expired session token would be able to mint a new one and hijack your session.
- You could verify that the request only comes from your application's domain, but then any attacker could just send an
httprequest with a spoofed domain value and get a token on your behalf.Neither of these options are secure. Clerk's unique authentication model requires a new mechanism to get a new session token. This is where handshake comes in - here's how it works:
- A request is made to an application that uses Clerk on the server.
- The Clerk SDK used by your app determines the authentication state of the request:
signed-in(valid session token),signed-out(no session token), orhandshake(expired session token).- If the authentication state is
handshake, meaning Clerk knows the session token is expired but can't be sure if the user is signed in or out, the Clerk SDK responds with a 307 redirect to the handshake endpoint:fapi/v1/client/handshake.- The handshake endpoint gets information about the current session and returns a handshake payload. The encoded handshake payload contains a list of
Set-Cookieheader directives to be passed along with the final response.
- If the session is active, a new, valid
__sessioncookie is returned.- If the session is inactive, the
__sessioncookie is wiped and the request will be treated as signed out.- The handshake endpoint redirects back to the host application along with the handshake payload, encoded either in the URL (development) or as a cookie (production).
- The handshake payload is parsed and
Set-Cookieheaders are set on the response by the Clerk SDK.- If an updated
__sessioncookie is returned, the JWT is verified. If verification is successful, the request is treated as signed in.- If an updated
__sessioncookie is not returned, the request is treated as signed out.This makes it such that you can't provide proof of authentication via an API call - the proof comes from the
__clientcookie on FAPI which can't be tampered with by attackers since it's not set on the application domain and isHttpOnly.
{/* Future sections to add
β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ