npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@revnu/auth

v1.1.0

Published

Authentication SDK for Revnu web app products

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:

  1. User purchases your product via Stripe checkout
  2. Account created automatically - Revnu creates their account using their email
  3. Setup email sent - User receives "Set up your password" email with a magic link
  4. User sets password - They click the link and create their password
  5. 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/auth

Subpath 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

  1. Log into your Revnu dashboard
  2. Go to Settings > Developers > Auth SDK
  3. Click Generate Public Key
  4. Copy the key (format: rev_pub_xxxxxxxxxxxxx)

2. Set Environment Variable

NEXT_PUBLIC_REVNU_KEY=rev_pub_xxxxxxxxxxxxx

3. 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_xxxxxxxxxxxxx

2. 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/server or @revnu/auth/nextjs.

Quick Start (Hono)

1. Set Environment Variable

REVNU_KEY=rev_pub_xxxxxxxxxxxxx

2. 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://..." } | null

Server 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: [...] } | null

getAuth()

Get the full auth object (Clerk-inspired API).

const auth = await getAuth();
// { user, token, checkAccess } | null

checkAccess(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..." | null

requireAuth(redirectTo?)

Get user or redirect if not authenticated.

const user = await requireAuth();  // Redirects to /auth/sign-in
const user = await requireAuth('/login');  // Custom redirect

requireAccess(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.ts to proxy.ts. Both imports (@revnu/auth/proxy and @revnu/auth/middleware) export the same withRevnuAuth helper - 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 currentPeriodEnd yet

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:

  1. Go to Products
  2. Click on a product
  3. 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, or REVNU_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/setup with an expired token

License

MIT