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:
- For server code, check the Nexty source extension branch
- For browser extension code, check the open source repository
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:
Let's first write a public, non-authenticated endpoint in the Nexty code:
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:
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:
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:
It looks a bit complex, but the core principles are just 3 points:
- Nexty website is the sole authentication center: Browser extension doesn't handle login credentials, only "borrows" the website's login state
- Server-side verification: All permission checks are performed on the Nexty backend to ensure data security
- 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:
// ... 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:
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:
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:
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:
// 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:
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:
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:
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
andlib/crypto-utils.ts
- Browser extension source's
background/index.ts
andbackground/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. - Nexty source extension branch's