npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wocha/nextjs

v0.1.0

Published

Next.js App Router adapter for Wocha authentication

Downloads

360

Readme

@wocha/nextjs

npm version npm downloads TypeScript License

Next.js App Router adapter for Wocha authentication. Implements a BFF (backend-for-frontend) pattern: OAuth runs server-side with a confidential client, and sessions are stored in encrypted httpOnly cookies.

Install

npm install @wocha/nextjs
# or: pnpm add / yarn add / bun add @wocha/nextjs

Peer dependencies: Next.js 14+, React 18+.

Environment variables

Set these in .env.local — all bundles (route handler, middleware, server helpers) read them automatically:

WOCHA_CLIENT_ID=your-client-id
WOCHA_CLIENT_SECRET=your-client-secret
WOCHA_ISSUER=https://tenant.auth.wocha.ai
WOCHA_API_URL=https://tenant.api.wocha.ai  # optional
WOCHA_COOKIE_SECRET=your-cookie-secret       # optional, defaults to client secret

Register the callback URI https://your-app.com/api/auth/callback in the Wocha Console.

Quick start

Three pieces of wiring: a route handler, middleware, and a client session provider.

1. Route handlerapp/api/auth/[...wocha]/route.ts:

Important: The catch-all segment must be named wocha (i.e. [...wocha]). The SDK reads params.wocha to route login, callback, logout, and other auth actions.

import { createWochaHandler } from "@wocha/nextjs";

export const { GET, POST } = createWochaHandler({
  clientId: process.env.WOCHA_CLIENT_ID!,
  clientSecret: process.env.WOCHA_CLIENT_SECRET!,
  issuer: process.env.WOCHA_ISSUER!,
});

When env vars are set, you can omit the config object entirely — createWochaHandler() will resolve from env:

import { createWochaHandler, wochaAuthConfigFromEnv } from "@wocha/nextjs";

export const { GET, POST } = createWochaHandler(wochaAuthConfigFromEnv()!);

2. Middlewaremiddleware.ts:

When WOCHA_CLIENT_ID, WOCHA_CLIENT_SECRET, and WOCHA_ISSUER are set, middleware works without passing config:

import { withWochaAuth } from "@wocha/nextjs/middleware";

export default withWochaAuth(undefined, {
  publicPaths: ["/", "/about"],
});

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico|api/auth).*)"],
};

Or simply:

export default withWochaAuth();

Note: Calling withWochaAuth() without publicPaths logs a console warning — every route except /api/auth requires authentication. Set publicPaths explicitly for landing pages and marketing routes.

Expired access tokens are refreshed silently using the refresh token before redirecting to login.

3. Client provider — wrap your layout:

"use client";

import { WochaSessionProvider } from "@wocha/nextjs/client";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <WochaSessionProvider refetchOnWindowFocus refetchInterval={0}>
      {children}
    </WochaSessionProvider>
  );
}

| Prop | Default | Description | |------|---------|-------------| | refetchOnWindowFocus | true | Refetch session when the browser window regains focus. | | refetchInterval | 0 | Poll interval in ms; 0 disables polling. |

Visit /api/auth/login to start the sign-in flow.

Subpath imports

| Import path | Purpose | |-------------|---------| | @wocha/nextjs | Route handler (createWochaHandler), middleware (withWochaAuth), server helpers | | @wocha/nextjs/middleware | Middleware only (for Edge runtime bundles) | | @wocha/nextjs/server | Server Components helpers (getSession, getUser, …) | | @wocha/nextjs/client | Client hooks and WochaSessionProvider |

Configuration reference

Config can be passed explicitly or resolved from environment variables (WOCHA_CLIENT_ID, WOCHA_CLIENT_SECRET, WOCHA_ISSUER). Each bundle entry (index, middleware, server) resolves config independently — there is no shared singleton across bundles.

Pass a WochaAuthConfig to createWochaHandler() or use wochaAuthConfigFromEnv():

