Server Side Sessions
@frend-digital/centra supports SSR based GEO location and session management. This is managed using the server side auth() hook
auth()
The Auth hook is a server side only request hook that handles session management and GEO location. If your project was bootstrapped using @frend-digital/cli then this middleware has already been configured for you. You can skip to RSC & Route Handlers, or read on to get an understanding of how it works.
It supports TWO call signatures:
-
Middleware -
auth(request, response)- This is the full signature, it is meant to be used in the
middleware.tsfile and executed only once. In this mode it will perform a GEO location request to Centra and set a session cookie on the client.
- This is the full signature, it is meant to be used in the
-
RSC -
auth()- The request less mode is meant to be used within React Server Components (RSC) or Route Handlers. This mode is a return only mode, it will not perform a GEO location request and will only parse and return the session cookie as data.
Ideally, both modes are used to produce accurate product results based on the user’s location.
Middleware
In your Next.js project, call the auth() middleware in your middleware.ts file.
import { auth } from "@frend-digital/centra/next";
export const middleware = (request: NextRequest) => { const response = NextResponse.next();
await auth(request, response);
return response;};This will automatically grab the x-real-ip header and forward it to Centra for GEO location. Once the GEO location has been determined, a session cookie will be set on the client in the form of a JWT. The cookie displays the following information:
centra-access-token for a Swedish visitor:
{ "token": "xxxxxxxxxxxxx", "country": { "country": "SE", "name": "Sweden", "eu": true, "shipTo": true, "language": "en", "currency": "SEK", "market": "1", "pricelist": "19" }, "exp": 1728043567, "iat": 1725451567}RSC and Route Handlers
In your RSC, Server Action or Route Handler, call the auth() hook to retreive the current user session:
import { auth } from "@frend-digital/centra/next";import { notFound } from "next/navigation";
export default async function ProductPage({ params }) { const userdata = await auth(); if (!userdata) notFound();
const { err, val: product } = await centra.fetchProductByUri(params.uri, { market: userdata.country.market, pricelist: userdata.country.pricelist, });
if (err || !product) notFound();
return ( <main> <h1>{product.name}</h1> <p>{product.description}</p> <p>Price: {product.price}</p> </main> );}"use server";import { auth } from "@frend-digital/centra/next";
export async function fetchProductsIds(ids: string[]) { const userdata = await auth(); if (!userdata) return [];
const { err, val: products } = await centra.fetchProducts({ products: ids, market: userdata.country.market, pricelist: userdata.country.pricelist, });
if (err || !products) return [];
return products}import { auth } from "@frend-digital/centra/next";
import { notFound } from "next/navigation";
export async function GET(request, { params }) {const userdata = await auth();if (!userdata) notFound();
const { err, val: product } = await centra.fetchProductByUri(params.uri, { market: userdata.country.market, pricelist: userdata.country.pricelist, });
if (err || !product) notFound();
return Response.json(product);
}In the above example, we server render a product page. The product is fetched based on its URI and the user’s country and pricelist. If the product is unavailable in the user’s market/pricelist we will return a 404.
Partial Pre Rendering Encouraged
PPR (Partial Pre Rendering) is a concept in Next.js that allows you to render a page completely static until the nearest Suspense boundary. In a real world product page, only the price is truly dependent on the user’s location. This means that we can pre-render the product page completely statically, leverage Suspense to Stream in the price and then hydrate the page.
import { auth } from "@frend-digital/centra/next";import { notFound } from "next/navigation";
const Price = async ({ product }) => { const userdata = await auth(); if (!userdata) notFound();
const pricelist = userdata.country.pricelist; const userPrice = product.prices[pricelist];
// User's price is not available in the pricelist, // return a 404 as the product is likely restricted to a specific market. if (!userPrice) notFound();
return <p>Price: {userPrice.price}</p>;};
export default async function ProductPage({ params }) { const { err, val: product } = await centra.fetchProductByUri(params.uri, { pricelist: "all", });
if (err || !product) notFound();
return ( <main> <h1>{product.name}</h1> <p>{product.description}</p> <Suspense fallback={<p>Loading...</p>}> <Price product={product} /> </Suspense> </main> );}In the above example, we pre-render the product page, cache it on the CDN and then hydrate the price using Suspense. This is all a single request.
Flow
This is the flow chart of the entire rendering process:
User: {icon: https://www.svgrepo.com/show/508195/user.svg}
NextJS: {icon: https://www.svgrepo.com/show/306466/next-dot-js.svg
Middleware: { Auth: { shape: stored_data label: "auth()" icon: https://www.svgrepo.com/show/521137/fingerprint.svg }
Auth -> SetSession: "LocationData"}
RenderPage: { Auth: { shape: stored_data label: "auth()" icon: https://www.svgrepo.com/show/521137/fingerprint.svg }
Auth <-> GetSession: "LocationData"}}
Request: {shape: page}
Centra: {GetProducts: { label: "POST /products"}GeoLocation: { label: "POST /countries/auto"}# Response -> SSRResponse: "Return Products Data"}
User -> RequestRequest -> NextJS.MiddlewareNextJS.Middleware.SetSession -> Request: "Set-Cookie"NextJS.Middleware.Auth -> Centra.GeoLocation: "Token Exists = NO"Centra.GeoLocation -> NextJS.Middleware.Auth: "LocationData"
NextJS.Middleware -> NextJS.RenderPage: "Request"
NextJS.RenderPage -> Centra.GetProducts: "Fetch Products with Session"Centra.GetProducts -> NextJS.RenderPage
NextJS.RenderPage -> User: "Response"