js-sdk: Can't set up properly Next.js's SSR

I’ve followed the examples displayed in the README, as well as looked for solutions in other frameworks but I simply can’t fix this problem.

Doing a little bit of debug and research, I can come to the conclusion that the cookie contained by the request may be not what the authStore expects, because, according to the examples, the key pb_auth= is set as the default, while my cookie has the following structure:

a_session_[code]_legacy=[token]

Also, if it has anything to do with this, the token stored in localStorage it’s not equal to the one served on the cookie.

As extra information, I leave the code I wrote.

type ServerRequest = IncomingMessage & {
  cookies: Partial<{
    [key: string]: string
  }>
}

class CustomAuthStore extends BaseAuthStore {
  public request: ServerRequest
  public response: ServerResponse<IncomingMessage>

  constructor (request: ServerRequest, response: ServerResponse<IncomingMessage>) {
    super()

    this.request = request
    this.response = response

    this.loadFromCookie(
      this.request.headers.cookie ?? ''
    )
  }

  save (token: string, model: User | Admin | null): void {
    super.save(token, model)

    this.response.setHeader('set-cookie', this.exportToCookie())
  }

  clear (): void {
    super.clear()

    this.response.setHeader('set-cookie', this.exportToCookie())
  }
}

Versions: pocketbase v0.7.10; pocketbase (sdk) v0.7.4

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 40 (5 by maintainers)

Most upvoted comments

Hey, I have written functions returning PocketBase instances for both Server and Client components. I have done it in a style @supabase/ssr does it for ease of understanding

For client components : This instantiates a PocketBase instance using the singleton pattern, so you can call it anywhere you want. It also syncs the cookie with localStorage, thus seamlessly responding to auth state change

createBrowserClient
import { TypedPocketBase } from "@/types/pocketbase-types";
import PocketBase from "pocketbase";

let singletonClient: TypedPocketBase | null = null;

export function createBrowserClient() {
  if (!process.env.NEXT_PUBLIC_POCKETBASE_API_URL) {
    throw new Error("Pocketbase API url not defined !");
  }

  const createNewClient = () => {
    return new PocketBase(
      process.env.NEXT_PUBLIC_POCKETBASE_API_URL
    ) as TypedPocketBase;
  };

  const _singletonClient = singletonClient ?? createNewClient();

  if (typeof window === "undefined") return _singletonClient;

  if (!singletonClient) singletonClient = _singletonClient;

  singletonClient.authStore.onChange(() => {
    document.cookie = singletonClient!.authStore.exportToCookie({
      httpOnly: false,
    });
  });

  return singletonClient;
}

For server components : This instantiates a PocketBase instance for each call, you can pass it a cookieStore, in which case you will get the authStore instantiated. For static routes, you can use it without passing any cookieStore.

createServerClient
import { TypedPocketBase } from "@/types/pocketbase-types";
import { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
import PocketBase from "pocketbase";

export function createServerClient(cookieStore?: ReadonlyRequestCookies) {
  if (!process.env.NEXT_PUBLIC_POCKETBASE_API_URL) {
    throw new Error("Pocketbase API url not defined !");
  }

  if (typeof window !== "undefined") {
    throw new Error(
      "This method is only supposed to call from the Server environment"
    );
  }

  const client = new PocketBase(
    process.env.NEXT_PUBLIC_POCKETBASE_API_URL
  ) as TypedPocketBase;

  if (cookieStore) {
    const authCookie = cookieStore.get("pb_auth");

    if (authCookie) {
      client.authStore.loadFromCookie(`${authCookie.name}=${authCookie.value}`);
    }
  }

  return client;
}

Middleware : You can have a middleware.ts at the root of your project or the src folder with matching paths for protected route. If user is not authenticated, redirect to your intended path.

middleware.ts
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { createServerClient } from "./lib/pocketbase";

// For protected pages
// If auth is not valid for matching routes
// Redirect to a redirect path
export function middleware(request: NextRequest) {
  const redirect_path = "http://localhost:3000/login";

  const cookieStore = cookies();

  const { authStore } = createServerClient(cookieStore);

  if (!authStore.isValid) {
    return NextResponse.redirect(redirect_path);
  }
}

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * - login (login route)
     * - / (root path)
     */
    "/((?!api|_next/static|_next/image|favicon.ico|login|$).*)",
  ],
};

Note :

  1. Please note this depends on a NEXT_PUBLIC_POCKETBASE_API_URL, you must have this in your .env file
  2. For type suggestions, I am using the pocketbase-typegen by @patmood, you can generate the types using this package and place in @types/pocketbase-types.ts or a befitting path and update the import paths.
  3. Please let me know if any problem arises, I’ll try my best to fix and you can keep an eye on my github repo, I will publish a NextJS & Pocketbase template soon with examples

