Next.js Adapter
@eclosion-tech/syntropy-auth-next provides everything needed to add Syntropy Auth to a Next.js App Router project: middleware for the OAuth flow, encrypted session cookies, and server-side helpers.
npm install @eclosion-tech/syntropy-auth-next
Configuration reference
All options are passed to syntropyAuthMiddleware:
| Option | Type | Required | Description |
|---|---|---|---|
baseUrl | string | ✓ | Base URL of the Syntropy Auth service, e.g. https://auth.syntropy.chat |
clientId | string | ✓ | OAuth client ID registered in the dashboard |
clientSecret | string | ✓ | OAuth client secret (keep server-side only) |
redirectUri | string | Must match a redirect URI registered in the dashboard. Defaults to <origin><callbackPath> | |
secret | string | ✓ | 32-byte random string used for AES-GCM session encryption |
cookieName | string | Cookie name for storing the session. Defaults to "syntropy_session" | |
loginPath | string | Path that triggers a login redirect. Defaults to /auth/login | |
callbackPath | string | Path that receives the OAuth callback. Defaults to /api/auth/callback/syntropy | |
publicPaths | string[] | Paths that do not require authentication (everything else is protected). Supports trailing * wildcards | |
scopes | string[] | OAuth scopes to request. Defaults to ["openid", "profile", "email", "org", "offline_access"] |
Logout is handled automatically at /auth/logout. Pass a ?returnTo= query parameter to control where the user lands after the session is cleared.
Before your Next.js app can complete an auth flow, make sure these are set in the Syntropy dashboard under Settings → Auth Clients → Configure:
- Redirect URI registered — must exactly match
redirectUriabove (including scheme and port) - Allowed origin registered — the origin your Next.js app runs on (e.g.
https://your-product.example.comorhttp://localhost:3000). Without this, browser requests to the auth service will be blocked by CORS.
Middleware setup
// middleware.ts
import { syntropyAuthMiddleware } from "@eclosion-tech/syntropy-auth-next/middleware";
export const middleware = syntropyAuthMiddleware({
baseUrl: process.env.SYNTROPY_AUTH_URL!,
clientId: process.env.SYNTROPY_AUTH_CLIENT_ID!,
clientSecret: process.env.SYNTROPY_AUTH_CLIENT_SECRET!,
redirectUri: process.env.SYNTROPY_AUTH_REDIRECT_URI!,
secret: process.env.SYNTROPY_AUTH_SESSION_SECRET!,
publicPaths: ["/", "/pricing", "/blog/*"],
});
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
The middleware runs at the Edge and handles:
- Login redirect — When an unauthenticated user hits a protected path, they are redirected to the Syntropy Auth login page. PKCE verifier, state, and next-URL are stored in short-lived cookies.
- Callback — On return from Syntropy Auth, the middleware exchanges the authorization code for tokens, encrypts the session into a cookie, and redirects the user to their original destination.
- Token refresh — On each request, if the stored access token is within 5 minutes of expiry and a refresh token exists, the middleware silently refreshes the tokens and updates the session cookie.
- Logout — The session cookie is cleared and the user is redirected to Syntropy Auth's end-session endpoint.
Server-side helpers
Import from @eclosion-tech/syntropy-auth-next (the main entry point):
getSession({ secret, cookieName? })
Returns the current session or null if unauthenticated. Must be called from a Server Component, Server Action, or Route Handler.
import { getSession } from "@eclosion-tech/syntropy-auth-next";
export default async function Page() {
const session = await getSession({
secret: process.env.SYNTROPY_AUTH_SESSION_SECRET!,
});
if (!session) return <p>Please log in</p>;
return <p>Hello, {session.user.email}</p>;
}
requireSession({ secret, cookieName? })
Returns the current session or throws an error if unauthenticated. Use in pages and layouts that are already behind middleware protection and should never render without a session.
import { requireSession } from "@eclosion-tech/syntropy-auth-next";
export default async function ProtectedPage() {
const session = await requireSession({
secret: process.env.SYNTROPY_AUTH_SESSION_SECRET!,
});
// session is always defined here
return <p>Welcome, {session.user.name}</p>;
}
Session object
interface SyntropySession {
user: {
sub: string; // stable user identifier — store this in your DB
email: string;
email_verified: boolean;
name?: string;
preferred_username?: string;
given_name?: string;
family_name?: string;
updated_at?: number;
org?: {
id: string; // owner UUID (org or project)
pool_id: string; // user pool isolation boundary
owner_type: "org" | "project";
name: string; // human-readable pool name
};
};
accessToken: string;
refreshToken?: string;
idToken?: string;
expiresAt: number; // Unix timestamp (seconds)
}
Calling protected APIs with the access token
Pass the stored access token to your own API routes or to Syntropy services:
import { getSession } from "@eclosion-tech/syntropy-auth-next";
export async function GET() {
const session = await getSession({
secret: process.env.SYNTROPY_AUTH_SESSION_SECRET!,
});
if (!session) return Response.json({ error: "Unauthorized" }, { status: 401 });
const data = await fetch("https://api.your-service.com/data", {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
return Response.json(await data.json());
}
Linking a Syntropy Auth user to your database record
On first login you will want to store the Syntropy Auth user ID alongside your own user record:
// lib/sync-user.ts
import { db } from "@/lib/db";
import { usersTable } from "@/db/schema";
import type { SyntropySession } from "@eclosion-tech/syntropy-auth-next";
export async function syncUser(session: SyntropySession) {
const existing = await db.query.usersTable.findFirst({
where: eq(usersTable.syntropyAuthId, session.user.sub),
});
if (!existing) {
await db.insert(usersTable).values({
syntropyAuthId: session.user.sub,
email: session.user.email,
name: session.user.name ?? null,
});
}
}
Call syncUser(session) in a Server Component or layout that runs after authentication.
Migrating from Auth0
If you are replacing Auth0, the swap is straightforward:
| Auth0 | Syntropy Auth |
|---|---|
auth0.getSession() | getSession({ secret }) |
session.user.sub | session.user.sub |
withMiddlewareAuthRequired | syntropyAuthMiddleware({ ..., publicPaths: [...] }) |
auth0.handleAuth() route handler | Handled by middleware automatically |
The session shape is compatible; both expose sub, email, and name.