Skip to main content

Authentication across different domains

Important

This feature or workflow is considered advanced - it may not be part of an official Clerk SDK or fall within typical usage patterns. The Clerk support team will do their best to assist you, but cannot guarantee a resolution for this type of advanced usage.

Warning

This guide addresses authentication across different domains with shared sessions. For example, example-site.com and example-site-admin.com. This is not to be confused with authentication across subdomains with shared sessions, which works by default with Clerk.

It is not recommended to use passkeys as a form of authentication when sharing sessions across different domains. Learn more about domain restrictions for passkeys.

Clerk supports sharing sessions across different domains by adding one or many "satellite" domains to an application.

Your "primary" domain is where the authentication state lives, and satellite domains are able to securely read that state from the primary domain, enabling a seamless authentication flow across domains.

Users must complete both the sign-in and sign-up flows on the primary domain by using the <SignIn /> component or the useSignIn() hook for sign-in and the <SignUp /> component or the useSignUp() hook for sign-up.

Users must complete both the sign-in and sign-up flows on the primary domain by using the <SignIn /> component for sign-in and the <SignUp /> component for sign-up.

How authentication syncing works

The syncing behavior between satellite and primary domains is controlled by the satelliteAutoSync option.

With satelliteAutoSync: false (the default), satellite domains do not automatically sync authentication state with the primary domain when a user visits the page. This means there is no upfront performance cost for users visiting a satellite domain. Authentication state is only synced when a user initiates a sign-in or sign-up action:

  1. When a user selects "Sign in" on the satellite domain, they are redirected to the primary domain.
    • If the user is already signed in on the primary domain, they are immediately redirected back to the satellite domain with the existing auth state — no additional user action is required.
    • If the user is not signed in on the primary domain, they complete the sign-in (or sign-up) flow there and are then redirected back to the satellite domain.
  2. After the initial sync, signing out from either domain signs out from all domains.

See Build sign-in and sign-up links for setup.

If you set satelliteAutoSync: true, the satellite domain will automatically redirect to the primary domain on first load to sync authentication state, even if the user has no session. This matches the original Core 2 behavior and is useful if you want users who are already signed in on the primary domain to be automatically recognized on the satellite without needing to select "Sign in". However, this comes with a performance cost since every first visit triggers a redirect.

When satelliteAutoSync is false (the default), you must ensure your sign-in and sign-up links on the satellite domain use Clerk.buildSignInUrl() and Clerk.buildSignUpUrl() instead of hardcoded URLs. These methods automatically append the __clerk_synced=false sync trigger parameter to the redirect URL. This parameter tells the satellite app to sync the session when the user returns from the primary domain.

Important

If you hardcode sign-in URLs (e.g., <a href="https://primary.dev/sign-in">) instead of using buildSignInUrl(), the sync trigger parameter will not be added. As a result, when the user returns to the satellite after signing in, the session will not be recognized.

// Example sign-in button in the satellite app
function SignInButton() {
  const { buildSignInUrl } = useClerk()

  return <a href={buildSignInUrl()}>Sign in</a>
}
// Example sign-in button in the satellite app
const signInBtn = document.getElementById('sign-in')
signInBtn.addEventListener('click', () => {
  window.location.href = Clerk.buildSignInUrl()
})

Warning

This feature requires a paid plan for production use, but all features are free to use in development mode so that you can try out what works for you. See the pricing page for more information.

Warning

Currently, multi-domain can be added to any Next.js, TanStack Start, or Nuxt application. For other React frameworks, multi-domain is still supported as long as you do not use server rendering or hydration.

To get started, you need to create an application from the Clerk Dashboard. Once you create an instance via the Clerk Dashboard, you will be prompted to choose a domain. This is your primary domain. For the purposes of this guide:

  • In production, the primary domain will be primary.dev.
  • In development, the primary domain will be localhost:3000.

When building your sign-in flow, you must configure it to run within your primary application, e.g. on /sign-in.

Note

For more information about creating your application, see the setup guide.

Add your first satellite domain

To add a satellite domain:

  1. In the Clerk Dashboard, navigate to the Domains page.
  2. Select the Satellites tab.
  3. Select the Add satellite domain button and follow the instructions provided.

For the purposes of this guide:

  • In production, the satellite domain will be satellite.dev.
  • In development, the satellite domain will be localhost:3001.

