# Next.js · App Router

> Dónde va el SDK en el App Router, y por qué el límite servidor–cliente decide casi todo.


```bash
npm install @andco/sdk @andco/sdk-react
```

En el App Router la pregunta no es «cómo uso el SDK», es **de qué lado**. Hay dos integraciones distintas y conviene elegir una a conciencia.

- **Sesión en el navegador**: la persona autoriza desde el cliente, la sesión vive en el navegador y tu servidor no la ve. Es lo más simple y sirve para leer datos de la propia persona.
- **Sesión en el servidor**: tu backend conduce OAuth, guarda la sesión en tu propia cookie y llama a Andco desde el servidor. Es lo que necesitas si tu backend tiene que actuar por cuenta de la persona.

## Sesión en el navegador

El provider es un Client Component, así que se monta en un archivo con `"use client"` y se usa desde el layout.

```tsx title="app/providers.tsx"
"use client";

import { AndcoProvider } from "@andco/sdk-react";
import type { AndcoBrowserOptions } from "@andco/sdk/browser";
import type { ReactNode } from "react";

const clientOptions: AndcoBrowserOptions = {
  clientId: process.env.NEXT_PUBLIC_ANDCO_CLIENT_ID!,
  redirectTo: process.env.NEXT_PUBLIC_APP_ORIGIN!,
  initialScopes: ["openid", "email", "profile"],
};

export function Providers({ children }: { children: ReactNode }) {
  return <AndcoProvider clientOptions={clientOptions}>{children}</AndcoProvider>;
}
```

```tsx title="app/layout.tsx"
import { Providers } from "./providers";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="es">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}
```

Cualquier Client Component por debajo ya puede leer la sesión:

```tsx title="app/account/page.tsx"
"use client";

import { useAndcoSession } from "@andco/sdk-react";

export default function Account() {
  const session = useAndcoSession();
  if (session === undefined) return <p>Cargando…</p>;
  return session ? <p>{session.user.email}</p> : <p>Sin sesión</p>;
}
```

### Evitar el parpadeo al renderizar en el servidor

Si el servidor ya sabe quién es la persona, entrégale esa sesión al provider. Sin esto el servidor renderiza «sin resolver», el cliente renderiza lo mismo, y la pantalla salta cuando el almacén termina de cargar.

En este modelo la sesión solo existe en el navegador, así que primero hay que dejársela al servidor en una cookie propia. `initialSession` no la lee de ahí por ti: solo la recibe.

```ts title="app/lib/session-cookie.ts"
"use client";

import type { AndcoSession } from "@andco/sdk";

const COOKIE_NAME = "andco_session";

/** Mirrors the session into a plain cookie, so a Server Component can read it for `initialSession`. */
export function writeSessionCookie(session: AndcoSession | null): void {
  if (!session) {
    document.cookie = `${COOKIE_NAME}=; path=/; max-age=0`;
    return;
  }
  const maxAge = Math.max(0, session.expiresAt - Math.floor(Date.now() / 1000));
  document.cookie = `${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(session))}; path=/; max-age=${maxAge}; samesite=lax`;
}
```

No es `httpOnly`: quien la escribe es el propio navegador, así que no protege nada que `sessionStorage` no exponga ya. Es una pista para el primer render, no una frontera de autorización — el servidor nunca la usa para llamar a Andco por cuenta de la persona en este modelo.

```tsx title="app/providers.tsx"
"use client";

import { AndcoProvider, useAndco } from "@andco/sdk-react";
import type { AndcoBrowserOptions, AndcoSession } from "@andco/sdk";
import { type ReactNode, useEffect } from "react";
import { writeSessionCookie } from "./lib/session-cookie";

const clientOptions: AndcoBrowserOptions = {
  clientId: process.env.NEXT_PUBLIC_ANDCO_CLIENT_ID!,
  redirectTo: process.env.NEXT_PUBLIC_APP_ORIGIN!,
  initialScopes: ["openid", "email", "profile"],
};

export function Providers({ children, initialSession }: { children: ReactNode; initialSession?: AndcoSession | null }) {
  return (
    <AndcoProvider clientOptions={clientOptions} initialSession={initialSession}>
      <SessionCookieSync />
      {children}
    </AndcoProvider>
  );
}

/** Keeps the cookie in sync every time the session changes, so the next SSR render has it. */
function SessionCookieSync() {
  const andco = useAndco();
  // Vuelve a suscribirse cuando cambia la Identidad autorizada, dentro del mismo commit: la
  // cancelación y el alta ocurren juntas, así que no hay ventana en la que se pierda un evento.
  useEffect(() => andco.auth.onChange(writeSessionCookie), [andco]);
  return null;
}
```

