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.devSatellite 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/dashboard2. 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
isSatelliteis set totruein ClerkProvider - Verify
domainpoints to the primary auth domain
4. Convex Auth Not Working
Problem: User authenticated but Convex queries fail
Solution:
- Ensure
convex/auth.config.tspoints 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
- Use HTTPS in Production - All auth flows must use HTTPS
- Set Proper Cookie Domains - Clerk handles this automatically for satellites
- Configure CORS - Primary domain must allow satellite domains
- 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:
- Build Dashboard UI - Create
/dashboardroute with customer management - Add Protected Routes - Create
/customers,/deals,/tasksroutes - Implement Convex Queries - Connect UI to Convex backend
- Add User Profile - Allow users to manage their profile
- Test Cross-Domain SSO - Verify sessions work across do.dev properties