Complete DNS setup for your satellite domain

To use a satellite domain in production, you will need to add a CNAME record for the clerk subdomain. For development instances, you can skip this step.

  1. In the Clerk Dashboard, navigate to the Domains page.
  2. Select the Satellites tab.
  3. Select the satellite domain you just added.
  4. Under DNS Configuration, follow the instructions to add a CNAME record in your DNS provider's settings.

Once your CNAME record is set up correctly, you should see a Verified label next to your satellite domain.

Note

It can take up to 48hrs for DNS records to fully propagate.

Tip

If you're unable to add a CNAME record for the Frontend API on the satellite domain, you can use a proxy instead. See Proxying the Clerk Frontend API for more information.

Configure your primary and satellite apps

There are two ways to configure your primary and satellite apps to work together:

  • Using environment variables
  • Using properties

Use the following tabs to select your preferred method. Clerk recommends using environment variables.

To configure satellite domains with environment variables, you need to set values in both your primary app and your satellite app.

Warning

This example is written for Next.js App Router but can be adapted for any framework/language. Next.js prefixes environment variables with NEXT_PUBLIC_ if they need to be exposed to the frontend. You must update this prefix for the framework or environment variable loader that you are using.

Add the following environment variables to your primary domain application. Your and can always be retrieved from the API keys page in the Clerk Dashboard.

.env
# In development, your API keys will start with `pk_test_` and `sk_test_`.
# In production, your API keys will start with `pk_live_` and `sk_live_`.
# Ensure you load the correct keys.
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=YOUR_PUBLISHABLE_KEY
CLERK_SECRET_KEY=YOUR_SECRET_KEY
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up

You will also need to add the allowedRedirectOrigins property wherever you initialize the Clerk integration to ensure that the redirect back from primary to satellite domain works correctly. For most SDKs, that means passing it to <ClerkProvider>. In other SDKs, it may be configured through the framework's Clerk integration or plugin. Refer to your framework's quickstart guide to find where to set it. For example:

app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ClerkProvider allowedRedirectOrigins={['http://localhost:3001']}>{children}</ClerkProvider>
      </body>
    </html>
  )
}
app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ClerkProvider allowedRedirectOrigins={['https://satellite.dev']}>{children}</ClerkProvider>
      </body>
    </html>
  )
}

Add the following environment variables to your satellite domain application. Your and can always be retrieved from the API keys page in the Clerk Dashboard.

.env
# In development, your API keys will start with `pk_test_` and `sk_test_` respectively.
# Ensure you load the correct keys.
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=YOUR_PUBLISHABLE_KEY
CLERK_SECRET_KEY=YOUR_SECRET_KEY
NEXT_PUBLIC_CLERK_IS_SATELLITE=true
NEXT_PUBLIC_CLERK_DOMAIN=localhost:3001
NEXT_PUBLIC_CLERK_SIGN_IN_URL=http://localhost:3000/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=http://localhost:3000/sign-up
.env
# In production, your API keys will start with `pk_live_` and `sk_live_` respectively.
# Ensure you load the correct keys.
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxxxx
CLERK_SECRET_KEY=sk_live_xxxxx
NEXT_PUBLIC_CLERK_IS_SATELLITE=true
NEXT_PUBLIC_CLERK_DOMAIN=satellite.dev
NEXT_PUBLIC_CLERK_SIGN_IN_URL=https://primary.dev/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=https://primary.dev/sign-up

To configure satellite domains with properties, use the following properties across your primary and satellite apps:

PropertyDescription
isSatelliteDefines the app as a satellite app when true.
domainSets the domain of the satellite application. This is required because the is shared across your multi-domain apps, so Clerk cannot infer the satellite domain from it.
signInUrlThe URL used when signing in on your satellite application. It needs to point to your primary application. This option is optional for production instances and required for development instances.
signUpUrlThe URL used when signing up on your satellite application. It needs to point to your primary application. This option is optional for production instances and required for development instances.
allowedRedirectOriginsA list of origins that the primary domain is allowed to redirect back to.

Tip

The URL parameter that can be passed to isSatellite and domain is the request URL for server-side usage or the current location for client usage.

