Skip to main content

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:

OptionTypeRequiredDescription
baseUrlstringBase URL of the Syntropy Auth service, e.g. https://auth.syntropy.chat
clientIdstringOAuth client ID registered in the dashboard
clientSecretstringOAuth client secret (keep server-side only)
redirectUristringMust match a redirect URI registered in the dashboard. Defaults to <origin><callbackPath>
secretstring32-byte random string used for AES-GCM session encryption
cookieNamestringCookie name for storing the session. Defaults to "syntropy_session"
loginPathstringPath that triggers a login redirect. Defaults to /auth/login
callbackPathstringPath that receives the OAuth callback. Defaults to /api/auth/callback/syntropy
publicPathsstring[]Paths that do not require authentication (everything else is protected). Supports trailing * wildcards
scopesstring[]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.

Dashboard checklist

Before your Next.js app can complete an auth flow, make sure these are set in the Syntropy dashboard under Settings → Auth Clients → Configure:

  1. Redirect URI registered — must exactly match redirectUri above (including scheme and port)
  2. Allowed origin registered — the origin your Next.js app runs on (e.g. https://your-product.example.com or http://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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

Auth0Syntropy Auth
auth0.getSession()getSession({ secret })
session.user.subsession.user.sub
withMiddlewareAuthRequiredsyntropyAuthMiddleware({ ..., publicPaths: [...] })
auth0.handleAuth() route handlerHandled by middleware automatically

The session shape is compatible; both expose sub, email, and name.