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.
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
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.
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
- npm
- yarn
- bun
pnpm add @krakentech/blueprint-auth @krakentech/blueprint-api @tanstack/react-query graphql-request
npm install @krakentech/blueprint-auth @krakentech/blueprint-api @tanstack/react-query graphql-request
yarn add @krakentech/blueprint-auth @krakentech/blueprint-api @tanstack/react-query graphql-request
bun add @krakentech/blueprint-auth @krakentech/blueprint-api @tanstack/react-query graphql-request
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:
KRAKEN_GRAPHQL_ENDPOINT="Kraken GraphQL endpoint URL"
KRAKEN_X_CLIENT_IP_SECRET_KEY="Client IP secret key"
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.
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.
- Next.js ≤15
- Next.js ≥16
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"],
};
The matcher array determines which routes the middleware protects. See Next.js middleware matcher docs for advanced patterns.
import { createAuthMiddleware } from "@krakentech/blueprint-auth/middleware";
import { authConfig } from "@/lib/auth/config";
export const proxy = createAuthMiddleware(authConfig);
export const config = {
matcher: ["/dashboard/:path*", "/login"],
};
The matcher array determines which routes the proxy protects. See Next.js proxy matcher docs for advanced patterns.
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.tspages/api/auth/logout.tspages/api/auth/session.tspages/api/graphql/kraken.ts
Login API handler
Create the login API handler using
createLoginHandler.
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.
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.
import { createSessionHandler } from "@krakentech/blueprint-auth/server";
export default createSessionHandler();
GraphQL API handler
Create the GraphQL API handler using
createGraphQLHandler.
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
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.
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.
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.
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.
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>
);
}
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.
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.
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.
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
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
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.
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.
This is only relevant in pages using both the useSession hook, and server-side
rendering with getServerSideProps.
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>
);
}
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
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),
},
};
}
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.
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.
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));
}
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>
);
}
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:
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:
- Organization-Scoped Authentication - Enable organization-scoped authentication
- Masquerade - Allow staff to impersonate users for support purposes
- Anonymous Authentication - Provide pre-authenticated access using scoped tokens
- Kraken OAuth - Enable OAuth-based authentication
- Building Forms - Learn advanced form patterns including validation, error handling, and accessibility best practices
For detailed information about all available functions, types, and configuration options, see the API Reference.