Skip to main content

Getting started - Pages Router

This guide walks you through the steps to enable Kraken authentication in your Next.js app. When you're done, you'll know how the different parts of the package work together in order to provide a seamless authentication experience for your users.

Learn more

This guide only covers the basic setup of the @krakentech/blueprint-auth package. Functions mentioned in this guide have more options, please refer to the API reference for more information. Functions are also annotated with JSDoc, providing in-editor documentation.

What You'll Build

By the end of this guide, you'll have:

  • A working login/logout flow
  • Protected routes using Next.js middleware
  • Session management with server-side authentication
  • Authenticated GraphQL queries
Learn more

This guide only covers the basic setup of the @krakentech/blueprint-auth package, check out our authentication guides to implement additional features.

Functions mentioned in this guide have more options, please refer to the API reference for more information. Functions are also annotated with JSDoc, providing in-editor documentation.

Requirements

The @krakentech/blueprint-auth package requires:

  • next@^14.2.25 || ^15.0.0 || ^16.0.0,
  • react@^18.2.0 || ^19.1.0,
  • react-dom@^18.2.0 || ^19.1.0

If your project uses unsupported versions, make sure you upgrade them before proceeding.

Installation

Install the @krakentech/blueprint-auth package and its peer dependencies.

Peer dependencies

The @krakentech/blueprint-auth package relies on:

  • @krakentech/blueprint-api@^3.3.0,
  • @tanstack/react-query@^5.29.2,
  • graphql-request@^7.2.0,
  • @vercel/edge-config@^1.1.0 (optional: required to enable organization-scoped authentication).
pnpm add @krakentech/blueprint-auth @krakentech/blueprint-api @tanstack/react-query graphql-request
Private packages

Packages of the @krakentech NPM organization are published privately to the NPM registry. To install these packages, you need to configure your package manager to use a Kraken issued NPM access token. Please get in touch if you need one.

Configuration

Use the createAuthConfig factory to create a centralized configuration object that can be reused throughout your application. Check out the API Reference to learn more about the available configuration options.

Environment variables

Create a .env.local file in the root of your project and define the following environment variables:

.env.local
KRAKEN_GRAPHQL_ENDPOINT="Kraken GraphQL endpoint URL"
KRAKEN_X_CLIENT_IP_SECRET_KEY="Client IP secret key"
Environment variables

The use of environment variables is strongly recommended for supported options. When deploying to Vercel, configure these environment variables in your project settings for preview and production deployments.

Configuration object

Create a configuration object that defines the API routes and app routes for your authentication flow. The paths in apiRoutes must match the actual API route files you'll create in the next section.

@/lib/auth/config.ts
import { createAuthConfig } from "@krakentech/blueprint-auth";

export const authConfig = createAuthConfig({
apiRoutes: {
login: "/api/auth/login",
logout: "/api/auth/logout",
graphql: { kraken: "/api/graphql/kraken" },
session: "/api/auth/session",
},
appRoutes: {
dashboard: { pathname: "/dashboard" },
login: { pathname: "/login" },
},
});

Middleware

Create a Next.js middleware (version ≤15) or proxy (version ≥16) using the createAuthMiddleware factory.

middleware.ts
import { createAuthMiddleware } from "@krakentech/blueprint-auth/middleware";
import { authConfig } from "@/lib/auth/config";

export const middleware = createAuthMiddleware(authConfig);

export const config = {
matcher: ["/dashboard/:path*", "/login"],
};
Customizing matchers

The matcher array determines which routes the middleware protects. See Next.js middleware matcher docs for advanced patterns.

Customize the matcher

The matcher configuration determines which routes are protected by the authentication middleware. Adjust the paths based on your application's routing structure. Learn more about middleware matchers in the Next.js documentation.

API routes

You need to create 4 different API routes to handle the authentication flow. We recommend using the following structure:

  • pages/api/auth/login.ts
  • pages/api/auth/logout.ts
  • pages/api/auth/session.ts
  • pages/api/graphql/kraken.ts

Login API handler

Create the login API handler using createLoginHandler.