I’ve managed to get it working in the new app directory, using (https://github.com/vvo/iron-session/issues/560#issuecomment-1324598048 as a base. The only thing to note is that any actions that would update the authStore can only happen in the client, fow now.

lib/getUserFromCookie.ts
import type { User } from "@/interfaces";

import { ReadonlyRequestCookies } from "next/dist/server/app-render";

import { pocketbase } from "@/lib";

/**
 * Can be called in page/layout server component.
 * @param cookies ReadonlyRequestCookies
 * @returns User or null
 * @author Arif "poltang" Muslax
 * @see {@link https://github.com/vvo/iron-session/issues/560#issuecomment-1324598048}
 */
function getUserFromCookie(cookies: ReadonlyRequestCookies): User | null {
  const authCookie = cookies.get("pb_auth");

  if (!authCookie) return null;

  pocketbase.authStore.loadFromCookie(`${authCookie.name}=${authCookie.value}`);
  const user = pocketbase.authStore.model;

  return user as unknown as User;
}

export { getUserFromCookie };

lib/pocketbase.ts
import PocketBase from "pocketbase";

import { env } from "@/lib/env";

const pocketbase = new PocketBase(env.POCKETBASE_URL);

/** @see {@link https://github.com/pocketbase/js-sdk/issues/69} */
if (typeof document !== "undefined") {
  pocketbase.authStore.loadFromCookie(document.cookie);

  pocketbase.authStore.onChange(() => {
    document.cookie = pocketbase.authStore.exportToCookie({ httpOnly: false });
  });
}

export { pocketbase };

And then, in

app/layout.tsx
async function RootLayout({ children }: { children: ReactNode }) {
  const user = await getUserFromCookie(cookies());
  console.log(user); // user is available

  if (!user) {
    redirect("/auth/sign-in");
  }

  return (
    <html lang="pt-BR" className={inter.className}>
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="initial-scale=1, width=device-width" />
      </head>

      <body>{children}</body>
    </html>
  );
}

I need your NextJS 13 SSR example so much! I’m using NextJS 13 with PocketBase. I’m very confused with how to use PocketBase for user authentication in client components and then fetching user’s data in server components.

In my project, I didn’t get and set cookies (I don’t know how to do that), and I’m not able to get user’s data after user login. It is as if the PocketBase client in the server component that performs data fetching is never aware of the PocketBase client in the client component that performs user authentication.

So what I did for my solution is the following…

  1. Run get serverSideProps whenever I want to detect the logged in state, protect a route, or get some user data
  2. If no cookie is present redirect to /login page
  3. If cookie is present run client.collection(“users”).authRefresh to get the user object, then just return the attributes I want to the client props
import Cookies from "cookies";
import { GetServerSideProps } from "next";
import client from "@services/config";

interface Data {
  firstName: string;
  lastName: string;
  id: string;
  avatar: string;
  isLoggedIn: boolean;
}

export const getServerSideProps: GetServerSideProps<{
  userData: Data;
}> = async (context) => {
  const { req, res } = context;
  const cookies = new Cookies(req, res);
  const pbCookie = cookies.get("pb_auth");

  if (!pbCookie) {
    return {
      redirect: {
        destination: "/login",
        permanent: false,
      },
    };
  }
  client.authStore.loadFromCookie(`pb_auth=${pbCookie}`);
  const data = await getUserProfile();

  const userData = {
    firstName: data.record.firstName,
    lastName: data.record.lastName,
    id: data.record.id,
    avatar: data.record.avatar,
    isLoggedIn: true,
  };

  return {
    props: {
      userData,
    },
  };
};

So the above helps me on the server and then for the client I have the following.

import PocketBase from "pocketbase";

export const client = new PocketBase(
  process.env.NEXT_PUBLIC_POCKETBASE_BASE_URL
);

typeof document !== "undefined" &&
  client.authStore.loadFromCookie(document.cookie);

client.authStore.onChange(() => {
  if (typeof document !== "undefined") {
    document.cookie = client.authStore.exportToCookie({ httpOnly: false });
  }
});

export default client;

In my case, I was working with Next.js 12 (mostly it was something I knew better) so I can’t give you a precise example. What I can say to you is that, while I was working on it, Next didn’t really append any cookie to the requests, I read someone said that it was because of the localhost but I didn’t investigate further on that. After Gani’s reply, I was able to work with the cookie as it was being exported to the server side when it changed. So maybe you can try to append that onChange method to your Pocketbase’s instance and then using the exported model for your queries, but I really don’t know how Pocketbase works in Next.js 13

Thank you for your reply! I tried a little bit and it seems to solve my current problem, which is to get data on the server side after user login. Following Gani’s suggestion, I create a new PocketBase client instance, get the cookie using the cookies().get('pb_auth') method in Next.js 13, which returns an object {name: 'pb_auth', value: '{...}'}, and then convert it to a string, then load the cookie string with authStore.loadFromCookie. Now I’m able to read data using client.records.getList. A minimal example:

// app/something/page.tsx. This is a server component. 
import PocketBase from 'pocketbase';
import { cookies } from 'next/headers';

export default async function Home() {
  const client = new PocketBase('http://127.0.0.1:8090');
  const nextCookies = cookies();
  const cookie_object = nextCookies.get('pb_auth');
  const cookie_string = cookie_object.name + '=' + cookie_object.value;
  client.authStore.loadFromCookie(cookie_string);
  const data = await client.records.getList('collection_name', 1, 100);

  return (
    <div>
    <h1>Results</h1>
    <div>
      {data.items.map(item => {
        return <SomeComponent key={item.id} item={item}/>;
      })}
    </div>
    </div>
    )
}

I’m quite new to web development and not very familiar with JS/TS, so maybe there are better ways to write the code.

@bentrynning Are you sure that authCookie.value is a raw serialized cookie string and not the token or the json object that contains the token?

Note you don’t have to use pb.authStore.loadFromCookie/exportToCookie if you are using the Next.js cookies abstraction. To load a token directly into the store you can call pb.authStore.save(token).

In any case, I’m closing this issue as it is starting to become too off-topic and there isn’t any clear actionable item here.

I’ve tried to check if anything has changed in Next.js couple months ago and unfortunately I wasn’t able to find an easy solution for sharing the SDK AuthStore state between server components and I’m reaching a point where I’d rather not recommend using PocketBase with Next.js as it feels too much work for very little benefit, at least to me.

If someone has a suggestion without having to maintain special Next.js helpers, feel free to open a PR with an example for the README.

This is really amazing work @tsensei! For the middleware.ts, /((?!api|_next/static|_next/image|favicon.ico|login|phrase|$).*), changing ^$ -> $ excluded the home page for me

thx @rafifos Your solution works great. The nextjs 13 is a hard one to play with…in comparison SvelteKit is such a better expirence…

I think there is some misunderstanding. The above will work only in a browser context because document.cookie is not available in node. If you are making the requests in the browser while only using node for server rendering then that’s OK.

But if you want to make requests from the node-server you’ll need to read and set the cookie from the ServerRequest and ServerResponse objects (or their equivalent in the new nextjs if you are using nextjs13). When using PocketBase in a server context, you need a unique PocketBase instance on each server request because node is single threaded and requests are usually executed in an event loop, meaning that while you are waiting something to execute, the same process could process another client request and if you use only a single instance the data from the new request may overwrite the state from the initial one. In the browser this is not an issue and you can have a single instance for the entire lifecycle of the application.

I understand that modern frameworks blur the line between client and server but please make sure that your code is executed where you expect it to avoid accidentally leaking sensitive information. I still haven’t got the time to explore the new api of nextjs13 and sometime after the v0.8.0 release I’ll try to test it and will add a SSR example for it in the readme.

The thing is that I can’t understand how does the cookie that loadFromCookie needs has to look like.

The default cookie entry should look something like this:

pb_auth=...encoded token and model json data...; Path=/; Expires=Thu, 27 Jun 2030 10:00:00 GMT; HttpOnly; Secure; SameSite=Strict

(the expires date should be the the same as the token “exp” claim)


The a_session… cookie is the only one that appears to be generated from the OAuth2 flow, as well as it’s created a localStorage entry with the user’s model and token.

If you have a localStorage entry, this means that you are currently handling the OAuth2 redirect in the browser. Or in other words, the PocketBase instance is running client-side. If you want to have a mixed SDK access (aka. making requests both client-side and server-side) then you’ll have to export the cookie with exportToCookie({ httpOnly: false }) and change your store to conditionally get and set the cookie either from the ServerRequest/ServerResponse objects or from document.cookie depending on what environment it is running. Alternatively the browser/client SDK instance could use the authStore.onChange listener instead of a custom store:

import PocketBase from 'pocketbase';

const client = new PocketBase("http://127.0.0.1:8090");

client.authStore.loadFromCookie(document.cookie);

client.authStore.onChange(() => {
    document.cookie = client.authStore.exportToCookie({ httpOnly: false });
})

export default client;

(the above will always update the document.cookie that will be added automatically to every request from the browser to the node-server)


Could you provide a code sample/repo of what you are trying to do?