Back to blogs
Blog
Writing in public
Longer posts — published here first, then cross-posted to Dev.to with a canonical URL back to this site.
Blog
Longer posts — published here first, then cross-posted to Dev.to with a canonical URL back to this site.
Back to blogs
Back to blogs
Postpocketbase, nextjs, authentication, cookies
The Pocketbase SDK assumes SPA by default and saves the auth response in the local storage , while...
Date published

Why You Might Still Pick Next.js Over TanStack Start From a TanStack Start...

Modify your nextauth client and add a callbacks section that will get the access token from the...

You can select the scopes you want to request from GitHub/your oauthprovider in the login page, and...
The Pocketbase SDK assumes SPA by default and saves the auth response in the local storage , while you can do this in the app directory or other SSR first frameworks , it will give you hydration errors as the state goes from unauthenticated on the server to authenticated on the client which will cause a html mismatch if for example you were rendering a user profile picture if a user exists .
Pocketbase has an exportToCookie which helps with this issue . The Nextjs13 integration is a little more tedious compared to the other frameworks.
First step is picking server or client login , this example ha s a server component and actions example included too ,but you'll have to setup some logic to sync the cookie with the client,
1export async function loginUser({ pb,user, password }: ILoginUser) {2 try {3 const authData = await pb.collection(pb_user_collection)4 .authWithPassword<PBUserRecord>(user, password);5 return authData;6 } catch (error) {7 throw error;8 }9}
1 const handleSubmit = async (e: React.FormEvent) => {2 e.preventDefault();3 try {4 const pb_user = await loginUser({5 pb,6 user: user.identity,7 password: user.password,8 });9 // add te authstore to a cookie for easy server side use10 document.cookie = pb.authStore.exportToCookie({ httpOnly: false });11 if (search_params) {12 // redirect to the page tey were going to before13 router.push(search_params);14 } else {15 router.push("/");16 }17 // console.log("auth success = ",pb_user);18 return pb_user;19 } catch (error: any) {20 console.log("error logging in user === ", error.originalError);21 throw error;22 }23 };
auth guarding , assuming the /admin page needs authentication and homepage can be used without a loginadd a middleware.ts in our root directory , in my case am using src/app so it'll be in the src directory
```ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import PocketBase from "pocketbase";
import { pb_url, pb_user_collection } from "./state/consts";
import { getNextjsCookie } from "./state/utils/server-cookie";
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
const request_cookie = request.cookies.get("pb_auth") // console.log("middlware request cookie ===",)
const cookie = await getNextjsCookie(request_cookie)
const pb = new PocketBase(pb_url);
if (cookie) {
try {
pb.authStore.loadFromCookie(cookie)
} catch (error) {
pb.authStore.clear();
response.headers.set(
"set-cookie",
pb.authStore.exportToCookie({ httpOnly: false })
);
}
}
try {
// get an up-to-date auth store state by verifying and refreshing the loaded auth model (if any)
pb.authStore.isValid &&
(await pb.collection(pb_user_collection).authRefresh());
} catch (err) {
// clear the auth store on failed refresh
pb.authStore.clear();
response.headers.set(
"set-cookie",
pb.authStore.exportToCookie({ httpOnly: false })
);
}
if (!pb.authStore.model && !request.nextUrl.pathname.startsWith("/auth")) {
const redirect_to = new URL("/auth", request.url);
if (request.nextUrl.pathname){
redirect_to.search = new URLSearchParams({
next: request.nextUrl.pathname,
}).toString();
}else{
redirect_to.search = new URLSearchParams({
next:'/',
}).toString();
}
return NextResponse.redirect(redirect_to);
}
if (pb.authStore.model && request.nextUrl.pathname.startsWith("/auth")) {
const next_url = request.headers.get("next-url") as string
if(next_url){
const redirect_to = new URL(next_url, request.url);
return NextResponse.redirect(redirect_to);
}
const redirect_to = new URL(/,request.url);
return NextResponse.redirect(redirect_to);
}
return response;
}
export const config = {
matcher: ["/admin/:path", "/auth/:path"],
};
1234This now allows you to redirect any unauthenticated users from `admin` to `/auth`56- getting the user7you can still get the user client side by using8
ts export const pb = new PocketBase(pb_url); pb.authStore.loadFromCookie(document?.cookie ?? ""); return pb.authStore.model
12But we can also fetch the user that was used to login and fetch data on the server in a server component3
ts import { server_component_pb } from "@/state/pb/server_component_pb"; import { TestClientCookie } from "./components/TestClientCookie"; import { cookies } from "next/headers"; import { TestServerCookie } from "./components/TestServerComponentCookie"; import { encodeCookie, encodeNextPBCookie } from "@/state/utils/encodeCookies";
export default async function AdminPage() { // const { pb,cookies } = await server_component_pb(); const pb_auth_cookie = await cookies().get('pb_auth') const parsed_cookie = encodeNextPBCookie(pb_auth_cookie) return (
<main className="flex min-h-screen flex-col items-center justify-between p-2">
<div className="h-full flex flex-col items-center justify-center">
<h1 className="text-8xl font-bold">Welcome to</h1>
<h1 className="text-5xl font-bold text-yellow-700">ADMIN Page</h1>
<TestClientCookie />
<TestServerCookie cookie={parsed_cookie}/>
</div>
</main>
); }
1and use it in our component
ts import PocketBase from 'pocketbase' import { pb_url } from '../consts' export function testPocketbaseCookie(cookie: string) { const pb = new PocketBase(pb_url) // load the auth store data from the request cookie string pb.authStore.loadFromCookie(cookie) // console.log("pb_auth model after cookie load === ", pb.authStore.model) // console.log("pb_auth is valid after cookie load === ", pb.authStore.isValid) if(pb.authStore.isValid){ return pb.authStore.model?.email } return "pb_auth is invalid" }
// server fetched user import { testPocketbaseCookie } from "@/state/utils/testCookie"; interface TestClientCookieProps { cookie: string }
export function TestServerCookie({cookie}: TestClientCookieProps) { const pb_email = testPocketbaseCookie(cookie);
if (pb_email === "pb_auth is invalid") { return (
<div className="w-full h-full flex flex-col items-center justify-center gap-3">
<div className="text-6xl font-bold">Server</div>
<textarea value={cookie} className="text-sm w-[90%] border rounded h-[200px] p-3" readOnly />
<div className="text-5xl text-red-500">{pb_email}</div>
</div>
); } return (
<div className="w-full h-full flex flex-col items-center justify-center gap-3">
<div className="text-6xl font-bold">Server</div>
<textarea value={cookie} className="text-sm w-[90%] border rounded h-[200px] p-3" readOnly />
<div className="text-5xl">{pb_email}</div>
</div>
); }
// client fetched user "use client" import { testPocketbaseCookie } from "@/state/utils/testCookie"; interface TestClientCookieProps {
}
export function TestClientCookie({}:TestClientCookieProps){ const cookie = document.cookie console.log("documanet cookie == ",document.cookie) // console.log("decoded cookie === ",decodeURIComponent(document.cookie)) const pb_email = testPocketbaseCookie(cookie) if(pb_email === "pb_auth is invalid"){ return (
<div className="w-full h-full flex flex-col items-center justify-center gap-3">
<div className="text-6xl font-bold">Client</div>
<textarea value={cookie} className="text-sm w-[90%] border rounded h-[200px] p-3" readOnly />
<div className="text-5xl text-red-500">{pb_email}</div>
</div>
); } return (
<div className="w-full h-full flex flex-col items-center justify-center gap-3">
<div className="text-6xl font-bold ">Client</div>
<textarea value={cookie} className="text-sm w-[90%] border rounded h-[200px] p-3" readOnly />
<div className="text-5xl">{pb_email}</div>
</div>
); }
1234And now we can SetUp 2 components to text this out56- Client Side User Fetch7
tsx "use client" import { pb } from "@/state/pb/client_config";
interface WillCauseHydrationIssuesProps {
}
export function WillCauseHydrationIssues({}:WillCauseHydrationIssuesProps){ const user = pb.authStore.model return (
<div className='w-full h-full flex items-center justify-center'>
{user && <h1 className="text-8xl font-bol text-red-400">User Logged in </h1>}
</div>
); }
12Server side user fetch and pass user as prop to the client component3
tsx "use client"; import { PBUserRecord } from "@/state/user/types"; import { Record, Admin } from "pocketbase";
interface WontCauseHydrationIssuesProps { user: Record | Admin | null; }
/**
export function WontCauseHydrationIssues({ user }: WontCauseHydrationIssuesProps) { return (
<div className="w-full h-full flex items-center justify-center">
{user && <h1 className="text-2xl font-bold">User Logged In</h1>}
</div>
); }
12Do note that for something with no user interactions like this , rendering it as a server component will serve you better , but components like this usually need user interactions like dark mode toggle or user logout on profile pic click ,NextJS server actions can also help out here eliminating need for client components but I'd usually place those items in toolbars and sidebars which work best as server components34also a few pointers on the cookies you get out of5
ts import { cookies } from "next/headers";
12might vary depending on whether it was saved with `document.cookie or cookie().set()` ,the PocketBase client will correctly parse the `document.cookie` string but you might have to encode the object from `cookie().get()` into a valid cookie string before passing it into3
ts pb.authStore.loadFromCookie(cookie || "");
1 looking at the ` pb.authStore` implementation and seems like it might work if you pass in the correct shape of cookie object but for convenience i made a [parsing function](https://github.com/tigawanna/next-pocketbase-demo/blob/9bcdfe43ad7dc4445e8240e3a2a841138010de9d/src/state/utils/encodeCookies.ts) that turns it the `cookie().get("pb_auth")` object that looks like23
ts { name: 'pb_auth', value: '{"token":"eyJhbGciOiJIJleHAiOjE2ODk4MjQ3ODIsImlkIjoiN2R4dzcyNDEyaDBlazdkIiwidHlwZSI6ImF1dG.MPqkEy9dd6UMXT46SXVvjss2OP-_J9agR7E5mdjS_kk","model":{"access_token":"","avatar":"","bio":"","collectionId":"5sckr8a13tos","collectionName":"developers","created":"2023-06-27 20:40:23.037Z","email":"picopicpo@email.com","emailVisibility":true,"github_avatar":"","github_login":"","id":"7dxw72412h0ek7d","updated":"2023-07-05 05:30:08.835Z","username":"picopico","verified":false,"expand":{}}}' }
12into a string that looks something like this3
json const cookie= "%7B%22token%22%3A%22JhbiOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9zcyNDEyaDBlazdkIiwidHlwZSI6ImF1dGhSZWNvcmQifQ...."
```