pages/api/auth/login.ts
import { createLoginHandler } from "@krakentech/blueprint-auth/server";
import { authConfig } from "@/lib/auth/config";

export default createLoginHandler(authConfig);

Logout API handler

Create the logout API handler using createLogoutHandler.

pages/api/auth/logout.ts
import { createLogoutHandler } from "@krakentech/blueprint-auth/server";
import { authConfig } from "@/lib/auth/config";

export default createLogoutHandler(authConfig);

Session API handler

Create the session API handler using createSessionHandler.

pages/api/auth/session.ts
import { createSessionHandler } from "@krakentech/blueprint-auth/server";

export default createSessionHandler();

GraphQL API handler

Create the GraphQL API handler using createGraphQLHandler.

pages/api/graphql/kraken.ts
import { createGraphQLHandler } from "@krakentech/blueprint-auth/server";
import { authConfig } from "@/lib/auth/config";

export default createGraphQLHandler(authConfig);

Client

The package provides React hooks to handle the authentication flow, as well as a context provider in order to make configuration available across your app.

Context providers

Context providers

The React hooks created using createClientSideAuth rely on the AuthProvider for configuration, and QueryClientProvider from @tanstack/react-query for data fetching.

Wrap your entire application with AuthProvider and QueryClientProvider.

pages/_app.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { AppProps } from "next/app";
import { useState } from "react";
import { AuthProvider } from "@/lib/auth/client";

export default function App({ Component, pageProps }: AppProps) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</QueryClientProvider>
);
}

Client Functions

Create the client-side functions using createClientSideAuth.

lib/auth/client.ts
import { createClientSideAuth } from "@krakentech/blueprint-auth/client";
import { authConfig } from "./config";

export const {
AuthProvider,
useGraphQLClient,
useKrakenAuthErrorHandler,
useLogin,
useLogout,
useSession,
} = createClientSideAuth(authConfig, "kraken");

Server Side Rendering

In order to enable Server Side Rendering of pages depending on GraphQL data, you need to create the getUserScopedGraphQLClient Server Function using the createServerSideAuth factory.

lib/auth/server.ts
import { createServerSideAuth } from "@krakentech/blueprint-auth/server";
import { authConfig } from "./config";

export const { getUserScopedGraphQLClient, prefetchSession } =
createServerSideAuth(authConfig);

Recipes

This section provides examples of how to use the authentication hooks in your application.

Login form

Create a login form using the useLogin hook.

components/LoginForm.tsx
import { useLogin } from "@/lib/auth/client";

export function LoginForm() {
const login = useLogin();

return (
<form
method="POST"
onSubmit={(event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const email = formData.get("email") as string;
const password = formData.get("password") as string;

login.mutate({ email, password });
}}
>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
</div>

<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" required />
</div>

{login.error && (
<p role="alert">Login failed. Please check your credentials.</p>
)}

<button type="submit" disabled={login.isPending}>
{login.isPending ? "Logging in..." : "Login"}
</button>
</form>
);
}
Form method

Make sure the method attribute of the form is set to POST, otherwise user credentials will be appended to the URL as search parameters if the form is submitted before the page is hydrated.

Advanced form patterns

For advanced form patterns including validation, error handling, and accessibility best practices, see the Building Forms tutorial. You can also use form libraries like Conform, Formik, or React Hook Form.

Logout button

Create a logout button using the useLogout hook.

components/LogoutButton.tsx
import { useLogout } from "@/lib/auth/client";

export function LogoutButton() {
const logout = useLogout();

return (
<button onClick={() => logout.mutate()} disabled={logout.isPending}>
Logout
</button>
);
}

Supporting Internationalization (i18n)

If your application supports multiple languages with localized URLs, configure the auth package to use your i18n library's pathname translation.

Next.js Native i18n vs Auth Config

The Pages Router has native i18n support for locale detection and prefixing (e.g., /fr/dashboard). However, the auth package's i18n config is needed when you use translated route segments (e.g., /fr/tableau-de-bord instead of /fr/dashboard).

Configuration with next-intl

