āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā š shadcn/directory/clerk/clerk-docs/guides/dashboard/dns-domains/satellite-domains ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
[!WARNING] This guide addresses authentication across different domains with shared sessions. For example,
example-site.comandexample-site-admin.com.It is not recommended to use passkeys as a form of authentication. Learn more about domain restrictions for passkeys.
Authentication across subdomains with shared sessions works by default with Clerk.
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 useSignIn() hook for sign-in and <SignUp /> component or useSignUp() hook for sign-up.
To access authentication state from a satellite domain, users will be transparently redirected to the primary domain. If users need to sign in, they must be redirected to a sign in flow hosted on the primary domain, then redirected back to the originating satellite domain. The same redirection process applies to sign-up flows.
[!WARNING] Currently, multi-domain can be added to any Next.js or Remix 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:
primary.devlocalhost: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.
To add a satellite domain:
For the purposes of this guide:
satellite.dev.localhost:3001.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.
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.
There are two ways that you can configure your Clerk satellite application to work with the primary domain:
Use the following tabs to select your preferred method. Clerk recommends using environment variables.
<Tabs items={["Environment variables", "Properties"]}> <Tab> You can configure your satellite application by setting the following environment variables:
> [!NOTE]
> In development, your Publishable and Secret Keys will start with `pk_test_` and `sk_test` respectively. In production, they will start with `pk_live_` and `sk_live` respectively.
- In the `.env` file associated with your primary domain:
<CodeBlockTabs options={["Next.js", "Remix"]}>
```env {{ filename: '.env' }}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY={{pub_key}}
CLERK_SECRET_KEY={{secret}}
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
```
```env {{ filename: '.env' }}
CLERK_PUBLISHABLE_KEY={{pub_key}}
CLERK_SECRET_KEY={{secret}}
CLERK_SIGN_IN_URL=/sign-in
```
</CodeBlockTabs>
- In the `.env` file associated with your other (satellite) domain:
<CodeBlockTabs options={["Next.js", "Remix"]}>
```env {{ filename: '.env' }}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY={{pub_key}}
CLERK_SECRET_KEY={{secret}}
NEXT_PUBLIC_CLERK_IS_SATELLITE=true
# Production example:
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
# Development example:
# NEXT_PUBLIC_CLERK_DOMAIN=http://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 {{ filename: '.env' }}
CLERK_PUBLISHABLE_KEY={{pub_key}}
CLERK_SECRET_KEY={{secret}}
CLERK_IS_SATELLITE=true
# Production example:
CLERK_DOMAIN=satellite.dev
CLERK_SIGN_IN_URL=https://primary.dev/sign-in
CLERK_SIGN_UP_URL=https://primary.dev/sign-up
# Development example:
# CLERK_DOMAIN=http://localhost:3001
# CLERK_SIGN_IN_URL=http://localhost:3000/sign-in
# CLERK_SIGN_UP_URL=http://localhost:3000/sign-up
```
</CodeBlockTabs>
- You will also need to add the `allowedRedirectOrigins` property to `<ClerkProvider>` on your _primary domain app_ to ensure that the redirect back from primary to satellite domain works correctly. For example:
<CodeBlockTabs options={["Development", "Production"]}>
```tsx {{ filename: '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>
)
}
```
```tsx {{ filename: '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>
)
}
```
</CodeBlockTabs>
</Tab>
<Tab>
You can configure your satellite application by setting the following properties:
- `isSatellite` - Defines the app as a satellite app when `true`.
- `domain` - Sets the domain of the satellite application. This is required since we cannot figure this out by your Publishable Key, since it is the same for all of your multi-domain apps.
- `signInUrl` - This url will be used when signing in on your satellite application and needs to point to your primary application. This option is optional for production instances and required for development instances.
- `signUpUrl` - This url will be used for signing up on your satellite application and needs to point to your primary application. This option is optional for production instances and required for development instances.
- `allowedRedirectOrigins` - This is a list of origins that are allowed to redirect back to from the primary domain.
> [!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.
<Tabs items={["Next.js", "Remix"]}>
<Tab>
In a Next.js application, you must set the properties in the [`<ClerkProvider>`](/docs/reference/components/clerk-provider) component _and_ in your [`clerkMiddleware()`](/docs/reference/nextjs/clerk-middleware).
- In the Next project associated with your primary domain, only the `signInUrl` prop needs to be configured as shown in the following example:
> [!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.
<CodeBlockTabs options={["App Router", "Pages Router"]}>
```tsx {{ filename: '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>Satellite Next.js app</p>
{children}
</ClerkProvider>
</body>
</html>
)
}
```
```jsx {{ filename: '_app.tsx' }}
import { ClerkProvider } from '@clerk/nextjs'
import Head from 'next/head'
export default function App({ Component, pageProps }) {
const primarySignInUrl = '/sign-in'
const primarySignUpUrl = '/sign-up'
const satelliteUrl = 'https://satellite.dev'
return (
<ClerkProvider
signInUrl={primarySignInUrl}
signUpUrl={primarySignUpUrl}
allowedRedirectOrigins={[satelliteUrl]}
{...pageProps}
>
<Head>
<title>Satellite Next.js app</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
<Component {...pageProps} />
</ClerkProvider>
)
}
```
</CodeBlockTabs>
- In the Next project associated with your satellite domain, configure your `<ClerkProvider>` as shown in the following example:
<CodeBlockTabs options={["App Router", "Pages Router"]}>
```tsx {{ filename: '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'
// Or, in development:
// const primarySignInUrl = 'http:localhost:3000/sign-in';
// const primarySignUpUrl = 'http:localhost:3000/sign-up';
return (
<html lang="en">
<body>
<ClerkProvider
isSatellite
domain={(url) => url.host}
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>
)
}
```
```jsx {{ filename: '_app.tsx' }}
import { ClerkProvider } from '@clerk/nextjs'
import Head from 'next/head'
export default function App({ Component, pageProps }) {
const primarySignInUrl = 'https://primary.dev/sign-in'
const primarySignUpUrl = 'https://primary.dev/sign-up'
// Or, in development:
// const primarySignInUrl = 'http:localhost:3000/sign-in';
// const primarySignUpUrl = 'http:localhost:3000/sign-up';
return (
<ClerkProvider
isSatellite
domain={(url) => url.host}
signInUrl={primarySignInUrl}
signUpUrl={primarySignUpUrl}
{...pageProps}
>
<Head>
<title>Satellite Next.js app</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
<Component {...pageProps} />
</ClerkProvider>
)
}
```
</CodeBlockTabs>
And the middleware associated with your satellite domain should look like this:
<Include src="_partials/nextjs/nextjs-15-callout" />
```ts {{ filename: 'proxy.ts' }}
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
// Set the homepage as a public route
const isPublicRoute = createRouteMatcher(['/'])
// Set the necessary options for a satellite application
const options = {
isSatellite: true,
signInUrl: 'https://primary.dev/sign-in',
signUpUrl: 'https://primary.dev/sign-up',
// Or, in development:
// signInUrl: 'http://localhost:3000/sign-in',
// signUpUrl: 'http://localhost:3000/sign-up',
domain: 'https://satellite.dev',
// Or, in development:
// domain: 'http://localhost:3001',
}
export default clerkMiddleware(async (auth, req) => {
if (isPublicRoute(req)) return // if it's a public route, do nothing
await auth.protect() // for any other route, require auth
}, 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)(.*)',
],
}
```
</Tab>
<Tab>
In a Remix application, you must set the properties in the [`ClerkApp`](/docs/reference/remix/clerk-app) wrapper.
- In the root file associated with your primary domain, you only need to configure the `signInUrl` prop:
```ts {{ filename: 'root.tsx' }}
export const loader = (args) => {
return rootAuthLoader(
args,
({ req }) => {
const { userId, sessionId, getToken } = req.auth
return json({
message: `Hello from the root loader :)`,
ENV: getBrowserEnvironment(),
})
},
{
loadUser: true,
signInUrl: '/sign-in',
signUpUrl: '/sign-up',
allowedRedirectOrigins: ['https://satellite.dev'],
} as const,
)
}
export default ClerkApp(App, {
signInUrl: '/sign-in',
signUpUrl: '/sign-up',
})
```
- In the root file associated with your satellite domain, configure `ClerkApp` as shown in the following example:
```ts {{ filename: 'root.tsx' }}
export const loader = (args) => {
return rootAuthLoader(
args,
({ req }) => {
const { userId, sessionId, getToken } = req.auth
return json({
message: `Hello from the root loader :)`,
ENV: getBrowserEnvironment(),
})
},
{
loadUser: true,
signInUrl: 'https://primary.dev/sign-in',
signUpUrl: 'https://primary.dev/sign-up',
// Or, in development:
// signInUrl: 'http:localhost:3000/sign-in',
// signUpUrl: 'http:localhost:3000/sign-up',
isSatellite: true,
domain: (url) => url.host,
} as const,
)
}
export default ClerkApp(App, {
isSatellite: true,
domain: (url) => url.host,
signInUrl: 'https://primary.dev/sign-in',
signUpUrl: 'https://primary.dev/sign-up',
// Or, in development:
// signInUrl: 'http:localhost:3000/sign-in',
// signUpUrl: 'http:localhost:3000/sign-up',
})
```
</Tab>
</Tabs>
</Tab>
</Tabs>
Your satellite application should now be able to access the authentication state from your satellite domain!
You can see it in action by:
<UserProfile /> component and update your information.You can repeat this process and create as many satellite applications as you need. </Steps>
If you have any questions about satellite domains, or you're having any trouble setting this up, contact support@clerk.com
ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā