Menu

Browser Extension Development Guide with Nexty Backend

Overview

When developing browser extensions, we typically need a server-side component. Nexty provides essential backend capabilities for SaaS applications, including Supabase authentication, Stripe payments, and more. You can build your server-side functionality using Nexty, making browser extension development faster as well.

This guide demonstrates how to implement browser extension login and business data synchronization using Nexty's server-side capabilities.

The following content primarily addresses 3 requirements:

  • Browser extension public data requests
  • Browser extension user login
  • Browser extension authenticated private data requests

Testing Method

This article mainly explains the approach, and the code is not complete. You can clone the complete source code below for local testing:

git clone https://github.com/WeNextDev/nexty.dev.git
cd nexty.dev
git checkout extension-request-demo
 
pnpm install
pnpm dev
git clone https://github.com/WeNextDev/nexty-extension-request-demo.git
cd nexty-extension-request-demo
 
pnpm install
pnpm dev

Requirement 1: Public Data Requests

Suppose our server has an endpoint that returns public data, with the request path like this:

Loading diagram...

We first write a public endpoint in the Nexty code that doesn't require authentication:

app/api/extension-demo/public-data/route.ts
import { apiResponse } from "@/lib/api-response";
 
export async function GET() {
  const publicData = {
    latestAnnouncement:
      "🎉 Welcome to Nexty.dev! This is a public endpoint.",
  };
 
  try {
    const response = apiResponse.success(publicData);
    response.headers.set(
      "Cache-Control",
      "public, s-maxage=86400, stale-while-revalidate=43200", // Add cache headers to avoid frequent requests and reduce server pressure
    );
  } catch (error) {
    console.error("Error fetching public data:", error);
    return apiResponse.serverError();
  }
} 

Make a request from the browser extension frontend:

popup.tsx
useEffect(() => {
  chrome.runtime.sendMessage({ type: "GET_PUBLIC_DATA" }, (response) => {
    if (response.success) {
      setPublicDataState({ data: response.data, error: null, isLoading: false });
    } else {
      setPublicDataState({ data: null, error: response.error, isLoading: false });
    }
  });
}, []);

Forward the request in the browser extension background:

background/index.ts
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.type === "GET_PUBLIC_DATA") {
    apiFetch("/api/extension-demo/public-data", false, "public_data_cache").then(sendResponse);
    return true;
  }
 
  return false;
});

Now the browser extension frontend can receive data from the public endpoint.

Requirement 2: Extension Login Authentication and Login State Synchronization

Authentication Flow Diagram

Let's first look at the authentication flow diagram

Loading diagram...

The core principles are 3 points:

  1. Nexty website is the sole authentication center: Browser extension doesn't handle login credentials, it only "borrows" the website's login state
  2. Server-side verification: All permission checks are performed on the Nexty backend to ensure data security
  3. Real-time synchronization: Browser extension verifies the latest user status every time it opens, ensuring information accuracy

Implementation Steps

Create an API to get user login state in the Nexty project app/api/user-status/route.ts. There's a very important step here: return the complete Cookie to ensure the extension's login state can persist:

app/api/user-status/route.ts
// ... other code ...
 
    // Return info for not logged in:
    if (!user) {
      return apiResponse.success({ isLoggedIn: false })
    }
 
    // Return info for logged in
    const responseData = {
      isLoggedIn: true, // Logged in identifier
      id: user.id,
      email: user.email,
      plan: profile?.role || 'user',
    }
 
    const finalResponse = apiResponse.success(responseData)
    // Important! Return Cookie
    response.cookies.getAll().forEach((cookie) => {
      finalResponse.cookies.set(cookie)
    })
    return finalResponse
 
// ... other code ...

Make a request from the browser extension frontend:

popup.tsx
useEffect(() => {
  chrome.runtime.sendMessage({ type: "GET_USER_STATUS" }, (response) => {
    if (response.success) {
      setUserStatusState({ data: response.data, error: null, isLoading: false });
    } else {
      if (!cachedStatus) setUserStatusState({ data: null, error: response.error, isLoading: false });
    }
  });
}, []);

Forward the request in the browser extension background:

background/index.ts
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.type === "GET_USER_STATUS") {
    apiFetch("/api/extension-demo/user-status", true, "user_status_cache").then(sendResponse);
    return true;
  }
 
  return false;
});