lib/auth/config.ts
import { createAuthConfig } from "@krakentech/blueprint-auth";
import { getPathname } from "@/i18n/navigation";
import { hasLocale } from "next-intl";
import { routing } from "@/i18n/routing";

export const authConfig = createAuthConfig({
appRoutes: {
/* ... */
},
i18n: {
localeCookie: "NEXT_LOCALE",
getLocalizedPathname({ locale, pathname }) {
const validLocale = hasLocale(routing.locales, locale)
? locale
: routing.defaultLocale;
return getPathname({ locale: validLocale, href: pathname });
},
},
// ... other config
});

See the next-intl documentation for setup instructions.

How It Works

When configured:

  • Middleware: Matches routes using localized pathnames
  • Redirects: Sends users to localized destinations after login/logout
  • Error Handling: Redirects to localized login pages on auth errors
Comprehensive Guide

For detailed examples including URL-based locale detection, simple path prefix patterns, and how to use with Next.js native i18n, see the i18n guide.

Using session data

Create a component rendering different UI based on the user's authentication status using the useSession hook.

components/Header.tsx
import { useSession } from "@/lib/auth/client";
import { Logo } from "@/components/Logo";
import { LogoutButton } from "@/components/LogoutButton";
import { MasqueradeIcon } from "@/icons/MasqueradeIcon";
import { NavigationMenu } from "@/components/NavigationMenu";
import { UserMenu } from "@/components/UserMenu";

export function Header() {
const {
data: { isAuthenticated, authMethod },
} = useSession();

return (
<header>
<NavigationMenu />
<Logo />
{authMethod === "masquerade" && <MasqueradeIcon />}
{isAuthenticated ? <LogoutButton /> : <UserMenu />}
</header>
);
}

Prefetching session data

You can prefetch the session data on the server by using the prefetchSession function. This will ensure session data is available on the client-side as soon as the page loads.

Server-side rendering

This is only relevant in pages using both the useSession hook, and server-side rendering with getServerSideProps.

pages/_app.tsx
import type { AppProps } from "next/app";
import { useState } from "react";
import {
type DehydratedState,
HydrationBoundary,
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import { AuthProvider } from "@/lib/auth/client";

export default function App({
Component,
pageProps,
}: AppProps<{ dehydratedState: DehydratedState }>) {
const [queryClient] = useState(() => {
return new QueryClient({
defaultOptions: {
queries: { staleTime: 60 * 1000 },
},
});
});

return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={pageProps.dehydratedState}>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</HydrationBoundary>
</QueryClientProvider>
);
}
pages/subscribe.tsx
import type { GetServerSidePropsContext } from "next";
import { QueryClient, dehydrate } from "@tanstack/react-query";
import { prefetchSession } from "@/lib/auth/server";
import { useSession } from "@/lib/auth/client";

export function SubscribePage() {
const {
data: { isAuthenticated },
} = useSession();

if (!isAuthenticated) {
return <NewUserSubscribeForm />;
}

return <ExistingUserSubscribeForm />;
}

export async function getServerSideProps(context: GetServerSidePropsContext) {
const queryClient = new QueryClient();

await prefetchSession({
context,
queryClient,
});

return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
}

GraphQL queries in getServerSideProps

pages/dashboard/accounts/[accountNumber].tsx
import { dehydrate, QueryClient } from "@tanstack/react-query";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
} from "next";
import * as z from "zod";
import { getUserScopedGraphQLClient, prefetchSession } from "@/lib/auth/server";
import { graphql } from "@/lib/graphql";
import { ErrorMessage } from "@/components/ErrorMessage";
import { PropertyCard } from "@/components/PropertyCard";

export default function AccountPage({
account,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<div>
<h1>Account #{account.number}</h1>
<h2>Properties</h2>
{account.properties.map((property) => (
<PropertyCard key={property.id} property={property} />
))}
</div>
);
}

export const AccountPageQuery = graphql(`
query AccountPageQuery($accountNumber: String!) {
account(accountNumber: $accountNumber) {
number
properties {
id
address
}
}
}
`);

const accountSchema = z.object({
number: z.string().min(1),
properties: z.array(
z.object({ address: z.string().min(1), id: z.string().min(1) }),
),
});