```ts title="app/lib/session-cookie.server.ts"
import "server-only";
import { cookies } from "next/headers";
import type { AndcoSession } from "@andco/sdk";

const COOKIE_NAME = "andco_session";

/** Reads the cookie the client wrote. `null` when there is none, or it fails to parse. */
export async function sessionFromCookie(): Promise<AndcoSession | null> {
  const raw = (await cookies()).get(COOKIE_NAME)?.value;
  if (!raw) return null;
  try {
    return JSON.parse(decodeURIComponent(raw)) as AndcoSession;
  } catch {
    return null;
  }
}
```

```tsx title="app/layout.tsx"
import { Providers } from "./providers";
import { sessionFromCookie } from "./lib/session-cookie.server";

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="es">
      <body>
        <Providers initialSession={await sessionFromCookie()}>{children}</Providers>
      </body>
    </html>
  );
}
```

Con esto el primer render del servidor ya conoce la sesión, `useAndcoSession` arranca con el mismo valor en el cliente, y `SessionCookieSync` mantiene la cookie al día para la próxima carga.

## Sesión en el servidor

La instancia de servidor se crea una vez y se comparte. No guarda sesión: cada petición liga la suya.

```ts title="app/lib/andco.ts"
import "server-only";
import { createAndcoInstanceForServer } from "@andco/sdk/server";

export const andco = createAndcoInstanceForServer({
  clientId: process.env.ANDCO_CLIENT_ID!,
  clientSecret: process.env.ANDCO_CLIENT_SECRET!,
});
```

`import "server-only"` no es decoración: es lo que convierte en error de compilación el día en que alguien importe este archivo desde un Client Component y arrastre el secreto al bundle.

### Iniciar la autorización

```ts title="app/api/andco/authorize/route.ts"
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { andco } from "~/app/lib/andco";

export async function GET() {
  const { data: request, error } = await andco.oauth.createAuthorizationRequest({
    redirectTo: `${process.env.APP_ORIGIN}/api/andco/callback`,
    scopes: ["openid", "email", "profile"],
  });
  if (error) throw error;

  (await cookies()).set("andco_tx", JSON.stringify({ ...request, authorizationUrl: request.authorizationUrl.href }), {
    httpOnly: true,
    sameSite: "lax",
    secure: true,
    maxAge: 600,
  });

  redirect(request.authorizationUrl.href);
}
```

### Recibir el callback

```ts title="app/api/andco/callback/route.ts"
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import { andco } from "~/app/lib/andco";

export async function GET(request: Request) {
  const cookieStore = await cookies();
  const raw = cookieStore.get("andco_tx")?.value;
  if (!raw) return NextResponse.json({ error: "missing transaction" }, { status: 400 });

  const stored = JSON.parse(raw);
  const { data: session, error } = await andco.oauth.exchangeCallback({
    callbackUrl: new URL(request.url),
    request: { ...stored, authorizationUrl: new URL(stored.authorizationUrl) },
  });
  cookieStore.delete("andco_tx");
  if (error) return NextResponse.json({ error: error.code }, { status: 400 });

  await saveToYourSession(session);
  return NextResponse.redirect(new URL("/", request.url));
}
```

### Leer datos desde un Server Component

```tsx title="app/accounts/page.tsx"
import { andco } from "~/app/lib/andco";

export default async function Accounts() {
  const session = await sessionFromYourStore();
  if (!session) return <p>Sin sesión</p>;

  const { data, error } = await andco.with(session).rest.http.GET("/accounts");
  if (error) return <p>No se pudieron leer las cuentas</p>;

  return <ul>{data.data.map((account) => <li key={account.id}>{account.name}</li>)}</ul>;
}
```

`with()` devuelve un cliente ligado a esa credencial y no muta la instancia compartida, que es lo que impide que el token de una petición aparezca en otra.

## Caché

Las lecturas autenticadas no se deben cachear entre personas. Si una ruta llama a Andco por cuenta de alguien, márcala dinámica:

```ts
export const dynamic = "force-dynamic";
```
