Chapter 13 · Clerk Chrome Extension Patterns
Subchapter 13.1
references/content-scripts.mdMarkdown3 KBView on GitHub
Content scripts run in an isolated JavaScript world injected into web pages. They cannot:
Use message passing to request auth state from the background service worker.
src/content.ts:
async function getToken(): Promise<string | null> {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ type: 'GET_TOKEN' }, (response) => {
resolve(response?.token ?? null)
})
})
}
async function isSignedIn(): Promise<boolean> {
const token = await getToken()
return token !== null
}
async function injectUI() {
const signedIn = await isSignedIn()
if (!signedIn) {
console.log('User not signed in, skipping injection')
return
}
const overlay = document.createElement('div')
overlay.id = 'my-extension-overlay'
document.body.appendChild(overlay)
}
injectUI()src/background/index.ts:
import { createClerkClient } from '@clerk/chrome-extension/client'
const publishableKey = process.env.PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY
async function getToken(): Promise<string | null> {
const clerk = await createClerkClient({ publishableKey, background: true })
if (!clerk.session) return null
return await clerk.session.getToken()
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === 'GET_TOKEN') {
getToken()
.then((token) => sendResponse({ token }))
.catch(() => sendResponse({ token: null }))
return true
}
})// content.ts
async function fetchUserData() {
const token = await getToken()
if (!token) return null
const res = await fetch('https://api.yourapp.com/me', {
headers: { Authorization: `Bearer ${token}` },
})
return res.json()
}package.json (Plasmo):
{
"manifest": {
"permissions": ["storage", "tabs"],
"host_permissions": ["<all_urls>"]
}
}For content scripts on specific domains only:
{
"manifest": {
"permissions": ["storage"],
"host_permissions": ["https://specific-site.com/*"]
}
}A file named content.ts or content.tsx at the project root is auto-registered as a content script matching all URLs.
For multiple content scripts with different match patterns, use package.json:
{
"manifest": {
"content_scripts": [
{
"matches": ["https://specific-site.com/*"],
"js": ["content.js"]
}
]
}
}Clerk enforces strict allowed origins for API requests. A content script can be injected into any domain (e.g., https://github.com, https://google.com). There is no way to add all possible domains to Clerk’s allowed origins, so direct Clerk usage in content scripts is blocked by design.