If the user is logged in, display user information; if the user is not logged in, display a login button. Clicking the button will call the chrome.tabs.create method to open the login page in a new tab:

    const handleLoginClick = () => {
      chrome.tabs.create({ url: process.env.PLASMO_PUBLIC_SITE_LOGIN_URL });
    };
 
    return (
    <div>
      <h2>User Info</h2>
      {userStatusState.data && (
        userStatusState.data.isLoggedIn ? (
          <div>
            <p>Welcome, <strong>{userStatusState.data.email}</strong>!</p>
            <p>Your user role: <strong>{userStatusState.data.plan}</strong></p>
          </div>
        ) : (
          <div>
            <h4>Please login to use</h4>
            <button onClick={handleLoginClick}>Go to login</button>
          </div>
        )
      )}
    </div>
  );

In the browser extension code provided in this documentation, optimistic updates are also applied. This means old cached data is displayed first, and new data overwrites the old data after the request completes:

popup.tsx
useEffect(() => {
  // First try to read cache
  const cachedStatus = await storage.get<UserStatus>("user_status_cache");
  if (cachedStatus) {
    setUserStatusState({ data: cachedStatus, error: null, isLoading: false });
  }
 
  chrome.runtime.sendMessage({ type: "GET_USER_STATUS" }, (response) => {
    // ... other code ...
  });
}, []);

To allow users to see the user status as soon as they open the browser extension, we can also make a request when the extension is installed and when the browser starts:

background/index.ts
// When the extension installed or the browser starts
// get the status once as the initial cache
chrome.runtime.onInstalled.addListener(() => {
  apiFetch("/api/extension-demo/user-status", true, "user_status_cache");
});
 
chrome.runtime.onStartup.addListener(() => {
  apiFetch("/api/extension-demo/user-status", true, "user_status_cache");
});

Requirement 3: Private Data Requests

During actual product development, there will be some data that requires logged-in users to access, such as user subscription status.

We create an endpoint to get user subscription status as an example:

app/api/extension-demo/user-benefits/route.ts
export async function GET(request: NextRequest) {
 
  try {
    // 1. Check login status
    const {
      data: { user },
    } = await supabase.auth.getUser();
    if (!user) {
      return apiResponse.unauthorized();
    }
 
    // 2. Call Server Actions, no need to write separate processing logic
    const benefits = await getUserBenefits(user.id);
 
    // 3. Return
    const finalResponse = apiResponse.success(benefits);
    response.cookies.getAll().forEach((cookie) => {
      finalResponse.cookies.set(cookie);
    });
    return finalResponse;
  } catch (error) {
    console.error("Error fetching user benefits:", error);
    return apiResponse.serverError();
  }
} 

The browser frontend needs to check login status before making requests - only logged-in users should make requests:

popup.tsx
if (!userStatusState.isLoading && userStatusState.data?.isLoggedIn) {
 
  const cachedBenefits = await storage.get<UserBenefits>("user_benefits_cache");
  if (cachedBenefits) {
    setUserBenefitsState({ data: cachedBenefits, error: null, isLoading: false });
  }
 
  chrome.runtime.sendMessage({ type: "GET_USER_BENEFITS" }, (response) => {
    if (response.success) {
      setUserBenefitsState({ data: response.data, error: null, isLoading: false });
    } else {
      if (!cachedBenefits) setUserBenefitsState({ data: null, error: response.error, isLoading: false });
    }
  });
}

The browser extension background still forwards requests:

background/index.ts
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.type === "GET_USER_BENEFITS") {
    apiFetch("/api/extension-demo/user-benefits", true, "user_benefits_cache").then(sendResponse);
    return true;
  }
 
  return false;
});

Other Best Practice Recommendations

  • Use independent API prefix: Use an independent prefix for APIs provided to browser extensions, making it easier to handle common logic

  • Leverage caching to reduce server pressure: For data that doesn't require high real-time performance, use Next.js caching capabilities or add cache headers to HTTP responses to reduce server pressure

    Good to know

    Refer to app/api/extension-demo/public-data/route.ts in the Nexty source code extension branch

  • Encrypt requests: Browser extension requests should add encrypted signature headers, so even public endpoints have an additional layer of protection

    Good to know

    Demo source code locations:

    • middleware.ts and lib/crypto-utils.ts in Nexty source code extension branch
    • background/index.ts and background/crypto-utils.ts in browser extension source code

    The encryption methods in crypto-utils.ts are for example purposes only. For more sophisticated encryption requirements, consider using AI-assisted development, as AI excels at implementing cryptographic algorithms.