| Field | Type | Required | Description | |-------|------|----------|-------------| | clientId | string | Yes | OAuth client ID. | | clientSecret | string | Yes | OAuth client secret (server-side only). | | issuer | string | Yes | OIDC issuer URL. | | baseUrl | string | No | Application base URL for redirect URIs. Defaults to the request origin. | | apiUrl | string | No | Platform API base URL for org switching. Defaults to the issuer origin. | | scopes | string[] | No | OIDC scopes. Default: openid, profile, email, org_id, offline_access. | | postLoginRedirect | string | No | Default path after login when no return_to is provided. Default: /. | | postLogoutRedirect | string | No | URL after logout. Defaults to the application base URL. | | authErrorRedirect | string | No | Path for OAuth error redirects (default: postLoginRedirect or /). Receives ?auth_error= query params. | | sessionCookieName | string | No | Encrypted session cookie name. Default: greet_session. | | sessionSecret | string | No | Separate secret for cookie encryption. Defaults to clientSecret. Set via WOCHA_COOKIE_SECRET. | | dpop | boolean \| { enabled, algorithm? } | No | Enable RFC 9449 DPoP sender-constrained tokens. Default: false. |

DPoP (Sender-Constrained Tokens)

When dpop: true is set, the SDK:

  1. Generates an ECDSA P-256 key pair at the callback step
  2. Includes DPoP proofs in all token requests
  3. Stores the private key in a separate encrypted cookie ({sessionCookieName}_dpop)
  4. Includes DPoP proofs when calling resource servers via buildResourceRequestHeaders()

The DPoP key is stored separately from the session cookie to keep both under the 4096-byte browser limit.

Pushed Authorization Requests (PAR)

When the OIDC discovery document advertises a pushed_authorization_request_endpoint, the SDK automatically uses it. This is required by Wocha production deployments. No configuration is needed — PAR support is transparent.

Route handler reference

Mount at app/api/auth/[...wocha]/route.ts. Routes are derived from the catch-all segment:

| Route | Method | Description | |-------|--------|-------------| | /api/auth/login | GET | Starts OAuth flow. Accepts ?return_to=/path to redirect after login. Sets an encrypted PKCE cookie. | | /api/auth/callback | GET | Exchanges the authorisation code, writes the session cookie, redirects to return_to. | | /api/auth/logout | GET, POST | Revokes tokens, clears session cookie, redirects to the OIDC end-session endpoint. | | /api/auth/session | GET | Returns the current session (see below). | | /api/auth/refresh | POST | Refreshes tokens server-side and updates the session cookie. | | /api/auth/switch-org | POST | Switches organisation. Body: { targetOrgId: string }. May return { redirect } for re-authorisation. |

Session response

GET /api/auth/session returns:

{
  "session": {
    "user": { "id": "...", "email": "...", "name": "...", "orgId": "..." },
    "accessToken": "...",
    "expiresAt": 1717000000,
    "orgId": "...",
    "orgIds": ["..."]
  }
}

When unauthenticated: { "session": null }.

Refresh tokens are not exposed to the browser — they remain encrypted in the httpOnly session cookie and are used only by the /refresh route.

Server helpers

Import from @wocha/nextjs/server (or the main package):

import { getSession, getUser, requireSession, getAccessToken } from "@wocha/nextjs/server";

| Function | Return type | Description | |----------|-------------|-------------| | getSession(config?) | Promise<GreetSession \| null> | Reads and decrypts the session cookie. Refreshes expired tokens automatically. | | getUser(config?) | Promise<GreetUser \| null> | Returns session.user or null. | | requireSession(config?) | Promise<GreetSession> | Returns the session or redirects to /api/auth/login?return_to=.... | | getAccessToken(config?) | Promise<string \| null> | Returns the access token from the session. |

Sign-in / sign-out URL helpers

import { signInUrl, signOutUrl } from "@wocha/nextjs/server";

signInUrl("/dashboard");   // → /api/auth/login?return_to=%2Fdashboard
signUpUrl("/welcome");     // → /api/auth/login?signup=1&return_to=%2Fwelcome
signOutUrl("/");           // → /api/auth/logout?return_to=%2F

// Custom auth base path (default: /api/auth)
signInUrl("/app", "/auth"); // → /auth/login?return_to=%2Fapp

Server helpers resolve config from env vars when not passed explicitly.

Use in Server Components, Route Handlers, and Server Actions:

import { requireSession } from "@wocha/nextjs/server";

export default async function DashboardPage() {
  const session = await requireSession();
  return <h1>Hello, {session.user.email}</h1>;
}