These properties are available wherever you initialize the Clerk integration. For most SDKs, that means passing them to the <ClerkProvider> component. In other SDKs, they may be configured through the framework's Clerk integration or plugin. Refer to your framework's quickstart guide to find where to set them.

In a Next.js setup, configure <ClerkProvider> in both your primary and satellite apps, then update clerkMiddleware()Next.js Icon in the satellite app. If your framework does not provide equivalent middleware or request handling for satellite domains, use the environment variable setup instead.

Important

You should set your CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY in your environment variables even if you're using props to configure satellite domains.

In the Next.js project associated with your primary domain, configure your <ClerkProvider> as shown in the following example:

app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const primarySignInUrl = '/sign-in'
  const primarySignUpUrl = '/sign-up'
  const satelliteUrl = 'http://localhost:3001'

  return (
    <html lang="en">
      <body>
        <ClerkProvider
          signInUrl={primarySignInUrl}
          signUpUrl={primarySignUpUrl}
          allowedRedirectOrigins={[satelliteUrl]}
        >
          <p>Primary Next.js app</p>
          {children}
        </ClerkProvider>
      </body>
    </html>
  )
}
app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const primarySignInUrl = '/sign-in'
  const primarySignUpUrl = '/sign-up'
  const satelliteUrl = 'https://satellite.dev'

  return (
    <html lang="en">
      <body>
        <ClerkProvider
          signInUrl={primarySignInUrl}
          signUpUrl={primarySignUpUrl}
          allowedRedirectOrigins={[satelliteUrl]}
        >
          <p>Primary Next.js app</p>
          {children}
        </ClerkProvider>
      </body>
    </html>
  )
}

In the Next.js project associated with your satellite domain, configure your <ClerkProvider> as shown in the following example:

app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const primarySignInUrl = 'http://localhost:3000/sign-in'
  const primarySignUpUrl = 'http://localhost:3000/sign-up'
  const satelliteDomain = 'localhost:3001'

  return (
    <html lang="en">
      <body>
        <ClerkProvider
          isSatellite
          domain={satelliteDomain}
          signInUrl={primarySignInUrl}
          signUpUrl={primarySignUpUrl}
        >
          <title>Satellite Next.js app</title>
          <meta name="viewport" content="width=device-width, initial-scale=1.0" />
          {children}
        </ClerkProvider>
      </body>
    </html>
  )
}
app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const primarySignInUrl = 'https://primary.dev/sign-in'
  const primarySignUpUrl = 'https://primary.dev/sign-up'
  const satelliteDomain = 'satellite.dev'

  return (
    <html lang="en">
      <body>
        <ClerkProvider
          isSatellite
          domain={satelliteDomain}
          signInUrl={primarySignInUrl}
          signUpUrl={primarySignUpUrl}
        >
          <title>Satellite Next.js app</title>
          <meta name="viewport" content="width=device-width, initial-scale=1.0" />
          {children}
        </ClerkProvider>
      </body>
    </html>
  )
}

And the middleware associated with your satellite domain should look like this:

Important

If you're using Next.js ≤15, name your file middleware.ts instead of proxy.ts. The code itself remains the same; only the filename changes.

proxy.ts
import { clerkMiddleware } from '@clerk/nextjs/server'

// Set the necessary options for a satellite application
const options = {
  isSatellite: true,
  signInUrl: 'http://localhost:3000/sign-in',
  signUpUrl: 'http://localhost:3000/sign-up',
  domain: 'localhost:3001',

  // satelliteAutoSync defaults to false. Uncomment below to automatically sync auth state on first load.
  // This adds a redirect on every first visit, which comes with a performance cost.
  // satelliteAutoSync: true,
}

export default clerkMiddleware(options)

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Always run for API routes
    '/(api|trpc)(.*)',
    // Always run for Clerk-specific frontend API routes
    '/__clerk/(.*)',
  ],
}
proxy.ts
import { clerkMiddleware } from '@clerk/nextjs/server'

// Set the necessary options for a satellite application
const options = {
  isSatellite: true,
  signInUrl: 'https://primary.dev/sign-in',
  signUpUrl: 'https://primary.dev/sign-up',
  domain: 'satellite.dev',

  // satelliteAutoSync defaults to false. Uncomment below to automatically sync auth state on first load.
  // This adds a redirect on every first visit, which comes with a performance cost.
  // satelliteAutoSync: true,
}

