Building Chrome extensions with Para requires special considerations for state persistence and user experience. This walkthrough covers how to implement Chrome storage overrides, background workers, and seamless authentication flows.
Chrome Extension Challenges
Chrome extensions present unique challenges for web applications:
- State resets - Clicking outside a popup can close and reset the application state
- Limited popup space - Small popup windows aren’t ideal for complex authentication flows
- Background execution - Background workers need access to authentication state
- Storage limitations - Standard localStorage/sessionStorage APIs work differently in extensions
Solution Overview
Para addresses these challenges through:
- Storage overrides - Custom storage implementations using Chrome extension APIs
- Singleton promise pattern - Shared Para instance across popup and background
- Smart routing - Background worker decides between popup vs tab based on auth state
- State persistence - Authentication state survives popup closures
Setup and Configuration
Install Dependencies
npm install @getpara/react-sdk
Create Chrome Storage Overrides
Create a storage implementation that uses Chrome extension storage APIs:
// Chrome local storage overrides
export const localStorageGetItemOverride = async (key: string): Promise<string | null> => {
try {
// Handle special cases
if (key === "guestWalletIds" || key === "pregenIds") {
return JSON.stringify({});
}
const result = await chrome.storage.local.get([key]);
return result[key] || null;
} catch (error) {
console.error("Local storage get error:", error);
return null;
}
};
export const localStorageSetItemOverride = async (key: string, value: string): Promise<void> => {
try {
await chrome.storage.local.set({ [key]: value });
} catch (error) {
console.error("Local storage set error:", error);
}
};
export const localStorageRemoveItemOverride = async (key: string): Promise<void> => {
try {
await chrome.storage.local.remove([key]);
} catch (error) {
console.error("Local storage remove error:", error);
}
};
// Chrome session storage overrides
export const sessionStorageGetItemOverride = async (key: string): Promise<string | null> => {
try {
if (key === "guestWalletIds" || key === "pregenIds") {
return JSON.stringify({});
}
const result = await chrome.storage.session.get([key]);
return result[key] || null;
} catch (error) {
console.error("Session storage get error:", error);
return null;
}
};
export const sessionStorageSetItemOverride = async (key: string, value: string): Promise<void> => {
try {
await chrome.storage.session.set({ [key]: value });
} catch (error) {
console.error("Session storage set error:", error);
}
};
export const sessionStorageRemoveItemOverride = async (key: string): Promise<void> => {
try {
await chrome.storage.session.remove([key]);
} catch (error) {
console.error("Session storage remove error:", error);
}
};
// Clear storage with Para prefix
export const clearStorageOverride = async (): Promise<void> => {
try {
// Get all keys from both storages
const [localKeys, sessionKeys] = await Promise.all([
chrome.storage.local.get(),
chrome.storage.session.get()
]);
// Filter keys with Para prefix
const paraLocalKeys = Object.keys(localKeys).filter(key => key.startsWith("@CAPSULE/"));
const paraSessionKeys = Object.keys(sessionKeys).filter(key => key.startsWith("@CAPSULE/"));
// Remove Para keys
await Promise.all([
paraLocalKeys.length > 0 ? chrome.storage.local.remove(paraLocalKeys) : Promise.resolve(),
paraSessionKeys.length > 0 ? chrome.storage.session.remove(paraSessionKeys) : Promise.resolve()
]);
} catch (error) {
console.error("Clear storage error:", error);
}
};
// Export all overrides as a single object
export const chromeStorageOverrides = {
localStorageGetItemOverride,
localStorageSetItemOverride,
localStorageRemoveItemOverride,
sessionStorageGetItemOverride,
sessionStorageSetItemOverride,
sessionStorageRemoveItemOverride,
clearStorageOverride
};
Create a singleton Para client with Chrome storage overrides:
import { ParaWeb } from "@getpara/react-sdk";
import { chromeStorageOverrides } from "../chrome-storage";
const PARA_API_KEY = process.env.NEXT_PUBLIC_PARA_API_KEY || "your-api-key";
const PARA_ENVIRONMENT = "BETA"; // or "PRODUCTION"
// Create Para instance with Chrome storage overrides
export const para = new ParaWeb(PARA_ENVIRONMENT, PARA_API_KEY, {
...chromeStorageOverrides,
useStorageOverrides: true,
});
// Export shared promise for initialization
export const paraReady = para.init();
Background Worker Implementation
Create a background worker that manages authentication flows:
import { para, paraReady } from "@/lib/para/client";
// Handle extension icon clicks
chrome.action.onClicked.addListener(async () => {
try {
// Wait for Para to be ready
await paraReady;
// Check authentication status
const isLoggedIn = await para.isFullyLoggedIn();
console.log("User authentication status:", isLoggedIn);
if (!isLoggedIn) {
// User not authenticated - open full tab for login
chrome.tabs.create({
url: chrome.runtime.getURL("index.html")
});
} else {
// User authenticated - open popup for quick actions
await chrome.action.setPopup({ popup: "index.html" });
await chrome.action.openPopup();
// Reset popup after opening (allows clicking icon again)
await chrome.action.setPopup({ popup: "" });
}
} catch (error) {
console.error("Authentication check failed:", error);
// Fallback to tab on error
chrome.tabs.create({
url: chrome.runtime.getURL("index.html")
});
}
});
// Optional: Handle installation
chrome.runtime.onInstalled.addListener(async () => {
console.log("Extension installed");
// Initialize Para on installation
try {
await paraReady;
console.log("Para initialized successfully");
} catch (error) {
console.error("Para initialization failed:", error);
}
});
Manifest Configuration
Create a manifest.json
file for your Chrome extension:
{
"manifest_version": 3,
"name": "Para Chrome Extension",
"version": "1.0",
"description": "Chrome extension with Para authentication",
"permissions": [
"storage",
"activeTab"
],
"background": {
"service_worker": "background.js",
"type": "module"
},
"action": {
"default_title": "Para Extension"
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
},
"web_accessible_resources": [
{
"resources": ["index.html"],
"matches": ["<all_urls>"]
}
]
}
Application Setup
Main Application Component
import { useEffect, useState } from "react";
import { para, paraReady } from "@/lib/para/client";
import { useAccount } from "@getpara/react-sdk";
export function App() {
const [isReady, setIsReady] = useState(false);
const { data: account } = useAccount();
useEffect(() => {
// Wait for Para initialization
paraReady.then(() => {
setIsReady(true);
}).catch((error) => {
console.error("Para initialization failed:", error);
setIsReady(true); // Show UI even on error
});
}, []);
if (!isReady) {
return (
<div className="flex items-center justify-center p-8">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto"></div>
<p className="mt-2">Initializing Para...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
{account?.isConnected ? (
<AuthenticatedView />
) : (
<LoginView />
)}
</div>
);
}
function AuthenticatedView() {
const { data: account } = useAccount();
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">Welcome Back!</h1>
<p className="text-gray-600">Address: {account?.address}</p>
<button
onClick={() => para.logout()}
className="mt-4 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
Logout
</button>
</div>
);
}
function LoginView() {
const handleLogin = async () => {
try {
await para.signUpOrLogin();
} catch (error) {
console.error("Login failed:", error);
}
};
return (
<div className="p-6 text-center">
<h1 className="text-2xl font-bold mb-4">Para Chrome Extension</h1>
<p className="text-gray-600 mb-6">Sign in to get started</p>
<button
onClick={handleLogin}
className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
>
Sign In with Para
</button>
</div>
);
}
Para Provider Setup
components/ParaProvider.tsx
import { ParaProvider as BaseParaProvider } from "@getpara/react-sdk";
import { para } from "@/lib/para/client";
interface ParaProviderProps {
children: React.ReactNode;
}
export function ParaProvider({ children }: ParaProviderProps) {
return (
<BaseParaProvider client={para}>
{children}
</BaseParaProvider>
);
}
User Experience Patterns
Smart Authentication Flow
The background worker implements smart routing based on authentication state:
// Pseudocode for authentication flow decision
if (userNotAuthenticated) {
// Open full tab - more space for authentication
openTab("index.html");
} else {
// Open popup - quick access for authenticated users
openPopup("index.html");
}
State Persistence
Para state persists across popup sessions:
// User clicks extension icon
// -> Popup opens with preserved authentication state
// -> User interacts with popup
// -> User clicks outside, popup closes
// -> User clicks extension icon again
// -> Popup reopens with same state (no re-authentication needed)
Error Handling
Implement robust error handling for extension-specific scenarios:
const handleExtensionError = (error: Error) => {
console.error("Extension error:", error);
// Always fallback to tab on critical errors
chrome.tabs.create({
url: chrome.runtime.getURL("index.html")
});
};
Building and Deployment
Build Configuration
Configure your build tool (Webpack, Vite, etc.) for Chrome extension:
module.exports = {
entry: {
background: './src/background.ts',
content: './src/index.tsx'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
// ... other webpack configuration
};
Development Testing
-
Build your extension:
-
Load in Chrome:
- Open
chrome://extensions/
- Enable “Developer mode”
- Click “Load unpacked”
- Select your
dist
folder
-
Test authentication flows:
- Click extension icon when logged out (should open tab)
- Complete authentication
- Click extension icon when logged in (should open popup)
Production Considerations
- Permissions: Only request necessary permissions in manifest
- CSP: Configure Content Security Policy for WASM and external resources
- Error reporting: Implement error tracking for production
- Performance: Optimize background worker to minimize resource usage
Advanced Features
Tab Communication
Communicate between popup and tabs:
// In popup
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id!, {
type: "PARA_AUTH_STATUS",
isAuthenticated: true
});
});
// In content script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "PARA_AUTH_STATUS") {
// Handle authentication status update
updateUIBasedOnAuth(message.isAuthenticated);
}
});
Add context menu integration:
// In background.ts
chrome.contextMenus.create({
id: "para-action",
title: "Sign with Para",
contexts: ["selection"]
});
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId === "para-action") {
// Handle context menu action
await paraReady;
const isLoggedIn = await para.isFullyLoggedIn();
if (isLoggedIn) {
// Perform action with selected text
console.log("Selected text:", info.selectionText);
}
}
});
Troubleshooting
Common Issues
Storage not persisting:
- Ensure
useStorageOverrides: true
is set
- Check Chrome storage permissions in manifest
- Verify storage override functions are async
Background worker not receiving state:
- Confirm
paraReady
promise is properly awaited
- Check console for initialization errors
- Verify manifest background configuration
Popup closing unexpectedly:
- This is expected Chrome behavior
- Use tabs for complex flows
- Implement state persistence with storage overrides
Next Steps