Client hooks

Import from @wocha/nextjs/client. Requires WochaSessionProvider.

useSession()

const { data, status, error, refresh } = useSession();
// data: GreetSession | null
// status: "loading" | "authenticated" | "unauthenticated"
// error: Error | null
// refresh: () => Promise<void>

useUser()

const { user, status } = useUser();
// user: GreetUser | null
// status: SessionStatus

useOrg()

const { orgId, orgIds, switchOrg, isLoading } = useOrg();

switchOrg(targetOrgId, endpoint?)

Standalone function (does not require a hook):

import { switchOrg } from "@wocha/nextjs/client";

await switchOrg("org-abc123");

usePermission(check)

Checks a SpiceDB permission using the session access token. Requires apiUrl in handler config.

const { allowed, isLoading } = usePermission({
  resource: { type: "document", id: docId },
  permission: "edit",
});

Return type: { allowed: boolean; isLoading: boolean; error: Error | null }.

signIn() / signOut()

import { signIn, signOut, signInUrl, signOutUrl } from "@wocha/nextjs/client";

signIn("/dashboard");   // navigates to login with return_to
signOut("/");           // navigates to logout with return_to

Client Components

Import from @wocha/nextjs/client. All components require WochaSessionProvider.

SignInButton / SignUpButton / SignOutButton

Link-styled controls that navigate to the BFF login, signup, or logout routes. Shorter aliases SignIn, SignUp, and SignOut are also exported.

import { SignInButton, SignUpButton, SignOutButton } from "@wocha/nextjs/client";

<SignInButton returnTo="/dashboard" />
<SignUpButton returnTo="/welcome" />
<SignOutButton returnTo="/" />

SignUpButton appends signup=1 to the login URL; the route handler forwards screen_hint=signup to the authorisation server.

UserButton

Shows the signed-in user with a sign-out action. Renders nothing when unauthenticated.

import { UserButton } from "@wocha/nextjs/client";

// Default avatar menu
<UserButton />

// Custom render
<UserButton>
  {({ user, signOut }) => (
    <div>
      <span>{user.email}</span>
      <button type="button" onClick={() => signOut("/")}>Sign out</button>
    </div>
  )}
</UserButton>

OrgSwitcher

Organisation dropdown when the user belongs to multiple orgs. Renders nothing for a single org or when unauthenticated.

import { OrgSwitcher } from "@wocha/nextjs/client";

<OrgSwitcher />

<OrgSwitcher>
  {({ orgId, orgIds, switchOrg, isLoading }) => (
    <select
      value={orgId}
      disabled={isLoading}
      onChange={(e) => void switchOrg(e.target.value)}
    >
      {orgIds?.map((id) => (
        <option key={id} value={id}>{id}</option>
      ))}
    </select>
  )}
</OrgSwitcher>

Authenticated / Unauthenticated

Conditional rendering based on session status.

import { Authenticated, Unauthenticated, SignInButton } from "@wocha/nextjs/client";

<Authenticated fallback={<p>Loading…</p>}>
  <Dashboard />
</Authenticated>

<Unauthenticated>
  <SignInButton />
</Unauthenticated>

Protect

Permission-gated wrapper. Renders children only when the user is authenticated and passes the permission check. Renders fallback (or null) while loading or when denied. Matches @wocha/react's Protect component.

import { Protect } from "@wocha/nextjs/client";

<Protect
  permission={{ resource: { type: "document", id: "doc-123" }, permission: "edit" }}
  fallback={<p>You don't have access.</p>}
>
  <EditForm />
</Protect>

Requires apiUrl in handler config — same as usePermission.

Middleware

When env vars are set, middleware requires no config:

import { withWochaAuth } from "@wocha/nextjs/middleware";

export default withWochaAuth(undefined, {
  publicPaths: ["/", "/about", "/pricing"],
  loginPath: "/api/auth/login",
});

You can also pass config explicitly:

export default withWochaAuth(
  {
    clientId: process.env.WOCHA_CLIENT_ID!,
    clientSecret: process.env.WOCHA_CLIENT_SECRET!,
    issuer: process.env.WOCHA_ISSUER!,
  },
  {
    publicPaths: ["/", "/about", "/pricing"],
    loginPath: "/api/auth/login",
  },
);

| Option | Default | Description | |--------|---------|-------------| | publicPaths | [] | Paths that skip auth checks. Supports trailing * wildcards (e.g. /blog*). | | loginPath | /api/auth/login | Redirect target for unauthenticated requests. Appends ?return_to= automatically. |

Expired access tokens are refreshed silently before redirecting to login. Routes under /api/auth are always public. Configure a custom matcher in middleware.ts to limit which routes the middleware runs on:

export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*"],
};