export default clerkMiddleware(options)

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Always run for API routes
    '/(api|trpc)(.*)',
    // Always run for Clerk-specific frontend API routes
    '/__clerk/(.*)',
  ],
}

Use the following tabs to choose whether your JavaScript app initializes Clerk with the npm package or a <script> tag.

For apps that initialize Clerk with @clerk/clerk-js, configure constructor options in new Clerk() and runtime options in clerk.load(). The following examples build on the setup from the JavaScript QuickstartJavaScript Icon — keep the @clerk/ui bundle loader from the quickstart in place, as it sets the window.__internal_ClerkUICtor value passed to clerk.load().

In the JavaScript app associated with your primary domain, configure Clerk as shown in the following example:

src/main.js
import { Clerk } from '@clerk/clerk-js'

// In development, your API keys will start with `pk_test_` and `sk_test_` respectively. Ensure you load the correct keys.
const clerk = new Clerk('YOUR_PUBLISHABLE_KEY')

await clerk.load({
  ui: { ClerkUI: window.__internal_ClerkUICtor },
  signInUrl: '/sign-in',
  signUpUrl: '/sign-up',
  allowedRedirectOrigins: ['http://localhost:3001'],
})
src/main.js
import { Clerk } from '@clerk/clerk-js'

// In production, your API keys will start with `pk_live_` and `sk_live_` respectively. Ensure you load the correct keys.
const clerk = new Clerk('pk_live_xxxxx')

await clerk.load({
  ui: { ClerkUI: window.__internal_ClerkUICtor },
  signInUrl: '/sign-in',
  signUpUrl: '/sign-up',
  allowedRedirectOrigins: ['https://satellite.dev'],
})

Your sign-in and sign-up flows should run on the primary app, and the URLs you pass to the satellite app as signInUrl and signUpUrl must point to those primary-app routes.

In the JavaScript app associated with your satellite domain, use the same as the primary app, then configure Clerk as shown in the following example:

src/main.js
import { Clerk } from '@clerk/clerk-js'

// In development, your API keys will start with `pk_test_` and `sk_test_` respectively. Ensure you load the correct keys.
const clerk = new Clerk('YOUR_PUBLISHABLE_KEY', {
  domain: 'localhost:3001',
})

await clerk.load({
  ui: { ClerkUI: window.__internal_ClerkUICtor },
  isSatellite: true,
  signInUrl: 'http://localhost:3000/sign-in',
  signUpUrl: 'http://localhost:3000/sign-up',

  // satelliteAutoSync defaults to false. Uncomment below to automatically sync auth state on first load.
  // satelliteAutoSync: true,
})
src/main.js
import { Clerk } from '@clerk/clerk-js'

// In production, your API keys will start with `pk_live_` and `sk_live_` respectively. Ensure you load the correct keys.
const clerk = new Clerk('pk_live_xxxxx', {
  domain: 'satellite.dev',
})

await clerk.load({
  ui: { ClerkUI: window.__internal_ClerkUICtor },
  isSatellite: true,
  signInUrl: 'https://primary.dev/sign-in',
  signUpUrl: 'https://primary.dev/sign-up',

  // satelliteAutoSync defaults to false. Uncomment below to automatically sync auth state on first load.
  // satelliteAutoSync: true,
})

Add sign-in and sign-up links to your satellite app using clerk.buildSignInUrl() and clerk.buildSignUpUrl() instead of hardcoding primary-domain URLs. These helpers add the sync trigger parameter required for the default satelliteAutoSync: false flow. See Build sign-in and sign-up links for more information.

src/main.js
document.getElementById('app').innerHTML = `
  <a href="${clerk.buildSignInUrl()}">Sign in</a>
  <a href="${clerk.buildSignUpUrl()}">Sign up</a>
`

Note

If you enable satelliteAutoSync: true, hardcoded primary sign-in or sign-up URLs can still work because the satellite app syncs automatically on first load.

For apps that initialize Clerk via a <script> tag, configure constructor options on the ClerkJS script and runtime options in Clerk's load() method.

Important

Clerk options passed as script tag attributes must be prefixed with data-clerk-. The script tag supports three attributes: data-clerk-publishable-key (publishableKey), data-clerk-proxy-url (proxyUrl), and data-clerk-domain (domain). All other options must be passed to Clerk's load() method.

