Next.js SDK
Package: @loomup/next (SDK repository path: packages/next).
Builds on @loomup/client for App Router and Pages Router apps.
Session model
Loomup Next uses Next-owned HttpOnly cookies on your app origin:
| Cookie | Purpose |
|---|---|
loomup-access | JWT access token (same name as @loomup/astro) |
loomup-refresh | Refresh token |
Requests to Loomup use the Authorization: Bearer header. Middleware and server clients read cookies, then attach the access token.
This is not the same as Loomup server auth.cookie_mode (which sets loomup_access / loomup_refresh with underscores on the Loomup origin). Prefer the Next cookie approach for cross-origin setups (e.g. Next on :3001, Loomup on :3000). Do not enable both modes on the same browser origin without a clear plan.
Install
cd packages/client && npm install && npm run build
cd ../next && npm install && npm run build
# in your Next app
npm install @loomup/client @loomup/next
# or file: paths during monorepo development
Environment
LOOMUP_URL=http://127.0.0.1:3000
NEXT_PUBLIC_LOOMUP_URL=http://127.0.0.1:3000
App Router setup
Auth routes
Use createAuthRouteHandlers so login/register set HttpOnly cookies:
// app/api/auth/login/route.ts
import { createAuthRouteHandlers } from "@loomup/next";
const handlers = createAuthRouteHandlers({
url: process.env.LOOMUP_URL!,
});
export async function POST(request: Request) {
return handlers.login(request);
}
Provide the same for register, logout, refresh, and GET session. Full example: examples/next-todo.
The session and login responses may include access_token in JSON so Client Components can hydrate an in-memory Bearer token for REST + realtime (refresh remains HttpOnly).
Middleware
// middleware.ts
import { NextResponse, type NextRequest } from "next/server";
import { updateSession } from "@loomup/next";
export async function middleware(request: NextRequest) {
return updateSession(request, {
url: process.env.LOOMUP_URL!,
createResponse: () => NextResponse.next({ request }),
});
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
updateSession decodes the access JWT exp when possible and calls Loomup POST /auth/refresh if the token is missing or near expiry (default skew 60s). Rotated tokens are written onto the middleware response cookies.
Server Components
import { cookies } from "next/headers";
import { createServerClient } from "@loomup/next";
export default async function Page() {
const client = await createServerClient({
url: process.env.LOOMUP_URL!,
cookies,
});
// REST only — do not call subscribe() on the server
const { data } = await client.from("todos").select({ limit: 20 });
return /* ... */;
}
Token rotation during a request (401 → refresh) invokes onTokens, which calls cookies().set. That works in Route Handlers and Server Actions; pure Server Components may not be able to mutate cookies mid-render — rely on middleware for proactive refresh.
Object storage (server)
Server clients already expose client.storage (same as @loomup/client). Helpers for Route Handlers:
// app/api/upload/route.ts
import { cookies } from "next/headers";
import {
createServerClient,
uploadFromFormData,
storageDownloadResponse,
} from "@loomup/next";
export async function POST(request: Request) {
const client = await createServerClient({
url: process.env.LOOMUP_URL!,
cookies,
});
const me = await client.auth.me();
const meta = await uploadFromFormData(
client,
"avatars",
await request.formData(),
{ pathPrefix: `${me.id}/`, upsert: true },
);
return Response.json({ data: meta });
}
export async function GET(request: Request) {
const client = await createServerClient({
url: process.env.LOOMUP_URL!,
cookies,
});
const path = new URL(request.url).searchParams.get("path")!;
return storageDownloadResponse(client, "avatars", path);
}
Or call the client directly:
await client.storage.from("avatars").upload("u/a.png", file, {
contentType: file.type,
upsert: true,
});
Requires server [storage].enabled = true and bucket rules in loomup.toml. See storage.md.
Client Components + realtime
"use client";
import { createBrowserClient } from "@loomup/next";
const client = createBrowserClient({
url: process.env.NEXT_PUBLIC_LOOMUP_URL!,
accessToken, // from server props or /api/auth/session
});
const unsub = await client.from("todos").subscribeReady((ev) => {
console.log(ev.op, ev.data);
});
Optional: wrap the tree in LoomupProvider / useLoomup from @loomup/next.
Pages Router
import type { GetServerSideProps } from "next";
import { createPagesServerClient } from "@loomup/next";
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const client = createPagesServerClient({
url: process.env.LOOMUP_URL!,
context: ctx,
});
if (!client.accessToken) {
return { redirect: { destination: "/login", permanent: false } };
}
const { data } = await client.from("todos").select({ limit: 20 });
return { props: { todos: data } };
};
Auth can use pages/api/* handlers that call the same createAuthRouteHandlers methods (login, logout, …) with the Fetch Request API, or manually set cookies via createPagesServerClient + auth.signIn.
Core client hook
@loomup/client exposes onTokens and setSession so adapters can persist sessions:
createClient({
url,
onTokens: (tokens) => {
// tokens === null on signOut
},
});
Security
- Keep refresh tokens HttpOnly (default cookie helpers do this).
- Use
Securein production (NODE_ENV=productionsets Secure by default). SameSite=Laxis the default.- Exclude static assets from the middleware matcher.
- Prefer auth through your Next origin (route handlers) rather than storing refresh tokens in
localStorage.
Example
See examples/next-todo.