Implement SaaSKit Next.js
Add login, callback, a scalekit_session cookie, logout, and refresh in the App Router. Then stop.
Guardrails
- MUST re-check the session in Server Components. Middleware cookie-presence is not enough.
- MUST keep
redirectUriidentical to the dashboard Allowed callback URL. - MUST set
sameSite: 'lax'on every auth cookie. MUST NOT use'strict'— it drops the cookie on the OAuth callback. - MUST treat
nextas a relative path only (/..., not//…). MUST NOT redirect to an arbitrary URL.
Gotchas
setup-saaskitalready wrote env and registered the redirect. Start there.- Read
SCALEKIT_ENVIRONMENT_URL,SCALEKIT_CLIENT_ID,SCALEKIT_CLIENT_SECRET, andSCALEKIT_REDIRECT_URI. NeverSCALEKIT_ENV_URL. Do not prependhttps://. - One
scalekit_sessionJSON HttpOnly cookie. Do not copy Express path-scoped cookies fromimplement-saaskit. ScalekitClientis Node-only. Do not import it inmiddleware.ts(Edge).- FSA scopes include
offline_access.refreshAccessTokenreturns{ accessToken, refreshToken }only. getLogoutUrl({ idTokenHint, postLogoutRedirectUri }). Register that post-logout URL.
Step 1 — Pick the path
- App Router → stay here.
- Pages Router or not Next.js → name
implement-saaskit. Stop. - Django, FastAPI, or Flask → name
implement-saaskit-python. Stop.
If env is missing, collect the four values from app.scalekit.com → Developers → Settings → API Credentials. Register SCALEKIT_REDIRECT_URI under Authentication → Redirect URLs → Allowed callback URLs. Do not invent values.
Done when: this skill is the right path, and the four env names exist.
Step 2 — Init the SDK and session cookie
Install @scalekit-sdk/node only when the repo has no Scalekit SDK yet. Do not add jose or cookie-parser.
// lib/scalekit.ts
import { ScalekitClient } from '@scalekit-sdk/node';
export const scalekit = new ScalekitClient(process.env.SCALEKIT_ENVIRONMENT_URL, process.env.SCALEKIT_CLIENT_ID, process.env.SCALEKIT_CLIENT_SECRET);
export const redirectUri = process.env.SCALEKIT_REDIRECT_URI;
// lib/session.ts — one scalekit_session JSON cookie
import { cookies } from 'next/headers';
const NAME = 'scalekit_session';
const opts = { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const, path: '/' };
export async function getSession() {
try { return JSON.parse((await cookies()).get(NAME)?.value ?? ''); } catch { return null; }
}
export async function setSession(data: object) {
(await cookies()).set(NAME, JSON.stringify(data), { ...opts, maxAge: 30 * 24 * 60 * 60 });
}
export async function clearSession() { (await cookies()).delete(NAME); }
Done when: the client initializes from those env vars, and the app can read, write, and clear scalekit_session.
Step 3 — Login
app/auth/login/route.ts — GET. Link to /auth/login. Do not router.push to Scalekit.
import { NextResponse } from 'next/server';
import { scalekit, redirectUri } from '@/lib/scalekit';
export async function GET(request: Request) {
const next = new URL(request.url).searchParams.get('next');
const dest = next?.startsWith('/') && !next.startsWith('//') ? next : '/dashboard';
const state = crypto.randomUUID();
const res = NextResponse.redirect(scalekit.getAuthorizationUrl(redirectUri, {
state, scopes: ['openid', 'profile', 'email', 'offline_access'],
}));
const cookie = { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const, path: '/', maxAge: 600 };
res.cookies.set('oauth_state', state, cookie);
res.cookies.set('oauth_next', dest, cookie);
return res;
}
Done when: /auth/login redirects to that URL.
Step 4 — Callback
Put this route at the path in SCALEKIT_REDIRECT_URI. Default is /auth/callback → app/auth/callback/route.ts. Same redirectUri as Step 3.
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { scalekit, redirectUri } from '@/lib/scalekit';
import { setSession } from '@/lib/session';
export async function GET(request: Request) {
const url = new URL(request.url);
const jar = await cookies();
const next = jar.get('oauth_next')?.value ?? '';
const dest = next.startsWith('/') && !next.startsWith('//') ? next : '/dashboard';
if (!url.searchParams.get('code') || url.searchParams.get('state') !== jar.get('oauth_state')?.value) {
return NextResponse.redirect(new URL('/auth/login', url.origin));
}
const { user, idToken, accessToken, refreshToken, expiresIn } =
await scalekit.authenticateWithCode(url.searchParams.get('code')!, redirectUri);
await setSession({ user, idToken, accessToken, refreshToken, expiresAt: Math.floor(Date.now() / 1000) + expiresIn });
const res = NextResponse.redirect(new URL(dest, url.origin));
res.cookies.delete('oauth_state');
res.cookies.delete('oauth_next');
return res;
}
Done when: the callback exchanges code, writes scalekit_session, and the browser leaves /auth/callback.
Step 5 — Logout
app/auth/logout/route.ts — GET. Link to /auth/logout. Register the origin as a Post logout URL (local default http://localhost:3000).
import { NextResponse } from 'next/server';
import { scalekit } from '@/lib/scalekit';
import { getSession, clearSession } from '@/lib/session';
export async function GET(request: Request) {
const session = await getSession();
await clearSession();
return NextResponse.redirect(scalekit.getLogoutUrl({
idTokenHint: session?.idToken, postLogoutRedirectUri: new URL(request.url).origin,
}));
}
Done when: scalekit_session is gone and the browser hits the logout URL.
Step 6 — Refresh
manage-saaskit-sessions is Express path-scoped cookies. Stay here. app/auth/refresh/route.ts — POST.
import { NextResponse } from 'next/server';
import { scalekit } from '@/lib/scalekit';
import { getSession, setSession } from '@/lib/session';
export async function POST() {
const session = await getSession();
if (!session?.refreshToken) return NextResponse.json({ error: 'no session' }, { status: 401 });
try {
const { accessToken, refreshToken } = await scalekit.refreshAccessToken(session.refreshToken);
await setSession({ ...session, accessToken, refreshToken, expiresAt: Math.floor(Date.now() / 1000) + 240 });
return NextResponse.json({ ok: true });
} catch {
return NextResponse.json({ error: 'refresh failed' }, { status: 401 });
}
}
Default path: if expiresAt is past, a Client Component runs fetch('/auth/refresh', { method: 'POST' }) and retries. Failed → /auth/login. Keep 401 only for a later /api route.
Done when: a past expiresAt POSTs /auth/refresh, and a failed refresh returns 401.
Step 7 — Protect pages
Put middleware.ts at the project root. It runs first. Check cookie presence only. Do not import ScalekitClient.
import { NextResponse, type NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;
if (path.startsWith('/dashboard') && !request.cookies.get('scalekit_session')) {
const login = new URL('/auth/login', request.url);
login.searchParams.set('next', path);
return NextResponse.redirect(login);
}
return NextResponse.next();
}
Then re-check: const session = await getSession(); if (!session?.accessToken) redirect('/auth/login'); If session.expiresAt is past, a Client Component POSTs /auth/refresh and retries.
Done when: a missing cookie hits /auth/login, the Server Component reads getSession(), and a past expiresAt refreshes.
Step 8 — Stop
Do not write Express cookie-parser middleware or Python auth.
Done when: login, callback, scalekit_session, logout, and refresh are in the App Router, and this skill has stopped.
Reach for
setup-saaskitif env or the redirect URI is missingimplement-saaskitfor Node, Express, or Pages Routerimplement-saaskit-pythonfor Django, FastAPI, or Flask
Live lookups
- Docs index: https://docs.scalekit.com/llms.txt
- Auth flow: https://docs.scalekit.com/authenticate/fsa/quickstart/
- MCP: https://mcp.scalekit.com