Security model

  • Confidential client: The client secret stays on the server. Token exchange and refresh happen in Route Handlers, never in the browser.
  • Encrypted httpOnly cookies: Sessions are serialised and encrypted with AES-256-GCM. The encryption key is derived from the client secret via PBKDF2 (100,000 iterations, SHA-256).
  • PKCE: The login flow uses PKCE with the verifier stored in a short-lived encrypted cookie (greet_pkce).
  • ID token verification: Callback exchanges verify the id_token signature against the issuer JWKS (RS256/ES256), and validate iss and aud.
  • No refresh token in the browser: The /session endpoint returns only user, accessToken, and expiresAt. Refresh tokens remain server-side.

Structured errors

Server-side auth flows throw WochaAuthError with a typed code (e.g. state_mismatch, token_exchange_failed). Import from the main package:

import { WochaAuthError } from "@wocha/nextjs";

try {
  // route handler logic
} catch (err) {
  if (err instanceof WochaAuthError && err.code === "state_mismatch") {
    // handle CSRF mismatch
  }
}

Self-hosted configuration

export const { GET, POST } = createWochaHandler({
  clientId: process.env.WOCHA_CLIENT_ID!,
  clientSecret: process.env.WOCHA_CLIENT_SECRET!,
  issuer: "https://auth.internal.example.com",
  apiUrl: "https://api.internal.example.com",
  baseUrl: "https://app.internal.example.com",
});

Or set the equivalent environment variables — see Environment variables above.

Related packages

| Package | Use when | |---------|----------| | @wocha/react | React SPA without a server-side BFF | | @wocha/sdk | Server-side user/org management via the Management API | | @wocha/cli | Scaffold auth integration with npx @wocha/cli init |

Migrating from @greet-auth/nextjs

If your project vendors the legacy @greet-auth/nextjs package, migrate to @wocha/nextjs:

  1. Replace the vendored package with npm install @wocha/nextjs
  2. Update imports: @greet-auth/nextjs@wocha/nextjs
  3. Rename env vars: GREET_CLIENT_IDWOCHA_CLIENT_ID, GREET_ISSUERWOCHA_ISSUER, etc.

The SDK reads GREET_* env vars as a backward-compatible fallback (with a deprecation warning), so the migration can be done incrementally.

Troubleshooting

redirect_uri_mismatch

The callback URL in your app must exactly match a redirect URI registered in the Wocha Console (including scheme, host, port, and path). For this SDK the default is https://your-app.com/api/auth/callback.

CSRF / state_mismatch

The authorisation flow stores a signed state value in a cookie. If login fails with state_mismatch, clear site cookies for your app origin and retry. Avoid opening multiple login tabs in parallel.

Session cookie not set

Cookies require secure: true when NODE_ENV=production. Use HTTPS in production. In local development, http://localhost is allowed. Confirm WOCHA_CLIENT_SECRET (or WOCHA_COOKIE_SECRET) is set and stable across deploys — changing it invalidates existing sessions.

Missing environment variables

The route handler and middleware read WOCHA_CLIENT_ID, WOCHA_CLIENT_SECRET, and WOCHA_ISSUER from the environment. Missing values throw at startup or on the first auth request. Check .env.local is loaded (Next.js does this automatically for dev).

Callback URL not registered

Every environment (local, staging, production) needs its own redirect URI in the Console. A common mistake is registering production only while testing on http://localhost:3000.

Separate API server

If your architecture includes a separate API backend (e.g. NestJS, Go, Python), that server must validate access tokens independently. The @wocha/nextjs BFF handles login and session management; your API validates the forwarded access token via JWKS.

See the Resource server guide for JWT validation patterns in Express, NestJS, Go, Python, and other frameworks.