Back to Documentation

Web App Integration

Add authentication and subscription access control to your Next.js, React + Vite, or Hono app with the revnu Auth SDK

Automatic Setup

Connect your GitHub repository and we'll automatically generate a PR that integrates the auth SDK into your project. Go to Developer → Auto Setup in your dashboard to get started.

Overview

The @revnu/auth SDK provides pre-built components and hooks for authentication and access control. Users who purchase your product automatically get an account - no separate signup flow needed.

How It Works

  1. 1.User purchases your product via Stripe checkout
  2. 2.revnu creates their account automatically using their email
  3. 3.User receives a "Set up your password" email with a magic link
  4. 4.From then on, they sign in with email/password

Key Features

  • Pre-built sign-in, password setup, and user menu components
  • Real-time access checks with checkAccess()
  • Server-side helpers for protected routes
  • JWT-based auth with embedded product access - no webhook setup required
  • Customizable appearance (colors, border radius, fonts)

Quick Start

1. Install the SDK

bun add @revnu/auth
# or
npm install @revnu/auth
  1. 1. Go to your revnu dashboard → Settings → Developers → Auth SDK
  2. 2. Click "Generate Public Key"
  3. 3. Copy the key (format: rev_pub_xxxxx)

2. Set Environment Variable

# .env.local
NEXT_PUBLIC_REVNU_KEY=rev_pub_xxxxxxxxxxxxx

3. Add the Provider

Wrap your app with RevnuAuthProvider:

// app/layout.tsx
import { RevnuAuthProvider } from '@revnu/auth';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <RevnuAuthProvider>
          {children}
        </RevnuAuthProvider>
      </body>
    </html>
  );
}

4. Create Auth Pages

Create these pages for the complete auth flow:

app/auth/sign-in/page.tsx

import { SignIn } from '@revnu/auth';

export default function SignInPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <SignIn redirectTo="/dashboard" />
    </div>
  );
}

app/auth/setup/page.tsx (for new customers)

import { SetPassword } from '@revnu/auth';

export default function SetupPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <SetPassword redirectTo="/dashboard" />
    </div>
  );
}

5. Protect Your Pages

Use auth guard components or the useRevnuAuth() hook:

'use client';

import { SignedIn, SignedOut, Protect, SignIn } from '@revnu/auth';

const PRODUCT_ID = "your-product-id"; // From revnu dashboard

export default function Dashboard() {
  return (
    <>
      <SignedOut>
        <SignIn redirectTo="/dashboard" />
      </SignedOut>
      <SignedIn>
        <Protect productId={PRODUCT_ID} fallback={<UpgradePrompt />}>
          <div>Protected content here</div>
        </Protect>
      </SignedIn>
    </>
  );
}

For server-side protection, import from @revnu/auth/nextjs:

import { getUser, checkAccess } from '@revnu/auth/nextjs';
import { redirect } from 'next/navigation';

export default async function Dashboard() {
  const user = await getUser();
  if (!user) redirect('/auth/sign-in');

  const hasPro = await checkAccess("your-product-id");
  // ...
}

Components

<SignIn />

Pre-built sign-in form with email/password fields and forgot password link.

<SignIn
  redirectTo="/dashboard"      // Where to go after sign-in
  onSuccess={(user) => {}}     // Callback on success
  onError={(error) => {}}      // Callback on error
  appearance={{                // Customize styling
    variables: {
      colorPrimary: '#6366f1',
      borderRadius: '8px',
    }
  }}
/>
<SignInButton />

Button that triggers sign-in via modal or redirect.

// Redirect mode (navigates to sign-in page)
<SignInButton mode="redirect" redirectUrl="/auth/sign-in">
  Sign in
</SignInButton>

// Modal mode (opens sign-in form in overlay)
<SignInButton
  mode="modal"
  afterSignInUrl="/dashboard"
  onSuccess={(user) => console.log('Signed in:', user)}
  appearance={{ variables: { colorPrimary: '#6366f1' } }}
>
  Sign in
</SignInButton>
<UserButton />

User avatar with dropdown menu for sign out.

<UserButton
  afterSignOutUrl="/"          // Where to go after sign-out
  appearance={{ variables: { colorPrimary: '#6366f1' } }}
/>
<SetPassword />

Password setup form for new users. Reads token from URL automatically. Shows "Request new link" form if token is expired.

<SetPassword
  redirectTo="/dashboard"
  onSuccess={(user) => {}}
  onError={(error) => {}}
/>

Auth Guard Components

Declarative components for auth-based rendering (client-side, React):

<SignedIn />

Renders children only when authenticated.

