Authdog

Team dashboard

Last updated Oct 7, 2026
View as Markdown

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

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