Skip to content

Wrapper for Next.js applications using PocketBase, with support for both client and server components

License

Notifications You must be signed in to change notification settings

g12i/next-pocketbase-auth

Folders and files

NameName
Last commit message
Last commit date

Latest commit

cf264d6 · Jan 3, 2025

History

21 Commits
Dec 3, 2024
Nov 24, 2024
Nov 24, 2024
Jan 3, 2025
Nov 25, 2024
Dec 3, 2024
Nov 25, 2024
Jan 3, 2025
Jan 3, 2025
Nov 24, 2024
Nov 24, 2024

Repository files navigation

next-pocketbase-auth

A lightweight authentication wrapper for Next.js applications using PocketBase, providing easy-to-use utilities for handling user session in both client and server components.

Installation

npm install next-pocketbase-auth

Security Considerations

Don't rely solely on pb.authStore.record inside server code such as middleware. It isn't guaranteed to revalidate the Auth token.

Always use await pb.collection("users").authRefresh(); to protect pages and user data.

Use following code to safely obtain information about the current user:

const pb = createServerClient(await cookies());

const { record: user } = await pb.collection("users").authRefresh();

Setup Guide

1. Environment Variables

Add your PocketBase URL to your environment variables:

NEXT_PUBLIC_PB_URL=http://127.0.0.1:8090/api/

2. Initialize PocketBase Clients

You'll need two different clients for client-side and server-side operations:

Client Component

For Client Components:

import { createBrowserClient } from "next-pocketbase-auth";

const pb = createBrowserClient();

createBrowserClient uses a singleton pattern, so you only ever create one instance, no matter how many times you call your createClient function.

Server Components

For Server Components, Server Actions, and Route Handlers::

import { createServerClient } from "next-pocketbase-auth";
import { cookies } from "next/headers";

const pb = createServerClient(await cookies());

3. Configure Middleware

The middleware is essential for maintaining authentication state across your application.

It handles:

  • Automatic token refresh
  • Cookie management
  • Authentication state persistence

Create a middleware.ts file in your project root (or in the src/ folder if that's where your code is).

// middleware.ts

import { createServerClient } from "next-pocketbase-auth";
import { NextResponse, type NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
  const response = NextResponse.next({ request });

  const pb = createServerClient({
    get: (name) => request.cookies.get(name),
    set: (name, value, opts) => {
      request.cookies.set(name, value);
      response.cookies.set(name, value, opts);
    },
    delete: (name) => {
      request.cookies.delete(name);
      response.cookies.delete(name);
    },
  });

  // If we have a valid token, refresh the token
  try {
    if (pb.authStore.isValid) await pb.collection("users").authRefresh();
  } catch {
    // If we can't refresh the token, clear the cookies
    pb.authStore.clear();
  }

  // If we have a user, continue
  if (pb.authStore.record) return response;

  // If we are already on the login page, continue
  if (request.nextUrl.pathname === "/login") return response;

  // Redirect to the login page
  const url = request.nextUrl.clone();
  url.pathname = "/login";
  return NextResponse.redirect(url);
}

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico, sitemap.xml, robots.txt (metadata files)
     */
    "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
  ],
};

4. Login Page

You can uses regular PocketBase's SDK authentication functions see here, as long as you:

  • create the client with createBrowserClient() function
  • refresh the page (or redirect) once the authentication is finished

For example to login with GitHub:

export function LoginForm(): React.ReactElement {
  const [submitError, setSubmitError] = useState<string>("");
  const router = useRouter();

  const handleGitHubLogin = async () => {
    try {
      setSubmitError("");

      await pb.collection("users").authWithOAuth2({ provider: "github" });

      router.push("/");
    } catch {
      setSubmitError("An unexpected error occurred");
    }
  };

  return (
    <Button variant="outline" className="w-full" onClick={handleGitHubLogin}>
      <GitHubLogoIcon className="mr-2" />
      Login with GitHub
    </Button>
  );
}

5. Access user info from Server Components

import { to } from "await-to-js";
import { createServerClient } from "next-pocketbase-auth";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

export default async function AccountPage() {
  const pb = createServerClient(await cookies());
  const [error, result] = await to(pb.collection("users").authRefresh());

  if (error) {
    redirect("/login");
  }

  return <p>Hello {result.record.name}</p>;
}

Common Patterns

Mixed Public/Private Routes

For applications with both public and protected routes the middleware can stop at refreshing the auth token if it's present:

// middleware.ts

import { createServerClient } from "next-pocketbase-auth";
import { NextResponse, type NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
  const response = NextResponse.next({ request });

  const pb = createServerClient({
    get: (name) => request.cookies.get(name),
    set: (name, value, opts) => {
      request.cookies.set(name, value);
      response.cookies.set(name, value, opts);
    },
    delete: (name) => {
      request.cookies.delete(name);
      response.cookies.delete(name);
    },
  });

  // If we have a valid token, refresh the token
  try {
    if (pb.authStore.isValid) await pb.collection("users").authRefresh();
  } catch {
    // If we can't refresh the token, clear the cookies
    pb.authStore.clear();
  }

  return response;
}

Using with TypeScript

Add type safety using pocketbase-typegen package.

Use it as:

import { createServerClient, createBrowserClient } from "next-pocketbase-auth";
import { TypedPocketBase } from "./lib/pb.generated";

const pb = createServerClient<TypedPocketBase>();
const pb2 = createBrowserClient<TypedPocketBase>();

API

createBrowserClient<T extends PocketBase = PocketBase>(baseUrl?: string, lang?: string, cookieOptions?: CookieOptions): T

Creates a PocketBase client instance for use in client components.

Parameters:

  • baseUrl - (optional) PocketBase API URL (defaults to process.env.NEXT_PUBLIC_PB_URL or http://127.0.0.1:8090/api/)
  • lang - (optional) language code passed to PocketBase client
  • cookieOptions - (optional) options to set cookies (see below)

Returns:

  • PocketBase client instance

createServerClient<T extends PocketBase = PocketBase>(cookies: CookiesAdapter, baseUrl?: string, lang?: string, cookieOptions?: CookieOptions): T

Creates a PocketBase client instance for use in server components.

Parameters:

  • cookies - required server cookies (see examples)
  • baseUrl - (optional) PocketBase API URL (defaults to process.env.NEXT_PUBLIC_PB_URL or http://127.0.0.1:8090/api/)
  • lang - (optional) language code passed to PocketBase client
  • cookieOptions - (optional) options to set cookies (see below)

Returns:

  • PocketBase client instance
CookieOptions

By default the cookie is set with following settings:

export const defaultCookieOptions: CookieOptions = {
  httpOnly: false,
  secure: process.env.NODE_ENV === "production",
  sameSite: "strict" as const,
  path: "/",
  expires: new Date(Date.now() + 86400), // will be set to the token expiration date, this is safe default of 24 hours
};

About

Wrapper for Next.js applications using PocketBase, with support for both client and server components

Resources

License

Stars

Watchers

Forks