Quickstart

Next.js sign-in with VibeID.

Add a production-shaped VibeID backend route, styled sign-in UI, and server-side session checks to an App Router app.

Install the packages

Use @vibe-id/next for route handlers, callback verification, sessions, and cookies. Use @vibe-id/react for the sign-in button, approval prompt, avatar menu, hooks, QR generation, polling, and logout.

pnpm add @vibe-id/next @vibe-id/react

Create the VibeID helper

Keep the server helper in one module. The returned object gives you route handlers and an auth helper for reading the current session from server components and route handlers.

// lib/vibe-id.ts
import { createVibeIdRouteHandlers } from "@vibe-id/next";

export const vibeId = createVibeIdRouteHandlers({
  basePath: "/api/vibe-id",
  siteUrl: process.env.NEXT_PUBLIC_SITE_URL,
});

export const vibeAuth = vibeId.auth;

Add one route file

Mount the catch-all route under /api/vibe-id. This one file handles request creation, callback verification, polling, session reads, and logout.

// app/api/vibe-id/[...vibe]/route.ts
import { vibeId } from "@/lib/vibe-id";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export const GET = vibeId.GET;
export const POST = vibeId.POST;
export const DELETE = vibeId.DELETE;
export const OPTIONS = vibeId.OPTIONS;
MethodRoutePurpose
POST/api/vibe-id/requestCreate a short-lived challenge and return a deep link plus status URL.
POST/api/vibe-id/callbackReceive the VibeID app result and verify the returned signature.
GET/api/vibe-id/status/[requestId]Let the browser poll until the request is approved, rejected, failed, or expired.
GET/api/vibe-id/sessionRead the current browser session.
POST/api/vibe-id/logoutDelete the current browser session.

Add the sign-in UI

The styled React components default to the same /api/vibe-id backend route.

"use client";

import "@vibe-id/react/styled.css";
import { VibeIdIdentityMenu } from "@vibe-id/react/styled";

export function HeaderAccount() {
  return (
    <VibeIdIdentityMenu
      signInLabel="Sign in"
      accountHref="/account"
      accountLabel="Account"
    />
  );
}

For a dedicated sign-in page, use VibeIdSignIn. For a custom login screen, use useVibeIdSignIn and render your own button, QR, app-open action, and error state.

Protect a page

Keep session checks on the server. The client can show QR state, but protected content should depend on the HttpOnly cookie.

import { redirect } from "next/navigation";
import { vibeAuth } from "@/lib/vibe-id";

export default async function AccountPage() {
  const session = await vibeAuth.getSessionFromCookies();

  if (!session) {
    redirect("/");
  }

  return <main>Signed in as {session.profile?.displayName ?? session.did}</main>;
}

How the flow works

  1. 1. The browser asks your site for a sign-in request.
  2. 2. Your server creates and stores a short-lived signin.v1 challenge by request ID.
  3. 3. The browser renders a QR code or opens a vibe-id://sign deep link on mobile.
  4. 4. The user opens the request in VibeID and approves it.
  5. 5. VibeID posts the DID, signature, algorithm, request ID, and optional display profile to your callback URL.
  6. 6. Your server verifies the signature against the stored challenge and creates a browser session.
  7. 7. The browser polls status; the polling response sets the HttpOnly cookie after verification succeeds.
  8. 8. Protected pages read that cookie on the server.

Sign-in flow

1

Browser

Create request

Calls your Next.js route to create a short-lived sign-in challenge.

2

App server

Store challenge

Stores the original challenge by request ID and returns a VibeID deep link.

3

Browser

Show QR

Renders the deep link as a QR code and starts polling request status.

4

VibeID

User approves

Scans the QR code, shows the origin, and asks the user to approve signing.

5

VibeID

Callback

Posts DID, signature, algorithm, and request ID to your callback route.

6

App server

Verify proof

Verifies the signature over the stored challenge and creates a browser session.

Local testing from a phone

A phone cannot call your PC's localhost. Use an HTTPS URL that reaches your local dev server. Tailscale Serve works well inside a tailnet.

# Run Next locally
pnpm dev --port 3004

# In another terminal, expose it as HTTPS inside your tailnet
tailscale serve --bg --https=8443 http://127.0.0.1:3004

# Point VibeID callbacks at the HTTPS tailnet URL
NEXT_PUBLIC_SITE_URL=https://your-machine.your-tailnet.ts.net:8443

Production storage

The default file-backed store is for local development and experiments. For production, pass a durable store backed by Redis, Postgres, SQLite, or your application database.