The handbook is an Astro site with a public home page and a member route. Public copy stays on `/`. Member chapters stay on `/handbook` and render only after `getUser` accepts the session.

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

## What we'll build

Two screens, and a hard line between them.

`/` is the public handbook. It does not read the session. The title is Handbook. The body says this site proves identity only: a signed-in session is not a permission grant. When `PUBLIC_AUTHDOG_PUBLIC_KEY` is set, the page decodes that key — drop the `pk_` prefix, base64-decode the JSON — and builds a sign-in link from `identityHost` and `environmentId`. That link opens the hosted Account portal. A second link points at `/handbook`. When the key is missing, the page names the env var instead of offering a dead link.

The same page loads `initAuthdog()` from `@authdog/astro/client`. After the Account portal returns with `?token=`, that script checks the token shape, stores it in `localStorage`, strips it from the URL, and reloads. The bootstrap does not write `authdog-session`. That cookie is `HttpOnly`. A server that has already validated the token has to set it.

`/handbook` is the member route. Before any chapter HTML is sent, the page calls `createAuthdogServer().getUser(Astro.request)`. `getUser` sends the cookie to Authdog userinfo. A missing cookie, a rejected token, or a failed call resolves to `null`, and the page returns `Astro.redirect("/")`. An accepted profile renders two chapters:

- On-call rotation — who holds the pager this week.
- Incident comms — what to post in the first 15 minutes.

The list is the same for every signed-in reader. It answers who the reader is. It does not decide what they may do. Apply [authorization](/docs/concepts/authorization) after `getUser`.

Five files draw that line.

`astro.config.mjs` sets `output: "server"` and the Node standalone adapter. Frontmatter that calls userinfo never runs in a static build.

`src/middleware.ts` wraps every request with `authdogMiddleware`. It copies the raw `authdog-session` cookie onto `Astro.locals.authdog.session` and sets `isAuthenticated` when that cookie is non-empty. `/` stays reachable, including when the cookie is stale. `src/env.d.ts` types `App.Locals.authdog` so the rest of the app can read that object.

`src/pages/handbook.astro` ignores the middleware boolean and uses `getUser` as the gate. The chapter list is the first markup after that check.

`src/pages/api/logout.ts` exposes `GET /api/logout`. `authdog.logout` clears the cookie and redirects. The next visit to `/handbook` fails `getUser` and returns home.

## What you need

| Requirement | Detail |
| --- | --- |
| Public key | `pk_...` from the [console](https://console.authdog.com) |
| Environment | Copy `.env.example` to `.env` and set `PUBLIC_AUTHDOG_PUBLIC_KEY` |
| Secret key | Never put `sk_...` in client code |
| Return URL | Account portal back to a member route. Local: `http://localhost:4321/handbook` |
| Runtime | Astro `^5` or `^6`, with SSR. `@authdog/astro/server` needs `output: "server"` or `"hybrid"` |

## Run

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

Open the URL Astro prints, usually http://localhost:4321.

With no session, `/` shows the handbook title, the identity note, and the sign-in link. `/handbook` redirects home. After sign-in, `/handbook` lists On-call rotation and Incident comms. Sign out from `/api/logout`, then open `/handbook` again: the redirect returns.

## Server

`getUser` runs in page frontmatter, against the incoming request. A static build never sees that request.

```js astro.config.mjs
import { defineConfig } from "astro/config"
import node from "@astrojs/node"

export default defineConfig({
  output: "server",
  adapter: node({ mode: "standalone" }),
})
```

## Middleware

`authdogMiddleware` runs on every request and populates `Astro.locals.authdog`. `isAuthenticated` means the `authdog-session` cookie exists. It does not validate the token. Public pages stay reachable.

```ts src/middleware.ts
import { defineMiddleware } from "astro:middleware"
import { authdogMiddleware } from "@authdog/astro/server"

export const onRequest = defineMiddleware(
  authdogMiddleware({
    publicKey: import.meta.env.PUBLIC_AUTHDOG_PUBLIC_KEY ?? "",
  }),
)
```

Declare the locals type so `Astro.locals.authdog` type-checks:

```ts src/env.d.ts
declare namespace App {
  interface Locals {
    authdog: import("@authdog/astro/server").AuthdogLocals
  }
}
```

## Public home

The home page never calls `getUser`. The sign-in URL comes from the public key. `initAuthdog()` only handles the `?token=` return in the browser.

```astro src/pages/index.astro
---
const publicKey = import.meta.env.PUBLIC_AUTHDOG_PUBLIC_KEY ?? ""
let signinUri: string | null = null
if (publicKey) {
  try {
    const payload = JSON.parse(
      Buffer.from(publicKey.replace("pk_", ""), "base64").toString(),
    )
    signinUri = `${payload.identityHost}/signin/${payload.environmentId}`
  } catch {
    signinUri = null
  }
}
---

<h1>Handbook</h1>
<p>
  This site proves identity only. A signed-in session is not a permission
  grant.
</p>
{
  signinUri ? (
    <p>
      <a href={signinUri}>Sign in</a>
      {" · "}
      <a href="/handbook">Member chapters</a>
    </p>
  ) : (
    <p>
      Set <code>PUBLIC_AUTHDOG_PUBLIC_KEY</code> in <code>.env</code>.
    </p>
  )
}

<script>
  import { initAuthdog } from "@authdog/astro/client"
  initAuthdog()
</script>
```

## Member chapters

`getUser` validates the cookie through userinfo. The redirect happens in frontmatter, so an anonymous reader never receives the chapter list.

```astro src/pages/handbook.astro
---
import { createAuthdogServer } from "@authdog/astro/server"

const CHAPTERS = [
  { title: "On-call rotation", summary: "Who holds the pager this week." },
  { title: "Incident comms", summary: "What to post in the first 15 minutes." },
]

const authdog = createAuthdogServer({
  publicKey: import.meta.env.PUBLIC_AUTHDOG_PUBLIC_KEY ?? "",
})

const profile = await authdog.getUser(Astro.request).catch(() => null)
if (!profile) {
  return Astro.redirect("/")
}
---

<ul>
  {CHAPTERS.map((chapter) => (
    <li>
      {chapter.title}. {chapter.summary}
    </li>
  ))}
</ul>
```

## Sign out

`GET /api/logout` clears `authdog-session` and redirects. The following request to `/handbook` fails `getUser` and returns home.

```ts src/pages/api/logout.ts
import type { APIRoute } from "astro"
import { createAuthdogServer } from "@authdog/astro/server"

const authdog = createAuthdogServer({
  publicKey: import.meta.env.PUBLIC_AUTHDOG_PUBLIC_KEY ?? "",
})

export const GET: APIRoute = ({ request }) => authdog.logout(request)
```
