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

# Next.js SDK

> @pylonsync/next — server-side helpers, edge proxy, cookie-based auth for Next.js App Router.

`@pylonsync/next` is the Next.js-specific layer on top of `@pylonsync/react`. It provides:

* **`createPylonServer`** — server-side fetch helpers (`pylon.requireAuth()`, `pylon.getMe()`, `pylon.json()`) that forward the user's session cookie
* **`createPylonClient`** — a same-origin client fetcher for Client Components that respects Next's request lifecycle
* **`createPylonProxy`** — middleware that gates protected routes on session presence so the UI doesn't flash before redirect
* **`@pylonsync/next/auth`** — cookie-aware sign-up / sign-in / OAuth flows

Requires the App Router on Next.js 16+ (the package's peer-dependency floor); Pages Router users can still use [`@pylonsync/react`](/clients/react) directly.

The fastest path to a Next.js + Pylon project is `npm create @pylonsync/pylon` — pick the `todo` (or any) template with `--platforms web` and you get a working App Router setup with all of this wired up. The rest of this page documents the pieces so you can integrate Pylon into an existing Next.js app.

## Install

```sh theme={null}
bun add @pylonsync/next
# or: npm i @pylonsync/next
```

`@pylonsync/sdk` + `@pylonsync/react` install transitively.

## 1. Configure the backend URL

Set `PYLON_TARGET` to the Pylon backend's origin. In dev it defaults to `http://localhost:4321` (the `pylon dev` default), so you only need to set it explicitly when the backend lives elsewhere.

```env filename=".env.local" theme={null}
PYLON_TARGET=https://api.example.com
```

On Vercel, add `PYLON_TARGET` in your project's Environment Variables for both `Production` and `Preview` environments — point it at your Pylon Cloud project's URL (`https://pylon-<slug>.fly.dev` for the default hostname, or your custom domain).

See [Deploying to Vercel](/operations/vercel) for the full checklist.

## 2. Server helpers — `createPylonServer`

Build a single `pylon` server-helper that every Server Component, Route Handler, or Server Action imports from.

```ts filename="src/lib/pylon.ts" theme={null}
import { createPylonServer } from "@pylonsync/next";

export const pylon = createPylonServer({
  // Match what your Pylon backend sets. Pylon emits `${app_name}_session`
  // by default — pass that exact name. There's no implicit default
  // because picking the wrong name silently breaks auth in production.
  cookieName: "myapp_session",
  // Optional — overrides PYLON_TARGET if both are set.
  target: process.env.PYLON_TARGET ?? "http://localhost:4321",
  // Where to redirect when requireAuth() finds no session.
  loginUrl: "/login",
});
```

Now use it from any server context:

```tsx filename="app/dashboard/page.tsx" theme={null}
import { pylon } from "@/lib/pylon";

type User = { id: string; email: string; displayName: string };
type Post = { id: string; title: string };

export default async function DashboardPage() {
  // 401 → redirect to /login automatically. Returns
  // { auth: { userId, tenantId, isAdmin, ... }, user: User }
  // — uses your getMe server function for the User row.
  const { user } = await pylon.requireMe<User>();
  // Forwards the user's session cookie to the Pylon backend.
  const posts = await pylon.json<Post[]>("/api/entities/Post");
  return (
    <div>
      <h1>Welcome, {user.displayName}</h1>
      <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
    </div>
  );
}
```

Useful methods on the returned `pylon`:

| Method                 | Returns                                                                | Use it for                                                      |
| ---------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------- |
| `requireAuth()`        | `PylonAuth` (`{userId, tenantId, isAdmin, cookieHeader}`) or redirects | Gate Server Components / Actions when you only need the user id |
| `getAuth()`            | `PylonAuth \| null`                                                    | Read auth without redirecting                                   |
| `requireMe<U>()`       | `{ auth, user }` (or redirects)                                        | Gate + load the user row via your `getMe` function              |
| `getMe<U>()`           | `{ auth, user } \| null`                                               | Same, without redirect                                          |
| `json<T>(path, init?)` | parsed JSON `T`                                                        | Call any Pylon API with the user's session                      |
| `fetch(path, init?)`   | raw `Response`                                                         | When you need headers, streams, status codes                    |

All paths are relative to the configured `target` and the session cookie rides along automatically.

## 3. Client fetcher — `createPylonClient` / `api`

For Client Components, the package exports a same-origin `api()` helper that the `useQuery` / `useMutation` hooks build on top of. Most apps don't construct one directly — just `import { api } from "@pylonsync/next/client"`:

```tsx theme={null}
"use client";
import { api } from "@pylonsync/next/client";

async function rotateToken(secretId: string) {
  await api("/api/fn/rotateSecret", {
    method: "POST",
    body: JSON.stringify({ id: secretId }),
  });
}
```

The client requests `/api/*` paths same-origin so the session cookie rides natively. If you split your frontend onto a different origin than your Pylon backend, set up a Next.js rewrite in `next.config.js`:

```js filename="next.config.js" theme={null}
/** @type {import('next').NextConfig} */
module.exports = {
  async rewrites() {
    const target = process.env.PYLON_TARGET ?? "http://localhost:4321";
    return [{ source: "/api/:path*", destination: `${target}/api/:path*` }];
  },
};
```

This keeps the browser talking to its own origin, sidesteps CORS preflight, and means the session cookie doesn't need cross-site config.

## 4. Live data — `<Providers>` + `db.useQuery`

Live queries from React Server Components are tricky (RSC has no client store). For live data, render a Client Component and use the [`db.useQuery`](/clients/react) hook from `@pylonsync/react`:

```tsx filename="app/providers.tsx" theme={null}
"use client";

import { useEffect } from "react";
import { configureClient, useSyncStatus } from "@pylonsync/react";

export function Providers({ children }: { children: React.ReactNode }) {
  // Empty baseUrl → use the current origin (which the Next.js
  // rewrite from step 3 forwards to PYLON_TARGET). Same-origin
  // requests keep cookies + sidestep CORS.
  useEffect(() => {
    configureClient({ baseUrl: "" });
  }, []);
  return <>{children}</>;
}
```

```tsx filename="app/dashboard/posts/page.tsx" theme={null}
"use client";

import { db } from "@pylonsync/react";

type Post = { id: string; title: string };

export default function PostsPage() {
  const { data: posts, loading } = db.useQuery<Post>("Post");
  if (loading) return <div>Loading…</div>;
  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}
```

`db.useQuery` subscribes to the local sync replica — server-pushed inserts, your own optimistic mutations, and cross-tab updates all re-render automatically.

## 5. Middleware gate — `createPylonProxy`

Block protected routes from rendering before auth resolves. Saves a flash of UI before the client-side redirect:

```ts filename="src/middleware.ts" theme={null}
import { createPylonProxy } from "@pylonsync/next/proxy";

const { proxy, config } = createPylonProxy({
  cookieName: "myapp_session",
  loginUrl: "/login",
  matcher: ["/dashboard/:path*"],
});

export { proxy as middleware, config };
```

The proxy only checks for the cookie's presence — forged values still fail server-side at `pylon.requireAuth()` inside the page. It's a UX optimization, not a security boundary.

## 6. Auth flows — `@pylonsync/next/auth`

Sign-up, sign-in, and OAuth from Client Components or Server Actions:

```tsx filename="app/login/page.tsx" theme={null}
"use client";

import { loginWithPassword } from "@pylonsync/next/auth";
import { useState } from "react";
import { useRouter } from "next/navigation";

export default function LoginPage() {
  const router = useRouter();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  async function onSubmit(e: React.FormEvent) {
    e.preventDefault();
    await loginWithPassword({ email, password });
    router.push("/dashboard");
  }

  return (
    <form onSubmit={onSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">Sign in</button>
    </form>
  );
}
```

The session cookie is set by the Pylon backend — your Next.js layer just makes the call. After login, `await pylon.requireAuth()` in any Server Component resolves to the freshly-authenticated user.

OAuth providers follow the same pattern via `startOAuthLogin({ provider: "google", returnTo: "/dashboard" })`.

## Environment variables

| Variable       | Where                  | Purpose                                                                                     |
| -------------- | ---------------------- | ------------------------------------------------------------------------------------------- |
| `PYLON_TARGET` | Server (`process.env`) | Origin of your Pylon backend. Used by `createPylonServer` and the `next.config.js` rewrite. |

There is intentionally no `NEXT_PUBLIC_PYLON_URL` — the client always talks same-origin via the Next rewrite, so the browser doesn't need to know the backend URL. If you don't want the rewrite, pass an explicit `baseUrl` to `configureClient` in your `Providers`.

## Common pitfalls

* **Cookie name mismatch.** Pylon emits `${app_name}_session`. If your `app.ts` says `name: "notes"`, the cookie is `notes_session` — not `pylon_session`. Pass the right name to `createPylonServer({ cookieName })` and `createPylonProxy({ cookieName })`.
* **Cross-origin without rewrite.** If your Next.js app is on `app.example.com` and Pylon is on `api.example.com`, you need either a rewrite (recommended) or matching `Domain=.example.com` cookie config on the backend. See [Sessions](/auth/sessions) for the cookie-domain checklist.
* **Server fetch timeouts.** `pylon.json()` and `pylon.fetch()` apply a 5s timeout by default — a stuck Pylon backend won't hang your Next.js page indefinitely. Use `loading.tsx` and `error.tsx` boundaries to render gracefully when the backend is unreachable.
* **Empty `baseUrl` only works when rewrites are in place.** `configureClient({ baseUrl: "" })` uses the current origin, which only works if `/api/*` reaches your Pylon backend. Without the Next rewrite, pass the absolute Pylon URL.
