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:

  1. Storage overrides - Custom storage implementations using Chrome extension APIs
  2. Singleton promise pattern - Shared Para instance across popup and background
  3. Smart routing - Background worker decides between popup vs tab based on auth state
  4. 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:

lib/chrome-storage.ts
// 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
};

Configure Para Client

Create a singleton Para client with Chrome storage overrides:

lib/para/client.ts
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:

background.ts
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.json
{
  "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

components/App.tsx
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:

webpack.config.js
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

  1. Build your extension:

    npm run build
    
  2. Load in Chrome:

    • Open chrome://extensions/
    • Enable “Developer mode”
    • Click “Load unpacked”
    • Select your dist folder
  3. 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);
  }
});

Context Menus

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