<SignedIn>
  <p>You are signed in!</p>
</SignedIn>
<SignedOut />

Renders children only when NOT authenticated.

<SignedOut>
  <SignIn redirectTo="/dashboard" />
</SignedOut>
<Protect />

Renders children only when user has access to a product. Shows fallback when denied.

<Protect productId="prod_abc123" fallback={<UpgradePrompt />}>
  <PremiumFeature />
</Protect>

Other Components

  • <ForgotPassword />- Request password reset email
  • <ResetPassword />- Reset password with token from email
  • <RequestSetupLink />- Request new setup link for expired tokens

Hooks

useRevnuAuth()

Main hook for auth state and methods.

const {
  user,              // RevnuUser | null
  isLoading,         // boolean - true during initial load
  isAuthenticated,   // boolean - shorthand for !!user
  signIn,            // { email: (credentials) => Promise<AuthResponse> }
  signOut,           // () => Promise<void>
  checkAccess,       // (productId: string | string[]) => boolean
  refreshSession,    // () => Promise<void>
} = useRevnuAuth();

Checking Product Access

The checkAccess() function checks if the user has an active subscription to a product.

const { checkAccess } = useRevnuAuth();

// Check single product
const hasPro = checkAccess("prod_abc123");

// Check multiple products (returns true if ANY match)
const hasAnyPlan = checkAccess(["prod_basic", "prod_pro"]);

Returns true if subscription is active, or cancelled but hasn't reached the end of the billing period.

Other Hooks

import { useUser, useSession, useAccess, useAuthActions } from '@revnu/auth';

// Get just the user
const user = useUser();

// Get session state with loading
const { user, isLoading, isAuthenticated } = useSession();

// Check access directly (returns false while loading)
const hasPro = useAccess("prod_abc123");

// Get auth actions only
const { signIn, signOut, refreshSession } = useAuthActions();

Server-Side Functions

Import from @revnu/auth/server for Server Components and Route Handlers.

import {
  getUser,        // Get current user (or null)
  checkAccess,    // Check product access
  getToken,       // Get raw JWT token
  requireAuth,    // Get user or redirect
  requireAccess,  // Get user with product access or redirect
} from '@revnu/auth/server';

Protected Server Component

// app/dashboard/page.tsx
import { getUser, checkAccess } from '@revnu/auth/server';
import { redirect } from 'next/navigation';

const PRODUCT_ID = "your-product-id";

export default async function Dashboard() {
  const user = await getUser();
  if (!user) redirect('/auth/sign-in');

  const hasPro = await checkAccess(PRODUCT_ID);

  return (
    <div>
      <h1>Welcome, {user.name || user.email}!</h1>
      {hasPro ? <ProFeatures /> : <UpgradePrompt />}
    </div>
  );
}

Shorthand Helpers

import { requireAuth, requireAccess } from '@revnu/auth/server';

// Redirects to /auth/sign-in if not authenticated
const user = await requireAuth();

// Redirects to /auth/sign-in if no access to product
const user = await requireAccess(PRODUCT_ID);

// Custom redirect URL
const user = await requireAuth('/login');
const user = await requireAccess(PRODUCT_ID, '/upgrade');

Hono Middleware

Import from @revnu/auth/hono for Hono applications (Node.js, Bun, Deno, Cloudflare Workers).

import {
  revnuMiddleware,        // Extract & validate JWT
  getAuth,                // Get auth from context
  requireAuth,            // Middleware: 401 if unauthenticated
  requireProductAccess,   // Middleware: 403 if no access
} from '@revnu/auth/hono';

Example

import { Hono } from 'hono';
import { revnuMiddleware, getAuth, requireAuth, requireProductAccess } from '@revnu/auth/hono';

const app = new Hono();
app.use('*', revnuMiddleware());

// Public — auth available but not required
app.get('/', (c) => {
  const auth = getAuth(c);
  return c.json({ user: auth?.user ?? null });
});

// Protected — 401 if not signed in
app.get('/api/profile', requireAuth(), (c) => {
  return c.json({ user: getAuth(c)!.user });
});

// Product-gated — 403 if no access
app.get('/api/premium', requireProductAccess("prod_abc123"), (c) => {
  return c.json({ data: 'Premium content' });
});

Core Utilities

Import from @revnu/auth/core for framework-agnostic token utilities. Useful for custom integrations.

import {
  verifyToken,       // Verify and decode JWT
  getUserFromToken,  // Extract user from token
  checkTokenAccess,  // Check product access from token
  hasProductAccess,  // Check if products include access
  extractToken,      // Extract token from request
  getAuthFromToken,  // Build auth object from token
  matchPath,         // Match glob-style patterns
  isPublicPath,      // Check if path is public
} from '@revnu/auth/core';

