The team dashboard lists project status for the signed-in member. The public home only links to sign-in and /dashboard.
Clone starters/nextjs. SDK reference: Next.js.
The project list answers who the member is. It does not grant a permission. Validate again on Route Handlers and Server Actions with a backend SDK, then apply authorization.
What you need
| Requirement | Detail |
|---|---|
| Public key | pk_... from the console |
| Environment | Copy .env.example to .env.local and set PK_AUTHDOG and NEXT_PUBLIC_PK_AUTHDOG to that same key |
| Secret key | Never put sk_... in the app |
| Return URL | Account portal back to a URL this app serves. Local: http://localhost:3000/dashboard |
Run
npm install
npm run devOpen http://localhost:3000. Sign in, then open /dashboard. You should see Billing, Mobile, and Docs with a status, plus Signed in as and an email from userinfo. Without a session the page says Not signed in.
Callback
useAuthMiddleware from @authdog/nextjs-app does not load in the Next.js 15 Edge runtime. This example forwards ?token= to a Node route that runs the same userinfo exchange and writes the same user_session_* cookies.
// middleware.ts
import { NextResponse, type NextRequest } from "next/server"
export function middleware(request: NextRequest) {
const token = request.nextUrl.searchParams.get("token")
if (!token || request.nextUrl.pathname === "/auth/callback") {
return NextResponse.next()
}
const url = request.nextUrl.clone()
url.pathname = "/auth/callback"
url.searchParams.set("next", request.nextUrl.pathname)
return NextResponse.redirect(url)
}// app/auth/callback/route.ts
import {
fetchUserData,
isAuthenticatedUserInfo,
sanitizeRedirectPath,
} from "@authdog/node-commons"
import { getServerSidePayloadPublicKey } from "@authdog/nextjs-app/server"
import { NextResponse, type NextRequest } from "next/server"
export async function GET(request: NextRequest) {
const token = request.nextUrl.searchParams.get("token")
const nextPath = sanitizeRedirectPath(
request.nextUrl.searchParams.get("next"),
"/dashboard",
)
const dest = new URL(nextPath, request.url)
if (token) dest.searchParams.set("token", token)
const publicKey = process.env.PK_AUTHDOG
if (!token || !publicKey) {
return NextResponse.redirect(dest)
}
const { identityHost, environmentId } =
getServerSidePayloadPublicKey(publicKey)
const userData = await fetchUserData(identityHost, environmentId, token)
const response = NextResponse.redirect(dest)
if (isAuthenticatedUserInfo(userData)) {
const options = {
maxAge: 60 * 60 * 24 * 7,
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax" as const,
}
response.cookies.set({
name: `user_session_${environmentId}`,
value: JSON.stringify(userData.user),
...options,
})
response.cookies.set({
name: `user_session_hash_${environmentId}`,
value: token,
...options,
})
}
return response
}The callback does not guard later requests.
Projects
useUser calls userinfo. The project rows render only after that call returns a user. useAuth only reports that a browser token exists. Do not use it to authorize.
// app/dashboard/page.tsx
"use client"
import { useUser } from "@authdog/nextjs-app"
const PROJECTS = [
{ name: "Billing", status: "On track" },
{ name: "Mobile", status: "At risk" },
{ name: "Docs", status: "On track" },
]
export default function DashboardPage() {
const { user, isLoading } = useUser()
if (isLoading) return <p>Loading…</p>
if (!user) return <p>Not signed in</p>
return (
<ul>
{PROJECTS.map((project) => (
<li key={project.name}>
{project.name} · {project.status}
</li>
))}
</ul>
)
}The provider stores ?token= after a JWT-shape check only. Real validation happens at userinfo.