In the JavaScript app associated with your primary domain, keep your existing Clerk <script> tags from the quickstart and update Clerk.load() as shown in the following example:

index.html
<script>
  window.addEventListener('load', async function () {
    await Clerk.load({
      ui: { ClerkUI: window.__internal_ClerkUICtor },
      signInUrl: '/sign-in',
      signUpUrl: '/sign-up',
      allowedRedirectOrigins: ['http://localhost:3001'],
    })
  })
</script>
index.html
<script>
  window.addEventListener('load', async function () {
    await Clerk.load({
      ui: { ClerkUI: window.__internal_ClerkUICtor },
      signInUrl: '/sign-in',
      signUpUrl: '/sign-up',
      allowedRedirectOrigins: ['https://satellite.dev'],
    })
  })
</script>

Your sign-in and sign-up flows should run on the primary app, and the URLs you pass to the satellite app as signInUrl and signUpUrl must point to those primary-app routes.

In the JavaScript app associated with your satellite domain, add data-clerk-domain to the ClerkJS script tag and set isSatellite in Clerk's load() method as shown in the following example:

index.html
<script
  defer
  crossorigin="anonymous"
  src="https://YOUR_FRONTEND_API_URL/npm/@clerk/ui@1/dist/ui.browser.js"
  type="text/javascript"
></script>

<script
  defer
  crossorigin="anonymous"
  data-clerk-publishable-key="YOUR_PUBLISHABLE_KEY"
  data-clerk-domain="localhost:3001"
  src="https://YOUR_FRONTEND_API_URL/npm/@clerk/clerk-js@6/dist/clerk.browser.js"
  type="text/javascript"
></script>

<script>
  window.addEventListener('load', async function () {
    await Clerk.load({
      ui: { ClerkUI: window.__internal_ClerkUICtor },
      isSatellite: true,
      signInUrl: 'http://localhost:3000/sign-in',
      signUpUrl: 'http://localhost:3000/sign-up',

      // satelliteAutoSync defaults to false. Uncomment below to automatically sync auth state on first load.
      // satelliteAutoSync: true,
    })
  })
</script>
index.html
<script
  defer
  crossorigin="anonymous"
  src="https://YOUR_FRONTEND_API_URL/npm/@clerk/ui@1/dist/ui.browser.js"
  type="text/javascript"
></script>

<script
  defer
  crossorigin="anonymous"
  data-clerk-publishable-key="YOUR_PUBLISHABLE_KEY"
  data-clerk-domain="satellite.dev"
  src="https://YOUR_FRONTEND_API_URL/npm/@clerk/clerk-js@6/dist/clerk.browser.js"
  type="text/javascript"
></script>

<script>
  window.addEventListener('load', async function () {
    await Clerk.load({
      ui: { ClerkUI: window.__internal_ClerkUICtor },
      isSatellite: true,
      signInUrl: 'https://primary.dev/sign-in',
      signUpUrl: 'https://primary.dev/sign-up',

      // satelliteAutoSync defaults to false. Uncomment below to automatically sync auth state on first load.
      // satelliteAutoSync: true,
    })
  })
</script>

Add sign-in and sign-up links to your satellite app using Clerk.buildSignInUrl() and Clerk.buildSignUpUrl() instead of hardcoding primary-domain URLs. These helpers add the sync trigger parameter required for the default satelliteAutoSync: false flow. See Build sign-in and sign-up links for more information.

index.html
<script>
  window.addEventListener('load', async function () {
    document.getElementById('app').innerHTML = `
      <a href="${Clerk.buildSignInUrl()}">Sign in</a>
      <a href="${Clerk.buildSignUpUrl()}">Sign up</a>
    `
  })
</script>

Note

If you enable satelliteAutoSync: true, hardcoded primary sign-in or sign-up URLs can still work because the satellite app syncs automatically on first load.

Ready to go

Your satellite application should now be able to access the authentication state from your satellite domain!

To verify it's working, visit your satellite domain and select "Sign in" — you should be redirected to the primary domain and back with an active session. For details on the sync flow, see the How authentication syncing works section.

You can repeat this process and create as many satellite applications as you need.

Feedback

What did you think of this content?

Last updated on