Skip to main content
Bondify integrates naturally with the Next.js App Router. The client-side @bondify/react package drives the Telegram auth flow in a 'use client' component, and a Server Action handles proof verification, session cookie creation, and the final redirect — all without a separate API route. This approach keeps your secret key on the server, enables streaming and React Server Components in the rest of your app, and gives you a single file for the entire login flow.

Prerequisites

  • Next.js 14 or later (App Router)
  • A Bondify project ID and secret key from the dashboard

Install

npm install @bondify/react @bondify/server

Set environment variables

Add both variables to .env.local. Only the project ID should be prefixed with NEXT_PUBLIC_ — that prefix is what Next.js uses to decide whether a variable is safe to bundle into the client.
.env.local
NEXT_PUBLIC_BONDIFY_PROJECT_ID=proj_xxxxxxxx
BONDIFY_SECRET_KEY=sk_live_...
BONDIFY_SECRET_KEY must never carry the NEXT_PUBLIC_ prefix. Any variable prefixed with NEXT_PUBLIC_ is inlined into the browser bundle at build time and becomes publicly readable. Your secret key must stay server-only.

Login page

Create the login page as a Client Component. It renders <PulseButton> and, on success, calls the createSession Server Action with the proof.
app/login/page.tsx
'use client';
import { PulseProvider, PulseButton } from '@bondify/react';
import { createSession } from './actions';

export default function LoginPage() {
  return (
    <PulseProvider projectId={process.env.NEXT_PUBLIC_BONDIFY_PROJECT_ID!}>
      <PulseButton
        onSuccess={async (user) => {
          await createSession(user.proof);
          // createSession calls redirect() on success — no further navigation needed
        }}
        onError={(err) => console.error('Auth error:', err)}
      />
    </PulseProvider>
  );
}

Server Action

The Server Action verifies the proof cryptographically, writes an httpOnly session cookie, and redirects to the dashboard. Because this runs on the server, the secret key is never exposed to the browser.
app/login/actions.ts
'use server';
import { verifyProof } from '@bondify/server';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

export async function createSession(proof: string) {
  const user = await verifyProof(proof, process.env.BONDIFY_SECRET_KEY!);
  // user.telegramId — the user's Telegram ID (string)
  // user.name       — display name from Telegram
  // user.username   — Telegram @handle (may be null)

  (await cookies()).set('user_id', user.telegramId, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7, // 7 days
  });

  redirect('/dashboard');
}
verifyProof throws if the proof is invalid, tampered with, or expired. Always let that exception propagate (or catch it and return an error to the client) — never fall through and create a session with an unverified proof.

Protecting routes

Add an async layout to any route group you want to protect. It reads the session cookie and redirects unauthenticated visitors to /login before rendering any children.
app/dashboard/layout.tsx
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const userId = (await cookies()).get('user_id')?.value;
  if (!userId) redirect('/login');

  return <>{children}</>;
}
For more advanced session handling — such as storing structured user data or rotating tokens — replace the plain user_id cookie with a signed JWT. The jose or jsonwebtoken package works well server-side in Next.js.

Reading the current user in Server Components

Once the session cookie is set, any Server Component in the protected subtree can read it directly:
app/dashboard/page.tsx
import { cookies } from 'next/headers';

export default async function DashboardPage() {
  const userId = (await cookies()).get('user_id')?.value;

  return (
    <main>
      <h1>Dashboard</h1>
      <p>Your Telegram ID: {userId}</p>
    </main>
  );
}

Sign out

Create a Server Action that clears the session cookie and redirects to /login:
app/dashboard/actions.ts
'use server';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

export async function signOut() {
  (await cookies()).delete('user_id');
  redirect('/login');
}
Then call it from a Client Component button:
app/dashboard/SignOutButton.tsx
'use client';
import { signOut } from './actions';

export default function SignOutButton() {
  return (
    <form action={signOut}>
      <button type="submit">Sign out</button>
    </form>
  );
}

Full flow at a glance

1

Visitor opens /login

LoginPage renders the <PulseButton> component client-side.
2

User taps the button

Telegram opens. The user taps Confirm. onSuccess fires with user.proof.
3

Server Action runs

createSession(user.proof) calls verifyProof server-side, sets an httpOnly cookie, and redirects to /dashboard.
4

Protected layout checks the cookie

The DashboardLayout reads user_id. If missing, it redirects back to /login.

Next steps

Verify proofs in Node.js

Learn the full verifyProof API and error handling patterns for any Node.js backend.

React SDK reference

Explore PulseProvider, PulseButton props, and the usePulse hook in detail.