@revnu/auth
v1.1.0
Published
Authentication SDK for Revnu web app products
Maintainers
Readme
@revnu/auth
Authentication SDK for Revnu web app products. Add sign-in and subscription access control to your Next.js, React + Vite, or Hono app with minimal setup.
How It Works
Revnu uses a purchase-first authentication model:
- User purchases your product via Stripe checkout
- Account created automatically - Revnu creates their account using their email
- Setup email sent - User receives "Set up your password" email with a magic link
- User sets password - They click the link and create their password
- Ready to sign in - From then on, they use email/password to sign in
This means no separate sign-up flow - purchasing IS signing up. Returning customers who already have a password just get an "Access granted" email instead.
Features
- Multi-framework support: Next.js, React + Vite SPA, Hono
- Pre-built
<SignIn />,<SignInButton />,<SetPassword />,<ForgotPassword />,<UserButton />, and<Avatar />components - Auth guard components:
<SignedIn />,<SignedOut />,<Protect /> - Automatic store branding on sign-in (logo + store name)
<SignInButton />with modal or redirect mode- Customizable appearance (colors, border radius, fonts)
useRevnuAuth()hook for auth state and access checks- Server-side helpers (
getUser,checkAccess,requireAuth) for Next.js - Hono middleware (
revnuMiddleware,requireAuth,requireProductAccess) - Core utilities for custom integrations (
verifyToken,hasProductAccess) - RS256 cryptographic signature verification (built-in, no keys to configure)
- JWT-based authentication with embedded product access
- No webhook setup required
- TypeScript support
Installation
npm install @revnu/auth
# or
bun add @revnu/auth
# or
yarn add @revnu/authSubpath Exports
| Import | Use in | Description |
|--------|--------|-------------|
| @revnu/auth | React client components | Provider, hooks, components, auth guards |
| @revnu/auth/core | Any JS environment | Token verification, access checks (framework-agnostic) |
| @revnu/auth/nextjs | Next.js server | getUser, checkAccess, requireAuth, getAuth, middleware |
| @revnu/auth/hono | Hono server | revnuMiddleware, getAuth, requireAuth, requireProductAccess |
| @revnu/auth/server | Next.js server | Alias for @revnu/auth/nextjs (backward compat) |
| @revnu/auth/middleware | Next.js middleware | withRevnuAuth middleware helper |
| @revnu/auth/proxy | Next.js 16+ proxy | withRevnuAuth proxy helper |
Quick Start (Next.js)
1. Get Your Public Key
- Log into your Revnu dashboard
- Go to Settings > Developers > Auth SDK
- Click Generate Public Key
- Copy the key (format:
rev_pub_xxxxxxxxxxxxx)
2. Set Environment Variable
NEXT_PUBLIC_REVNU_KEY=rev_pub_xxxxxxxxxxxxx3. Add the Provider
Wrap your app with RevnuAuthProvider in your root layout:
// 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
Sign In Page (required):
// 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>
);
}Password Setup Page (required - for new customers):
// app/auth/setup/page.tsx
import { SetPassword } from '@revnu/auth';
export default function SetupPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<SetPassword redirectTo="/dashboard" />
</div>
);
}Forgot Password Page (recommended):
// app/auth/forgot-password/page.tsx
import { ForgotPassword } from '@revnu/auth';
export default function ForgotPasswordPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<ForgotPassword />
</div>
);
}Reset Password Page (required if using forgot password):
// app/auth/reset-password/page.tsx
import { ResetPassword } from '@revnu/auth';
export default function ResetPasswordPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<ResetPassword redirectTo="/dashboard" />
</div>
);
}5. Protect Pages
Client-side (React components):
'use client';
import { useRevnuAuth } from '@revnu/auth';
import { redirect } from 'next/navigation';
const PRODUCT_ID = "your-product-id"; // From Revnu dashboard
export default function Dashboard() {
const { user, isLoading, isAuthenticated, checkAccess } = useRevnuAuth();
if (isLoading) return <div>Loading...</div>;
if (!isAuthenticated) redirect('/auth/sign-in');
const hasPro = checkAccess(PRODUCT_ID);
return (
<div>
<h1>Welcome, {user?.name || user?.email}!</h1>
{hasPro ? <ProFeatures /> : <UpgradePrompt />}
</div>
);
}Server-side (Server Components):
// app/dashboard/page.tsx
import { getUser, checkAccess } from '@revnu/auth/nextjs';
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/nextjs';
// 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');Quick Start (React + Vite)
1. Set Environment Variable
VITE_REVNU_KEY=rev_pub_xxxxxxxxxxxxx2. Add the Provider
Wrap your app in main.tsx:
// main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { RevnuAuthProvider } from '@revnu/auth';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<RevnuAuthProvider publicKey={import.meta.env.VITE_REVNU_KEY}>
<App />
</RevnuAuthProvider>
</BrowserRouter>
</React.StrictMode>
);3. Create Auth Pages
Create React Router pages for /sign-in, /auth/setup, /auth/forgot-password, and /auth/reset-password using the same components as Next.js (see above).
4. Protect Pages
Use auth guard components or the useRevnuAuth() hook:
import { SignedIn, SignedOut, Protect, SignIn } from '@revnu/auth';
const PRODUCT_ID = "your-product-id";
export default function Dashboard() {
return (
<>
<SignedOut>
<SignIn redirectTo="/dashboard" />
</SignedOut>
<SignedIn>
<Protect productId={PRODUCT_ID} fallback={<UpgradePrompt />}>
<div>Protected content here</div>
</Protect>
</SignedIn>
</>
);
}Note: React + Vite apps are client-only. Do NOT import from
@revnu/auth/serveror@revnu/auth/nextjs.
Quick Start (Hono)
1. Set Environment Variable
REVNU_KEY=rev_pub_xxxxxxxxxxxxx2. Add Middleware and Protect Routes
import { Hono } from 'hono';
import { revnuMiddleware, getAuth, requireAuth, requireProductAccess } from '@revnu/auth/hono';
const app = new Hono();
// Extract and validate JWT on all routes
app.use('*', revnuMiddleware());
// Public route — auth available but not required
app.get('/', (c) => {
const auth = getAuth(c);
if (auth?.user) {
return c.json({ message: `Hello, ${auth.user.email}!` });
}
return c.json({ message: 'Hello, guest!' });
});
// Protected route — requires authentication (returns 401)
app.get('/api/profile', requireAuth(), (c) => {
const auth = getAuth(c);
return c.json({ user: auth!.user });
});
// Product-gated route — requires product access (returns 403)
app.get('/api/premium', requireProductAccess("your-product-id"), (c) => {
const auth = getAuth(c);
return c.json({ data: 'Premium content', user: auth!.user });
});
export default app;Note: Hono uses server-side middleware only. Do NOT import React components from
@revnu/auth.
API Reference
Auth Guard Components
Components for declarative auth protection (client-side, React):
<SignedIn>
Renders children only when the user is authenticated.
import { SignedIn } from '@revnu/auth';
<SignedIn>
<p>You are signed in!</p>
</SignedIn><SignedOut>
Renders children only when the user is NOT authenticated.
import { SignedOut } from '@revnu/auth';
<SignedOut>
<p>Please sign in to continue.</p>
</SignedOut><Protect>
Renders children only when the user has access to a specific product. Shows fallback when access is denied.
import { Protect } from '@revnu/auth';
<Protect productId="prod_abc123" fallback={<UpgradePrompt />}>
<PremiumFeature />
</Protect>| Prop | Type | Description |
|------|------|-------------|
| productId | string | Product ID to check access for |
| fallback | ReactNode | Content to show when access is denied |
| children | ReactNode | Content to show when access is granted |
Components
<RevnuAuthProvider>
Wrap your app with this provider. Reads NEXT_PUBLIC_REVNU_KEY (Next.js) or accepts publicKey prop (Vite).
<RevnuAuthProvider
publicKey="rev_pub_xxx" // Optional, defaults to env var
authUrl="https://custom.api.com" // Optional, for custom auth API
onAuthStateChange={(user) => console.log(user)} // Optional callback
>
{children}
</RevnuAuthProvider><SignIn />
Pre-built sign-in form.
<SignIn
redirectTo="/dashboard" // Where to go after sign-in
onSuccess={(user) => {}} // Callback on success
onError={(error) => {}} // Callback on error
className="custom-class" // Custom styling
initialValues={{ email: '[email protected]' }} // Pre-fill email
appearance={{ // Customize colors, borders, fonts
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" // Where to go after sign-in
onSuccess={(user) => {}} // Callback on success
onError={(error) => {}} // Callback on error
appearance={{ // Passed to SignIn component
variables: { colorPrimary: '#6366f1' }
}}
initialValues={{ email: '[email protected]' }}
className="my-button-class" // Style the button
>
Sign in
</SignInButton>The modal:
- Opens as an overlay with backdrop blur
- Closes on Escape key or clicking outside
- Has a close button (X)
- Prevents body scroll when open
<SetPassword />
Password setup form for new users. Reads token from URL query params automatically.
When a user purchases your product, they receive an email with a link like:
https://yourapp.com/auth/setup?token=xxx
This component handles that token and lets them set their password.
<SetPassword
redirectTo="/dashboard" // Where to go after setup
onSuccess={(user) => {}} // Callback on success
onError={(error) => {}} // Callback on error
className="custom-class"
appearance={{ variables: { colorPrimary: '#6366f1' } }}
/>What happens with expired/invalid tokens:
- If the token is expired, the component automatically shows a "Request new link" form
- If the user already has a password, they're redirected to sign in
<RequestSetupLink />
Form for users to request a new setup link (when their original link expired).
<RequestSetupLink
onSuccess={() => {}} // Callback on success
onError={(error) => {}}
className="custom-class"
appearance={{ variables: { colorPrimary: '#6366f1' } }}
/><ForgotPassword />
Form for existing users to request a password reset email.
<ForgotPassword
onSuccess={() => {}} // Callback on success
onError={(error) => {}}
className="custom-class"
appearance={{ variables: { colorPrimary: '#6366f1' } }}
/><ResetPassword />
Password reset form. Reads token from URL query params automatically.
<ResetPassword
redirectTo="/dashboard" // Where to go after reset
onSuccess={(user) => {}}
onError={(error) => {}}
className="custom-class"
appearance={{ variables: { colorPrimary: '#6366f1' } }}
/><UserButton />
User avatar with dropdown menu (sign out, etc.).
<UserButton
afterSignOutUrl="/" // Where to go after sign-out
className="custom-class"
appearance={{ variables: { colorPrimary: '#6366f1' } }}
/><Avatar />
Standalone avatar component showing user initials or an image.
import { Avatar } from '@revnu/auth';
// With user (shows initials)
<Avatar user={user} />
// With image
<Avatar user={user} imageUrl="https://example.com/photo.jpg" />
// Sizes: 'small' (24px), 'medium' (32px, default), 'large' (40px), or custom number
<Avatar user={user} size="large" />
<Avatar user={user} size={48} />| Prop | Type | Description |
|------|------|-------------|
| user | { name?: string; email: string } \| null | User for initials |
| imageUrl | string | Image URL (overrides initials) |
| size | 'small' \| 'medium' \| 'large' \| number | Size (default: 'medium') |
| className | string | Custom CSS class |
Customization
All components accept an appearance prop for styling customization.
The default borderRadius is 8px (rounded). Pass 0px for square corners.
Appearance Variables
<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
colorInputBackground: 'transparent',
// Shape
borderRadius: '8px', // 0px for square, 8px for rounded
// Typography
fontFamily: 'Inter, sans-serif',
}
}}
/>Dark Mode
The components automatically adapt to dark mode using prefers-color-scheme. Custom colors you set will be used as the base, with automatic dark mode variants applied.
Example: Rounded Purple Theme
const purpleTheme = {
variables: {
colorPrimary: '#7c3aed',
colorError: '#ef4444',
borderRadius: '12px',
fontFamily: 'Inter, system-ui, sans-serif',
}
};
// Use across all components
<SignIn appearance={purpleTheme} />
<SetPassword appearance={purpleTheme} />
<ForgotPassword appearance={purpleTheme} />Example: Square Minimal Theme
const squareTheme = {
variables: {
colorPrimary: '#000000',
borderRadius: '0px', // Override the default 8px
}
};
<SignIn appearance={squareTheme} />Hooks
useRevnuAuth()
Access auth state and methods.
const {
user, // RevnuUser | null
isLoading, // boolean
isAuthenticated, // boolean
signIn, // { email: (credentials) => Promise<AuthResponse> }
signOut, // () => Promise<void>
checkAccess, // (productId: string | string[]) => boolean
refreshSession, // () => Promise<void>
} = useRevnuAuth();Programmatic sign-in:
const { signIn } = useRevnuAuth();
const result = await signIn.email({
email: '[email protected]',
password: 'password123',
});
if (result.success) {
console.log('Signed in:', result.user);
} else {
console.error('Error:', result.error);
}useStoreInfo()
Get the store info (name, logo) for the current creator.
import { useStoreInfo } from '@revnu/auth';
const storeInfo = useStoreInfo();
// { name: "My Store", logoUrl: "https://..." } | nullServer Functions (Next.js)
Import from @revnu/auth/nextjs (or @revnu/auth/server):
import {
getUser,
getAuth,
checkAccess,
getToken,
requireAuth,
requireAccess,
} from '@revnu/auth/nextjs';getUser()
Get the current user from cookies. Returns null if not authenticated.
const user = await getUser();
// { id, email, name, createdAt, products: [...] } | nullgetAuth()
Get the full auth object (Clerk-inspired API).
const auth = await getAuth();
// { user, token, checkAccess } | nullcheckAccess(productId)
Check if the user has access to a product.
const hasAccess = await checkAccess("product-id");
// or check multiple products (returns true if ANY match)
const hasAccess = await checkAccess(["product-1", "product-2"]);getToken()
Get the raw JWT token (for passing to external APIs).
const token = await getToken();
// "eyJhbG..." | nullrequireAuth(redirectTo?)
Get user or redirect if not authenticated.
const user = await requireAuth(); // Redirects to /auth/sign-in
const user = await requireAuth('/login'); // Custom redirectrequireAccess(productId, redirectTo?)
Get user or redirect if no access.
const user = await requireAccess("product-id");
const user = await requireAccess("product-id", '/upgrade');Hono Middleware
Import from @revnu/auth/hono:
import { revnuMiddleware, getAuth, requireAuth, requireProductAccess } from '@revnu/auth/hono';revnuMiddleware()
Extracts JWT from cookies/Authorization header, validates it, sets auth in context. Does NOT reject unauthenticated requests.
getAuth(c)
Returns the auth object from Hono context: { user, token, checkAccess }. Returns null if not authenticated.
requireAuth()
Middleware that returns 401 if not authenticated.
requireProductAccess(productId)
Middleware that returns 401 if not authenticated, 403 if no access to the specified product.
Core Utilities
Import from @revnu/auth/core for framework-agnostic utilities:
import {
verifyToken,
getUserFromToken,
checkTokenAccess,
hasProductAccess,
extractToken,
getAuthFromToken,
matchPath,
isPublicPath,
} from '@revnu/auth/core';Proxy / Middleware (Next.js)
Protect routes automatically with Next.js proxy (or middleware for older versions).
Next.js 16+ (uses proxy.ts):
// proxy.ts
import { withRevnuAuth } from '@revnu/auth/proxy';
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).*)'],
};Next.js 14-15 (uses middleware.ts):
// middleware.ts
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).*)'],
};Note: Next.js 16 renamed
middleware.tstoproxy.ts. Both imports (@revnu/auth/proxyand@revnu/auth/middleware) export the samewithRevnuAuthhelper - just use the one matching your file name.
Required Routes
Your app needs these auth routes for the full flow:
| Route | Component | Purpose |
|-------|-----------|---------|
| /auth/sign-in | <SignIn /> | Existing users sign in |
| /auth/setup | <SetPassword /> | New users set their password |
| /auth/forgot-password | <ForgotPassword /> | Request password reset |
| /auth/reset-password | <ResetPassword /> | Reset password with token |
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
}
interface AuthResponse {
success: boolean;
user?: RevnuUser;
error?: string;
}
interface RevnuAuth {
user: RevnuUser | null;
token: string | null;
checkAccess: (productId: string | string[]) => boolean;
}
interface Appearance {
variables?: AppearanceVariables;
}
interface AppearanceVariables {
colorPrimary?: string;
colorError?: string;
colorText?: string;
colorTextSecondary?: string;
colorBackground?: string;
colorBorder?: string;
colorInputBackground?: string;
borderRadius?: string;
fontFamily?: string;
}
interface SignInInitialValues {
email?: string;
}Access Check Logic
checkAccess() returns true if the user has:
- An active subscription to the product, OR
- A cancelled subscription that hasn't reached
currentPeriodEndyet
Environment Variables
| Variable | Framework | Required | Description |
|----------|-----------|----------|-------------|
| NEXT_PUBLIC_REVNU_KEY | Next.js | Yes | Public API key from Revnu dashboard |
| VITE_REVNU_KEY | React + Vite | Yes | Public API key from Revnu dashboard |
| REVNU_KEY | Hono / Other | Yes | Public API key from Revnu dashboard |
| VITE_REVNU_AUTH_URL | React + Vite | No | Custom auth API URL (defaults to Revnu) |
Just one environment variable per framework. The SDK uses RS256 asymmetric cryptography with an embedded public key, so no secrets need to be configured in your app.
Product IDs
Find product IDs in your Revnu dashboard:
- Go to Products
- Click on a product
- Copy the ID from the URL
User Flow Diagram
┌─────────────────┐
│ User visits │
│ your checkout │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Completes Stripe│
│ checkout │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Revnu creates │
│ account │
└────────┬────────┘
│
▼
┌─────────────────────────────────────┐
│ Is this a new user? │
└──────────┬─────────────┬────────────┘
│ Yes │ No (has password)
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ "Set up your │ │ "You now have │
│ password" email│ │ access" email │
└────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ /auth/setup │ │ /auth/sign-in │
│ <SetPassword/> │ │ <SignIn /> │
└────────┬────────┘ └────────┬────────┘
│ │
└────────┬───────────┘
▼
┌─────────────────┐
│ Dashboard │
└─────────────────┘Troubleshooting
"Missing public key" error
- Ensure your env var is set (
NEXT_PUBLIC_REVNU_KEY,VITE_REVNU_KEY, orREVNU_KEY) - Restart your dev server after adding env vars
User is null after sign-in
- Check browser localStorage for
revnu_access_token - Try clearing localStorage and signing in again
checkAccess always returns false
- Verify the product ID is correct
- Ensure the user has purchased the product
- Check the purchase status is "active"
Setup link expired
- The
<SetPassword />component automatically shows a "Request new link" form - Users can enter their email to receive a fresh setup link
User purchased but didn't receive setup email
- Check spam folder
- Verify the email address was correct at checkout
- User can request a new setup link from
/auth/setupwith an expired token
License
MIT
