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.User purchases your product via Stripe checkout
- 2.revnu creates their account automatically using their email
- 3.User receives a "Set up your password" email with a magic link
- 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. Go to your revnu dashboard → Settings → Developers → Auth SDK
- 2. Click "Generate Public Key"
- 3. Copy the key (format:
rev_pub_xxxxx)
2. Set Environment Variable
# .env.local
NEXT_PUBLIC_REVNU_KEY=rev_pub_xxxxxxxxxxxxx3. 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
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',
}
}}
/>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>User avatar with dropdown menu for sign out.
<UserButton
afterSignOutUrl="/" // Where to go after sign-out
appearance={{ variables: { colorPrimary: '#6366f1' } }}
/>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):
Renders children only when authenticated.
<SignedIn>
<p>You are signed in!</p>
</SignedIn>Renders children only when NOT authenticated.
<SignedOut>
<SignIn redirectTo="/dashboard" />
</SignedOut>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
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_xxxxxxxxxxxxxJust 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
Sent when a customer completes a purchase.
Sent when a subscription is cancelled.
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_secretREST API
For server-to-server access checks or custom integrations, use the REST API.
/api/v1/accesscurl "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.