Clerk Satellite Domain Implementation Guide for customers.dev

Overview

This guide provides detailed implementation steps for setting up customers.dev as a Clerk satellite domain under the do.dev authentication system.

Last Updated: October 15, 2025 Status: Active Implementation Guide Based On: /local-dev/docs/authentication/CLERK_SATELLITE_IMPLEMENTATION.md

Architecture Decision

customers.dev will operate as a Clerk satellite domain, delegating authentication to auth.do.dev (or customers.do.dev). This provides:

  • Centralized authentication across all do.dev properties
  • Single sign-on (SSO) across subdomains
  • Consistent user sessions across different services
  • Shared user database via Convex

Satellite Domain Configuration

1. Environment Configuration

Primary Domain (auth.do.dev or customers.do.dev)

The primary auth domain should have:

# .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_***
CLERK_SECRET_KEY=sk_test_***

# Domain configuration
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_DOMAIN=localhost:3005  # Development
# NEXT_PUBLIC_CLERK_DOMAIN=auth.do.dev   # Production

# Allowed satellite domains
CLERK_SATELLITE_URLS=http://localhost:3015,https://customers.dev

Satellite Domain (customers.dev) - Port 3015

# apps/web/.env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_***
CLERK_SECRET_KEY=sk_test_***

# Satellite configuration
NEXT_PUBLIC_CLERK_IS_SATELLITE=true
NEXT_PUBLIC_CLERK_DOMAIN=localhost:3005  # Development - primary auth domain
# NEXT_PUBLIC_CLERK_DOMAIN=auth.do.dev   # Production - primary auth domain

NEXT_PUBLIC_CLERK_SIGN_IN_URL=http://localhost:3005/sign-in  # Development
# NEXT_PUBLIC_CLERK_SIGN_IN_URL=https://auth.do.dev/sign-in  # Production

NEXT_PUBLIC_CLERK_SIGN_UP_URL=http://localhost:3005/sign-up  # Development
# NEXT_PUBLIC_CLERK_SIGN_UP_URL=https://auth.do.dev/sign-up  # Production

CLERK_SATELLITE_URL=http://localhost:3015  # Development
# CLERK_SATELLITE_URL=https://customers.dev  # Production

# Redirect URLs after auth
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=http://localhost:3015/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=http://localhost:3015/dashboard

# Convex Configuration (already set)
NEXT_PUBLIC_CONVEX_URL=https://impartial-panda-219.convex.cloud
CONVEX_DEPLOY_KEY=dev:impartial-panda-219|***

2. Provider Setup

Update lib/providers/convex-provider.tsx:

"use client"

import { ClerkProvider, useAuth } from "@clerk/nextjs"
import { ConvexProvider, ConvexReactClient } from "convex/react"
import { ConvexProviderWithClerk } from "convex/react-clerk"
import { type ReactNode, useMemo } from "react"

interface ConvexClerkProviderProps {
  children: ReactNode
}

export function ConvexClerkProvider({ children }: ConvexClerkProviderProps) {
  // Create the Convex client only when we have a valid URL
  const convexClient = useMemo(() => {
    const url = process.env.NEXT_PUBLIC_CONVEX_URL
    if (!url) return null
    return new ConvexReactClient(url, {
      logger: false, // Disable console logging in development
    })
  }, [])

  // Check if we have valid Clerk keys
  const clerkPublishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
  const hasValidClerkKey =
    clerkPublishableKey &&
    clerkPublishableKey !== "your_clerk_publishable_key_here" &&
    clerkPublishableKey.startsWith("pk_")

  // Check if this is a satellite domain
  const isSatellite = process.env.NEXT_PUBLIC_CLERK_IS_SATELLITE === "true"
  const clerkDomain = process.env.NEXT_PUBLIC_CLERK_DOMAIN
  const signInUrl = process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL
  const signUpUrl = process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL

  // If no valid Clerk key, render without authentication
  if (!hasValidClerkKey) {
    return <>{convexClient ? <ConvexProvider client={convexClient}>{children}</ConvexProvider> : <>{children}</>}</>
  }

  return (
    <ClerkProvider
      publishableKey={clerkPublishableKey}
      isSatellite={isSatellite}
      domain={clerkDomain}
      signInFallbackRedirectUrl="/dashboard"
      signUpFallbackRedirectUrl="/dashboard"
      signInUrl={signInUrl}
      signUpUrl={signUpUrl}
    >
      {convexClient ? (
        <ConvexProviderWithClerk client={convexClient} useAuth={useAuth}>
          {children}
        </ConvexProviderWithClerk>
      ) : (
        // When Convex is unavailable, render children without the ConvexProvider
        // This allows basic app functionality while the backend is unavailable
        <>{children}</>
      )}
    </ClerkProvider>
  )
}