Next.js Middleware Protection

Protect routes automatically with Next.js middleware.

// middleware.ts (or proxy.ts for Next.js 16+)
import { withRevnuAuth } from '@revnu/auth/middleware';

export default withRevnuAuth({
  publicRoutes: [
    '/',
    '/pricing',
    '/auth/sign-in',
    '/auth/setup',
    '/auth/forgot-password',
    '/auth/reset-password',
  ],
  // All other routes require authentication
});

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

Customization

All components accept an appearance prop for styling.

<SignIn
  appearance={{
    variables: {
      // Colors
      colorPrimary: '#6366f1',      // Buttons, focus rings
      colorError: '#dc2626',         // Error messages
      colorText: '#18181b',          // Main text
      colorTextSecondary: '#71717a', // Subtitles, hints
      colorBackground: '#ffffff',    // Card background
      colorBorder: '#e5e7eb',        // Input borders

      // Shape
      borderRadius: '8px',           // 0px for square, 8px for rounded

      // Typography
      fontFamily: 'Inter, sans-serif',
    }
  }}
/>

Components automatically adapt to dark mode using prefers-color-scheme.

Types

interface RevnuUser {
  id: string;
  email: string;
  name?: string;
  createdAt: number;
  products: ProductAccess[];
}

interface ProductAccess {
  productId: string;
  name: string;
  status: 'active' | 'cancelled' | 'past_due' | 'paused';
  cancelAtPeriodEnd: boolean;
  currentPeriodEnd?: number;  // Unix timestamp
  purchasedAt: number;        // Unix timestamp
}

Environment Variables

# Next.js
NEXT_PUBLIC_REVNU_KEY=rev_pub_xxxxxxxxxxxxx

# React + Vite
VITE_REVNU_KEY=rev_pub_xxxxxxxxxxxxx
# VITE_REVNU_AUTH_URL=https://custom.api.com  # Optional

# Hono / Other
REVNU_KEY=rev_pub_xxxxxxxxxxxxx

Just one environment variable per framework. The SDK uses RS256 asymmetric cryptography with an embedded public key, so no secrets need to be configured.

Advanced: Webhooks

While the Auth SDK handles most use cases, you can also use webhooks for server-side event handling, syncing data to your own database, or triggering custom workflows.

When to Use Webhooks

  • Syncing purchase data to your own database
  • Triggering emails or notifications on purchase events
  • Integrating with third-party services (CRMs, analytics)
  • Custom business logic on subscription changes

Webhook Events

purchase.completed

Sent when a customer completes a purchase.

purchase.cancelled

Sent when a subscription is cancelled.

payment.failed

Sent when a recurring payment fails.

Webhook Handler Example

Webhooks are signed with HMAC-SHA256. Verify using the x-rev-signature header.

// app/api/webhooks/revnu/route.ts
import crypto from "crypto";
import { NextResponse } from "next/server";

function verifySignature(payload: string, signature: string, secret: string): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

export async function POST(request: Request) {
  const payload = await request.text();
  const signature = request.headers.get("x-rev-signature") ?? "";

  if (!verifySignature(payload, signature, process.env.REV_WEBHOOK_SECRET!)) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }

  const event = JSON.parse(payload);

  switch (event.event) {
    case "purchase.completed":
      // Sync to your database, send welcome email, etc.
      break;
    case "purchase.cancelled":
      // Update your records
      break;
    case "payment.failed":
      // Notify the user
      break;
  }

  return NextResponse.json({ received: true });
}

Webhook Environment Variables

# For webhooks (optional - only if using webhooks)
REV_WEBHOOK_SECRET=whsec_your_webhook_secret

REST API

For server-to-server access checks or custom integrations, use the REST API.

GET/api/v1/access
curl "https://yourstore.revnu.dev/api/v1/access?email=user@example.com" \
  -H "Authorization: Bearer rev_your_api_key"
{
  "hasAccess": true,
  "customer": {
    "email": "user@example.com",
    "name": "John Doe"
  },
  "products": [
    {
      "id": "prod_abc123",
      "name": "Pro Plan",
      "status": "active",
      "isSubscription": true
    }
  ]
}

Troubleshooting

"Missing public key" error

Ensure NEXT_PUBLIC_REVNU_KEY is set in .env.local and restart your dev server.

checkAccess() always returns false

Verify the product ID is correct (find it in your revnu dashboard under Products). Ensure the user has purchased the product and the status is "active".

Setup link expired

The <SetPassword /> component automatically shows a "Request new link" form when the token is expired.