> ## Documentation Index
> Fetch the complete documentation index at: https://help.pixwel.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Documentation Authentication

> How the Papervine documentation site is gated behind the platform login — a cookie-authenticated EdDSA JWT redirect handshake, with role-based page access.

The documentation site is gated behind the platform login. When an unauthenticated visitor hits a private docs page, Papervine redirects them to a minting endpoint on the API, which **authenticates them from their session cookie**, signs a short-lived JWT, and redirects them back to the docs — already signed in. No SPA round-trip; it's a plain server-side redirect.

## The handshake

```mermaid theme={null}
sequenceDiagram
    participant U as Browser
    participant P as Papervine (docs.pixwel.com)
    participant A as API (platform.pixwel.com/api/session/docs)
    U->>P: GET /features/work-requests (no docs session)
    P->>A: 302 /api/session/docs?redirect=%2Ffeatures%2Fwork-requests
    Note over U,A: the browser carries the platform session cookie
    A->>A: authenticate from cookie, mint EdDSA JWT
    A->>P: 302 /login/jwt-callback?redirect=…#JWT
    P->>U: verify JWT (public key) → land on /features/work-requests
```

The JWT rides in the URL **fragment** (`#…`), never a query param, so the token stays out of server logs — Papervine reads it client-side.

## Why a redirect works

A top-level browser redirect doesn't carry the SPA's `Authorization` header, but it **does** carry cookies, and the API authenticates from those:

| Session        | Cookie                   | Authenticated on a redirect?                                                                  |
| -------------- | ------------------------ | --------------------------------------------------------------------------------------------- |
| Legacy / token | `pixwel_api_credentials` | yes — read by the `Http` auth adapter                                                         |
| Clerk          | `__session`              | yes — `Session::docs()` verifies it with the Clerk key when there's no `Authorization` header |

If the visitor has no valid session cookie, the endpoint redirects to the platform (`pixwel_host`) so they can sign in.

## The pieces

| Component  | Path                                     | Role                                                                                                                       |
| ---------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| **Minter** | `api/controllers/Session.php` → `docs()` | `GET /session/docs?redirect=…` — authenticates from cookie, signs the JWT, and **302-redirects** to the Papervine callback |

No new API route or frontend page is needed — the catch-all `/{:controller}/{:action}` dispatches `/session/docs` to `Session::docs`.

## The token

Signed **EdDSA (Ed25519)** with `firebase/php-jwt`. Claims:

| Claim       | Value                                                                   |
| ----------- | ----------------------------------------------------------------------- |
| `host`      | the docs domain (`docs.pixwel.com`); must match or Papervine rejects it |
| `exp`       | `now + 5s` — the handshake window only (Papervine requires ≤ 10s)       |
| `expiresAt` | `now + 8h` — the docs session length                                    |
| `groups`    | `["admin"]` or `["user"]`, from `$user->is(GroupsModel::ADMIN)`         |
| `email`     | the user's email, for personalization                                   |

The `redirect` path is validated (must be site-relative) to prevent an open redirect.

## Configuration

Point Papervine's JWT login URL at the minter, and set two environment variables (both issued from the Papervine dashboard):

| Setting                     | Value                                                  |
| --------------------------- | ------------------------------------------------------ |
| Papervine login URL         | `https://platform.pixwel.com/api/session/docs`         |
| `papervine_jwt_private_key` | base64 Ed25519 private key (also accepts a PKCS#8 PEM) |
| `papervine_docs_host`       | the docs domain, e.g. `docs.pixwel.com`                |

If the key or host is unset, the endpoint returns `503 docs_auth_unconfigured`.

## Role-based pages

The `groups` claim drives per-page visibility. A page restricted to a group uses frontmatter:

```mdx theme={null}
---
title: "Slurpee3"
groups: ["admin"]
---
```

It's an allow-list — a user outside the listed groups gets a **404**, so the page is fully hidden, not just unlinked. Pages with no `groups` are visible to anyone past the docs login. The entire **Engineering** tab is tagged `groups: ["admin"]`.

<Warning>
  Group gating only activates once authentication is enabled — until then the markers are inert. Conversely, once auth is live, a page with a `groups` requirement 404s for any user without a matching group.
</Warning>

## Setup checklist

<Steps>
  <Step title="Enterprise plan">
    Papervine JWT auth requires an Enterprise plan.
  </Step>

  <Step title="Generate the keypair">
    In the Papervine dashboard, generate the EdDSA keypair and set the JWT login URL to `https://platform.pixwel.com/api/session/docs`. Store the private key as `papervine_jwt_private_key`.
  </Step>

  <Step title="Serve on a subdomain">
    Host the docs at `docs.pixwel.com` — auth isn't supported on a path basepath like `pixwel.com/docs`.
  </Step>

  <Step title="Gate pages">
    Engineering pages already carry `groups: ["admin"]`; add the marker to any other page that should be admin-only.
  </Step>
</Steps>
