The team dashboard lists project status for the signed-in member. The public home only links to sign-in and `/dashboard`.

Clone [starters/nextjs](https://github.com/authdog/samples/tree/main/starters/nextjs). SDK reference: [Next.js](/docs/frameworks/nextjs).

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](/docs/backend), then apply [authorization](/docs/concepts/authorization).

## What you need

| Requirement | Detail |
| --- | --- |
| Public key | `pk_...` from the [console](https://console.authdog.com) |
| 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

```bash
npm install
npm run dev
```

Open 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.

```ts
// 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)
}
```

```ts
// 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.

```tsx
// 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.
