Menu

Browser Extension Development Guide Based on Nexty Server

Overview

When developing browser extensions, we typically need a server backend. Nexty provides essential server capabilities for SaaS products, including Supabase authentication and Stripe payments. You can completely build your server functionality based on Nexty, making browser extension development faster.

This article will detail how to implement browser extension login and business data synchronization based on Nexty template's server capabilities.

The following content primarily addresses 3 requirements:

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

Testing Method

This article mainly explains the concepts. The code is not complete. You can clone the following complete source code to test locally:

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

Assume our server has an endpoint that returns public data with the following request path:

Loading diagram...

Let's first write a public, non-authenticated endpoint in the Nexty code:

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 get the data returned by 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...

It looks a bit complex, but the core principles are just 3 points:

  1. Nexty website is the sole authentication center: Browser extension doesn't handle login credentials, 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 accurate information

Implementation Steps

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

app/api/user-status/route.ts
// ... other code ...
 
    // Return info for non-logged-in users:
    if (!user) {
      return apiResponse.success({ isLoggedIn: false })
    }
 
    // Return info for logged-in users
    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 not logged in, display a login button. Clicking the button calls 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 document, the optimistic update concept is also applied, which means displaying old cached data first, then overwriting with new 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 ...
  });
}, []);

Additionally, to let users see their 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, some data definitely requires logged-in users to access, such as user subscription status.

Let's 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();
  }
} 

Browser frontend needs to check login status before making requests, only logged-in users 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 });
    }
  });
}

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 to facilitate handling common logic

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

    Good to know

    Reference Nexty source extension branch's app/api/extension-demo/public-data/route.ts

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

    Good to know

    Demo source code locations:

    • Nexty source extension branch's middleware.ts and lib/crypto-utils.ts
    • Browser extension source's background/index.ts and background/crypto-utils.ts

    The encryption methods in crypto-utils.ts are examples only. If you need more complex encryption methods, you can have AI write them for you - they're excellent at encryption algorithms.