Chapter 13 · Clerk Chrome Extension Patterns
Subchapter 13.3
references/headless-extension.mdMarkdown4 KBView on GitHub
An extension that runs entirely in the background – no UI, no popup, no side panel. It syncs auth state from a companion web app and acts on behalf of the signed-in user automatically.
Examples:
syncHost + createClerkClient({ background: true }) combinationsrc/background/index.ts:
import { createClerkClient } from '@clerk/chrome-extension/client'
const publishableKey = process.env.PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY
const syncHost = process.env.PLASMO_PUBLIC_CLERK_SYNC_HOST
if (!publishableKey || !syncHost) {
throw new Error('Missing publishable key or sync host')
}
async function getAuthenticatedUser() {
const clerk = await createClerkClient({
publishableKey,
syncHost,
background: true,
})
return clerk.user
}
async function getSessionToken(): Promise<string | null> {
const clerk = await createClerkClient({
publishableKey,
syncHost,
background: true,
})
if (!clerk.session) return null
return await clerk.session.getToken()
}
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.status !== 'complete') return
const token = await getSessionToken()
if (!token) return
await fetch('https://api.yourapp.com/page-visit', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: tab.url }),
})
}).env.development:
PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_FRONTEND_API=https://your-app.clerk.accounts.dev
PLASMO_PUBLIC_CLERK_SYNC_HOST=http://localhost.env.production:
PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
CLERK_FRONTEND_API=https://clerk.your-domain.com
PLASMO_PUBLIC_CLERK_SYNC_HOST=https://clerk.your-domain.compackage.json:
{
"manifest": {
"key": "$CRX_PUBLIC_KEY",
"permissions": ["cookies", "storage", "tabs"],
"host_permissions": [
"$PLASMO_PUBLIC_CLERK_SYNC_HOST/*",
"$CLERK_FRONTEND_API/*"
]
}
}host_permissions for the sync host domain is what allows the extension to read the Clerk session cookie from the web app.
The extension ID must be in your web app instance’s allowed origins:
curl -X PATCH https://api.clerk.com/v1/instance \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-type: application/json" \
-d '{"allowed_origins": ["chrome-extension://YOUR_EXTENSION_ID"]}'In a popup extension with syncHost, the user can also sign in directly via the popup (email/password, OTP). In a headless extension, there is no UI at all – the user MUST sign in via the web app. The extension only reads auth state.
To verify auth state is syncing:
const clerk = await createClerkClient({ publishableKey, syncHost, background: true })
console.log('User:', clerk.user?.emailAddresses[0]?.emailAddress ?? 'Not signed in')
console.log('Session:', clerk.session?.id ?? 'No session')If user is null despite being signed in on the web app, check:
host_permissions includes the sync host domainsyncHost value matches the Clerk Frontend API URL (not the web app’s main domain)