// For pages that don't need authentication
export function ConvexOnlyProvider({ children }: ConvexClerkProviderProps) {
  // Create the Convex client only when we have a valid URL
  const convexClient = useMemo(() => {
    const url = process.env.NEXT_PUBLIC_CONVEX_URL
    if (!url) return null
    return new ConvexReactClient(url, {
      logger: false, // Disable console logging in development
    })
  }, [])

  // If no Convex URL is available, just render children without Convex provider
  if (!convexClient) {
    return <>{children}</>
  }

  return <ConvexProvider client={convexClient}>{children}</ConvexProvider>
}

3. Middleware Configuration

Create apps/web/middleware.ts:

import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'

const isPublicRoute = createRouteMatcher([
  '/',
  '/products',
  '/api/health(.*)',
  '/api/webhooks(.*)',
  '/_next(.*)',
  '/favicon.ico',
  '/apple-icon.png',
  '/apple-touch-icon.png',
])

const isProtectedRoute = createRouteMatcher([
  '/dashboard(.*)',
  '/profile(.*)',
  '/customers(.*)',
  '/deals(.*)',
  '/tasks(.*)',
  '/contacts(.*)',
])

export default clerkMiddleware(async (auth, request) => {
  try {
    // Allow public routes without authentication
    if (isPublicRoute(request)) {
      return NextResponse.next()
    }

    const { userId } = await auth()

    // Redirect to primary auth domain if trying to access protected route without auth
    if (isProtectedRoute(request) && !userId) {
      const primaryAuthUrl = process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL || 'http://localhost:3005/sign-in'
      const authUrl = new URL(primaryAuthUrl)
      authUrl.searchParams.set('redirect_url', request.url)
      return NextResponse.redirect(authUrl)
    }

    return NextResponse.next()
  } catch (error) {
    console.error('Middleware error:', error)
    return NextResponse.next()
  }
})

export const config = {
  matcher: [
    // Skip Next.js internals and static files
    '/((?!_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)(.*)',
  ],
}

4. Convex Auth Configuration

Update apps/web/convex/auth.config.ts:

export default {
  providers: [
    {
      domain: process.env.CLERK_DOMAIN || "http://localhost:3005", // Primary auth domain
      applicationID: "convex",
    },
  ],
}

URL Parameter Management

Why URL Parameters Appear

When using satellite domains, Clerk adds synchronization parameters to URLs:

  • __clerk_synced - Indicates sync status
  • __clerk_db_jwt - Contains encrypted session data
  • __clerk_hs_reason - Sync reason (e.g., "primary-responds-to-syncing")

These parameters are necessary for the authentication handshake between primary and satellite domains.

Client-Side URL Cleaning (Optional)

You can implement a component to clean these parameters after sync:

// components/clerk-url-cleaner.tsx
"use client"

import { useEffect } from 'react'
import { usePathname, useSearchParams, useRouter } from 'next/navigation'

