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:
- For server-side code, check Nexty source code extension branch
- For browser extension code, check 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
Suppose our server has an endpoint that returns public data, with the request path like this:
We first write a public endpoint in the Nexty code that doesn't require authentication:
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 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
The core principles are 3 points:
- Nexty website is the sole authentication center: Browser extension doesn't handle login credentials, it 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 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:
// ... 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:
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 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:
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:
// 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:
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:
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:
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
andlib/crypto-utils.ts
in Nexty source code extension branchbackground/index.ts
andbackground/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.