export async function getServerSideProps(
context: GetServerSidePropsContext<{ accountNumber: string }>,
) {
const accountNumber = context.params?.accountNumber ?? "";
const graphqlClient = getUserScopedGraphQLClient({ context });
const { account } = await graphqlClient.request(AccountPageQuery, {
accountNumber,
});

return {
props: {
account: accountSchema.parse(account),
},
};
}
Data validation

GraphQL queries tend to type data as nullable by default, validating GraphQL response data using a data validation library can help ensure the data received from the GraphQL server matches the expected format.

Error handling

The example voluntary omits error handling for brevity. Make sure errors are properly handled, this can be done at the page level in getServerSideProps, or globally by adding an error boundary in _app.tsx.

GraphQL queries with @tanstack/react-query

This example uses gql.tada for type-safe GraphQL queries. The graphql function and VariablesOf type are exported from your project's GraphQL setup.

queries/account.ts
import type { AuthenticatedGraphQLClient } from "@krakentech/blueprint-auth";
import { queryOptions, useQuery } from "@tanstack/react-query";
import type { GraphQLClient } from "graphql-request";
import { graphql, type VariablesOf } from "@/lib/graphql";
import { useGraphQLClient } from "@/lib/auth/client";

export const AccountQuery = graphql(`
query AccountPageQuery($accountNumber: String!) {
account(accountNumber: $accountNumber) {
number
properties {
id
address
}
}
}
`);

const accountSchema = z.object({
number: z.string().min(1),
properties: z.array(
z.object({ address: z.string().min(1), id: z.string().min(1) }),
),
});

export function accountQueryOptions(
graphQLClient: GraphQLClient | AuthenticatedGraphQLClient,
variables: VariablesOf<typeof AccountQuery>,
) {
return queryOptions({
enabled: Boolean(variables.accountNumber),
queryKey: ["account", variables],
queryFn: async () => {
const { account } = await graphQLClient.request(AccountQuery, variables);
return accountSchema.parse(account);
},
});
}

export function useAccount(variables: VariablesOf<typeof AccountQuery>) {
const { graphQLClient } = useGraphQLClient();
return useQuery(accountQueryOptions(graphQLClient, variables));
}
pages/accounts/[accountNumber].tsx
import { useRouter } from "next/router";
import { ErrorMessage } from "@/components/ErrorMessage";
import { PropertyCard } from "@/components/PropertyCard";
import { useAccount } from "@/queries/account";

export default function AccountPage() {
const router = useRouter();
const {
data: account,
isLoading,
error,
} = useAccount({ accountNumber: String(router.query.accountNumber) });

if (isLoading) {
return <p>Loading...</p>;
}

if (error) {
return <ErrorMessage error={error} />;
}

return (
<div>
<h1>Account #{account.number}</h1>
<h2>Properties</h2>
{account.properties.map((property) => (
<PropertyCard key={property.id} property={property} />
))}
</div>
);
}
Prefetching with React Query

Queries made with @tanstack/react-query can be prefetched on the server. Check out our Prefetching session data recipe, or @tanstack/react-query's documentation to learn how to enable prefetching.

Learn more

Building on top of the example above, you can prefetch the account data by defining a getServerSideProps function like this:

pages/accounts/[accountNumber].tsx
import { QueryClient, dehydrate } from "@tanstack/react-query";
import { getUserScopedGraphQLClient } from "@/lib/auth/server";
import { accountQueryOptions } from "@/queries/account";

export default function AccountPage() {
// ...
}

export async function getServerSideProps(
context: GetServerSidePropsContext<{ accountNumber: string }>,
) {
const accountNumber = context.params?.accountNumber ?? "";
const queryClient = new QueryClient();
const graphqlClient = getUserScopedGraphQLClient({ context });

await Promise.all([
queryClient.prefetchQuery(
accountQueryOptions(graphqlClient, { accountNumber }),
),
prefetchSession({
context,
queryClient,
}),
]);

return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
}

Next Steps

Now that you've set up basic authentication, explore these advanced features:

For detailed information about all available functions, types, and configuration options, see the API Reference.