export function ClerkUrlCleaner() {
  const pathname = usePathname()
  const searchParams = useSearchParams()
  const router = useRouter()

  useEffect(() => {
    const params = new URLSearchParams(searchParams.toString())
    let hasClerkParams = false

    // Remove Clerk sync parameters
    const clerkParams = ['__clerk_synced', '__clerk_db_jwt', '__clerk_hs_reason', '__clerk_handshake']
    clerkParams.forEach(param => {
      if (params.has(param)) {
        params.delete(param)
        hasClerkParams = true
      }
    })

    if (hasClerkParams) {
      const newUrl = params.toString() ? `${pathname}?${params.toString()}` : pathname
      router.replace(newUrl, { scroll: false })
    }
  }, [pathname, searchParams, router])

  return null
}

Add to layout.tsx:

import { ClerkUrlCleaner } from '../components/clerk-url-cleaner'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ConvexClerkProvider>
          <ClerkUrlCleaner />
          {children}
        </ConvexClerkProvider>
      </body>
    </html>
  )
}

Common Issues and Solutions

1. Infinite Redirect Loop

Problem: Browser shows "too many redirects" error

Solution:

# Add to satellite domain .env.local
CLERK_SATELLITE_URL=http://localhost:3015
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=http://localhost:3015/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=http://localhost:3015/dashboard

2. Headers Warning in Next.js 15

Problem: "Route used ...headers() or similar iteration"

Solution: Make middleware async and await auth()

export default clerkMiddleware(async (auth, request) => {
  const { userId } = await auth() // Note the await
  // ... rest of middleware
})

3. Session Not Syncing

Problem: User authenticated on primary but not on satellite

Solution:

  • Ensure both domains use the same Clerk publishable key
  • Check CORS settings allow credentials
  • Verify isSatellite is set to true in ClerkProvider
  • Verify domain points to the primary auth domain

4. Convex Auth Not Working

Problem: User authenticated but Convex queries fail

Solution:

  • Ensure convex/auth.config.ts points to correct primary domain
  • Verify Convex deployment has auth configured
  • Check that ConvexProviderWithClerk is used (not just ConvexProvider)

Testing Checklist

  • User can access public pages without auth (/, /products)
  • Accessing /dashboard redirects to primary auth domain
  • Sign in on primary domain redirects back to customers.dev
  • After auth, user can access /dashboard
  • Session persists across page refreshes
  • Sign out works and clears session
  • Convex queries work with authentication
  • No infinite redirect loops
  • No console errors or warnings

Production Configuration

Environment Variables for Production

# Production - customers.dev
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_***
CLERK_SECRET_KEY=sk_live_***

NEXT_PUBLIC_CLERK_IS_SATELLITE=true
NEXT_PUBLIC_CLERK_DOMAIN=auth.do.dev
NEXT_PUBLIC_CLERK_SIGN_IN_URL=https://auth.do.dev/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=https://auth.do.dev/sign-up
CLERK_SATELLITE_URL=https://customers.dev

NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=https://customers.dev/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=https://customers.dev/dashboard

# Convex
NEXT_PUBLIC_CONVEX_URL=https://impartial-panda-219.convex.cloud
CONVEX_DEPLOY_KEY=prod:***

Security Considerations

  1. Use HTTPS in Production - All auth flows must use HTTPS
  2. Set Proper Cookie Domains - Clerk handles this automatically for satellites
  3. Configure CORS - Primary domain must allow satellite domains
  4. Use Environment-Specific Keys - Different keys for dev/staging/prod

Implementation Checklist

  • Copy this document to customers-dev/docs/authentication/
  • Update .env.local with satellite configuration
  • Create middleware.ts with route protection
  • Update ConvexClerkProvider with satellite settings
  • Update convex/auth.config.ts with primary domain
  • Get real Clerk keys from do.dev primary instance
  • Configure Clerk dashboard to allow customers.dev as satellite
  • Test authentication flow in development
  • Create dashboard route structure
  • Deploy and test in production

Next Steps

After implementing satellite auth:

  1. Build Dashboard UI - Create /dashboard route with customer management
  2. Add Protected Routes - Create /customers, /deals, /tasks routes
  3. Implement Convex Queries - Connect UI to Convex backend
  4. Add User Profile - Allow users to manage their profile
  5. Test Cross-Domain SSO - Verify sessions work across do.dev properties

References

On this page