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

@donotdev/billing

v0.0.4

Published

Simplified Stripe billing integration for DoNotDev framework with Firebase custom claims

Readme

@donotdev/billing

Production-ready billing system with Stripe integration and smart idempotency.

Features

  • ✅ One-time payments and subscriptions
  • ✅ Automatic webhook processing
  • ✅ Smart idempotency (auto-detects Firestore)
  • ✅ Customizable hooks for business logic
  • ✅ React components and templates
  • ✅ Zero configuration required

Quick Start

1. Install Dependencies

bun add @donotdev/billing @donotdev/types

2. Configure Your Products

Important: Frontend and backend configs are split for security and separation of concerns.

Backend Config (Functions)

// apps/your-app/functions/src/config/stripeBackConfig.ts
import type { StripeBackConfig } from '@donotdev/types';

export const stripeBackConfig: StripeBackConfig = {
  web_dev_course: {
    type: 'StripePayment',
    name: 'Web Development Masterclass',
    price: 29900, // In cents (299.00 USD)
    currency: 'USD',
    priceId: process.env.STRIPE_PRICE_COURSE!,
    tier: 'course_access',
    duration: 'lifetime',
    description: 'Complete course with lifetime access',

    // Custom hook - grant course access after payment
    onPurchaseSuccess: async (userId, metadata) => {
      await grantCourseAccess(userId, 'web-dev-masterclass');
      await sendWelcomeEmail(userId);
    },
  },
  pro_monthly: {
    type: 'StripeSubscription',
    name: 'Pro Plan',
    price: 2900, // In cents (29.00 USD)
    currency: 'USD',
    priceId: process.env.STRIPE_PRICE_PRO_MONTHLY!,
    tier: 'pro',
    duration: '1month',
    description: 'Monthly subscription with all features',

    onSubscriptionCreated: async (userId, metadata) => {
      await enableProFeatures(userId);
      await sendWelcomeEmail(userId);
    },
  },
};

Frontend Config (App)

// apps/your-app/src/config/stripeFrontConfig.ts
import type { StripeFrontConfig } from '@donotdev/types';

export const stripeFrontConfig: StripeFrontConfig = {
  web_dev_course: {
    name: 'Web Development Masterclass',
    price: 299, // Display price (in currency units)
    currency: 'USD',
    priceId: import.meta.env.VITE_STRIPE_PRICE_COURSE || '',
    description: 'Complete course with lifetime access',
    features: [
      '50+ hours of video content',
      'Lifetime access',
      'Downloadable resources',
      'Community support',
    ],
    allowPromotionCodes: true,
  },
  pro_monthly: {
    name: 'Pro Plan',
    price: 29, // Display price (in currency units)
    currency: 'USD',
    priceId: import.meta.env.VITE_STRIPE_PRICE_PRO_MONTHLY || '',
    description: 'Monthly subscription with all features',
    features: [
      'All premium features',
      'Priority support',
      'Advanced analytics',
      'Custom integrations',
    ],
    allowPromotionCodes: true,
  },
};

3. Create Webhook Handler

// apps/your-app/functions/src/index.ts
import {
  createCheckoutSession,
  createStripeWebhook,
} from '@donotdev/functions/firebase';
import { stripeBackConfig } from './config/stripeBackConfig.js';

export const createCheckout = createCheckoutSession(stripeBackConfig);
export const handleStripeWebhook = createStripeWebhook(stripeBackConfig);

4. Create Frontend Page

// apps/your-app/src/pages/PricingPage.tsx
import { PaymentTemplate } from '@donotdev/templates';
import { stripeFrontConfig } from '../config/stripeFrontConfig';

export default function PricingPage() {
  return (
    <PaymentTemplate
      namespace="pricing"
      meta={{
        namespace: 'pricing',
        auth: { required: true },
        title: 'Pricing',
      }}
      billing={stripeFrontConfig}
      successUrl="/success"
      cancelUrl="/pricing"
    />
  );
}

5. Configure App

// apps/your-app/src/App.tsx
import { stripeFrontConfig } from './config/stripeFrontConfig';

const APP_CONFIG = {
  // ... other config
  billing: {
    config: stripeFrontConfig,
    functions: {
      createCheckout: 'createCheckout',
      webhook: 'handleStripeWebhook',
    },
  },
};

6. Deploy

firebase deploy --only functions

That's it! Your billing system is live. 🎉

Summary:

  • Backend: stripeBackConfig.ts (with hooks) → Functions
  • Frontend: stripeFrontConfig.ts (display only) → App
  • Security: Frontend config has no hooks/secrets, safe to bundle

Idempotency

The framework automatically prevents duplicate webhook processing using smart idempotency.

How It Works

The framework detects your environment and chooses the best storage:

  • Firestore enabled → Uses Firestore (production-ready, scales to millions)
  • ⚠️ Firestore not enabled → Uses in-memory (works for development, limited for production)

Zero configuration required - it just works.

When to Enable Firestore

We recommend enabling Firestore when you have:

  • 📈 > 100 transactions per day
  • 🚀 Multiple function instances (auto-scaling)
  • 💰 Revenue-critical operations (prevent duplicates)

How to Enable Firestore

If you don't have Firestore yet:

  1. Enable Firestore (one-time, 2 minutes)

    • Go to Firebase Console
    • Click Firestore DatabaseCreate database
    • Choose your location (e.g., us-central1)
    • Select Start in production mode
    • Click Enable
  2. Add Security Rules (copy-paste, 1 minute)

    Add to firestore.rules:

    rules_version = '2';
    service cloud.firestore {
      match /databases/{database}/documents {
        // Webhook idempotency - Functions only
        match /webhook_idempotency/{document=**} {
          allow read, write: if false;
        }
      }
    }
  3. Deploy Rules

    firebase deploy --only firestore:rules
  4. Redeploy Functions (automatic upgrade)

    firebase deploy --only functions

Done! Framework automatically detects Firestore and upgrades to production-ready idempotency.

Cost

Firestore costs are minimal for billing:

| Transactions/Month | Firestore Cost | | ------------------ | -------------- | | 100 | < $0.01 | | 1,000 | ~$0.50 | | 10,000 | ~$2 | | 100,000 | ~$10 |

Much cheaper than alternatives (Stripe Billing: $10/mo minimum, Chargebee: $249/mo minimum).

Health Check

Check your idempotency status:

curl https://YOUR-REGION-YOUR-PROJECT.cloudfunctions.net/billingHealth

Response:

{
  "status": "healthy",
  "timestamp": 1234567890,
  "idempotency": "firestore" // or "in-memory"
}

Common Use Cases

SaaS Subscription Plans

// Backend: apps/your-app/functions/src/config/stripeBackConfig.ts
export const stripeBackConfig: StripeBackConfig = {
  starter_monthly: {
    type: 'StripeSubscription',
    name: 'Starter',
    price: 1900, // In cents (19.00 USD)
    currency: 'USD',
    priceId: 'price_starter_monthly',
    tier: 'starter',
    duration: '1month',
    onSubscriptionCreated: async (userId) => {
      await enableFeatures(userId, ['feature1', 'feature2']);
    },
  },
  pro_monthly: {
    type: 'StripeSubscription',
    name: 'Pro',
    price: 4900, // In cents (49.00 USD)
    currency: 'USD',
    priceId: 'price_pro_monthly',
    tier: 'pro',
    duration: '1month',
    onSubscriptionCreated: async (userId) => {
      await enableFeatures(userId, ['feature1', 'feature2', 'feature3']);
    },
  },
};

// Frontend: apps/your-app/src/config/stripeFrontConfig.ts
export const stripeFrontConfig: StripeFrontConfig = {
  starter_monthly: {
    name: 'Starter',
    price: 19, // Display price
    currency: 'USD',
    priceId: import.meta.env.VITE_STRIPE_PRICE_STARTER_MONTHLY || '',
    description: 'Perfect for getting started',
    features: ['Feature 1', 'Feature 2'],
  },
  pro_monthly: {
    name: 'Pro',
    price: 49, // Display price
    currency: 'USD',
    priceId: import.meta.env.VITE_STRIPE_PRICE_PRO_MONTHLY || '',
    description: 'For growing businesses',
    features: ['Feature 1', 'Feature 2', 'Feature 3'],
  },
};

Digital Products (Courses, eBooks)

// Backend: functions/src/config/stripeBackConfig.ts
export const stripeBackConfig: StripeBackConfig = {
  react_course: {
    type: 'StripePayment',
    name: 'React Masterclass',
    price: 19900, // In cents (199.00 USD)
    currency: 'USD',
    priceId: process.env.STRIPE_PRICE_REACT_COURSE!,
    tier: 'course_react',
    duration: 'lifetime',
    onPurchaseSuccess: async (userId, metadata) => {
      await grantCourseAccess(userId, 'react-masterclass');
      await sendCourseCredentials(userId);
    },
  },
};

// Frontend: src/config/stripeFrontConfig.ts
export const stripeFrontConfig: StripeFrontConfig = {
  react_course: {
    name: 'React Masterclass',
    price: 199, // Display price
    currency: 'USD',
    priceId: import.meta.env.VITE_STRIPE_PRICE_REACT_COURSE || '',
    description: 'Complete React course with lifetime access',
    features: ['50+ hours', 'Downloadable resources', 'Community support'],
  },
};

Environment Variables

Required

# Stripe API Keys
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
STRIPE_API_VERSION=2025-08-27.basil  # REQUIRED - No fallback

# Stripe Price IDs
VITE_STRIPE_PRICE_COURSE=price_xxx
VITE_STRIPE_PRICE_PRO_MONTHLY=price_xxx
VITE_STRIPE_PRICE_PRO_YEARLY=price_xxx

# Frontend
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_xxx

Optional (Firebase)

If not running in Firebase Functions environment:

FIREBASE_PROJECT_ID=your-project-id
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxx@your-project.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"

Components

PurchaseButton

import { PurchaseButton } from '@donotdev/billing';
import { stripeFrontConfig } from '../config/stripeFrontConfig';

<PurchaseButton
  configKey="react_course"
  config={stripeFrontConfig.react_course}
  successUrl="/success"
  cancelUrl="/pricing"
/>

SubscriptionButton

import { SubscriptionButton } from '@donotdev/billing';
import { stripeFrontConfig } from '../config/stripeFrontConfig';

<SubscriptionButton
  configKey="pro_monthly"
  config={stripeFrontConfig.pro_monthly}
  successUrl="/success"
  cancelUrl="/pricing"
/>

PaymentTemplate

Auto-renders all products from your frontend config:

import { PaymentTemplate } from '@donotdev/templates';
import { stripeFrontConfig } from '../config/stripeFrontConfig';

<PaymentTemplate
  namespace="pricing"
  meta={{ namespace: 'pricing', auth: { required: true } }}
  billing={stripeFrontConfig}
  successUrl="/success"
  cancelUrl="/pricing"
/>

Custom Hooks

All hooks are optional and run after subscription updates:

One-Time Payments

onPurchaseSuccess?: (userId: string, metadata: any) => Promise<void>;
onPurchaseFailure?: (userId: string, metadata: any) => Promise<void>;

Example:

onPurchaseSuccess: async (userId, metadata) => {
  // Grant access to purchased content
  await database.update('users', userId, {
    hasCourseAccess: true,
    purchasedAt: new Date(),
  });

  // Send confirmation email
  await sendEmail(userId, 'purchase-confirmation');

  // Track analytics
  await analytics.track('purchase_completed', { userId, product: 'course' });
};

Subscriptions

onSubscriptionCreated?: (userId: string, metadata: any) => Promise<void>;
onSubscriptionRenewed?: (userId: string, metadata: any) => Promise<void>;
onSubscriptionCancelled?: (userId: string, metadata: any) => Promise<void>;
onPaymentFailed?: (userId: string, metadata: any) => Promise<void>;

Example:

onSubscriptionCreated: async (userId, metadata) => {
  // Enable premium features
  await database.update('users', userId, {
    tier: 'pro',
    features: ['advanced-analytics', 'priority-support'],
  });

  // Send welcome email
  await sendEmail(userId, 'welcome-pro-plan');
};

onSubscriptionCancelled: async (userId, metadata) => {
  // Schedule downgrade at period end
  await database.update('users', userId, {
    scheduledDowngrade: true,
    downgradeTo: 'free',
  });

  // Send cancellation email with feedback request
  await sendEmail(userId, 'subscription-cancelled');
};

Hook Error Handling

Hooks are wrapped in try-catch blocks. If a hook fails:

  • ✅ Error is logged (non-critical)
  • ✅ Webhook continues processing
  • ✅ Subscription is still updated
  • ✅ User is not affected

Example:

onPurchaseSuccess: async (userId, metadata) => {
  // Grant access to external service
  await sendToSlack(userId); // If Slack API fails...

  // Result:
  // - Error logged: "Hook failed (non-critical)"
  // - User subscription still updated
  // - User still gets access
  // - You can retry manually later
};

TypeScript Types

import type {
  StripeFrontConfig, // Frontend config (display only)
  StripeBackConfig, // Backend config (with hooks)
  StripePayment,
  StripeSubscription,
} from '@donotdev/types';

Note: Frontend and backend configs are separate:

  • Frontend (StripeFrontConfig): Display-only, safe to bundle in client code
  • Backend (StripeBackConfig): Includes hooks and business logic, must stay in functions

Testing

Test Webhooks (Stripe CLI)

# Install Stripe CLI
brew install stripe/stripe-cli/stripe

# Login
stripe login

# Forward webhooks to local
stripe listen --forward-to localhost:5001/YOUR-PROJECT/us-central1/stripeWebhook

# Trigger test webhook
stripe trigger checkout.session.completed

Test Idempotency

Send the same webhook twice:

# First time - processes successfully
stripe trigger checkout.session.completed

# Second time - logs "Already processed"
stripe trigger checkout.session.completed

Architecture

User clicks "Purchase"
  ↓
Frontend calls Stripe directly (no proxy needed)
  ↓
User completes payment on Stripe
  ↓
Stripe sends webhook to your endpoint
  ↓
Framework processes webhook:
  ├─ Verifies signature ✅
  ├─ Checks idempotency (Firestore or in-memory) ✅
  ├─ Updates user subscription (customClaims) ✅
  ├─ Calls your custom hooks ✅
  └─ Marks event as processed ✅
  ↓
User has access ✅

Troubleshooting

"Firestore not configured, using in-memory"

This is normal for development. Framework works immediately with in-memory storage.

To upgrade to production:

  1. Enable Firestore (see above)
  2. Redeploy functions
  3. Framework auto-detects and upgrades

"Webhook signature verification failed"

Check:

  1. STRIPE_WEBHOOK_SECRET matches Stripe Dashboard
  2. Using raw request body (not parsed JSON)
  3. Webhook endpoint URL is correct

"Unknown billing config key"

Check:

  1. billingConfigKey in metadata matches your stripeBackConfig keys
  2. You're passing configKey to components (not config.name)
  3. Backend config (stripeBackConfig) has the matching key

Duplicate Processing

With Firestore: Should never happen (production-ready)

Without Firestore: Possible on function restart (rare)

Solution: Enable Firestore for production (> 100 transactions/day)


Advanced

Custom Metadata

Pass custom data to hooks:

import { stripeFrontConfig } from '../config/stripeFrontConfig';

<PurchaseButton
  configKey="react_course"
  config={stripeFrontConfig.react_course}
  metadata={{
    referralCode: 'FRIEND20',
    campaignId: 'summer-sale',
  }}
/>

Access in hooks:

onPurchaseSuccess: async (userId, metadata) => {
  if (metadata.referralCode) {
    await rewardReferrer(metadata.referralCode);
  }
  await trackCampaign(metadata.campaignId);
};

Manual Idempotency Check

import { createIdempotencyStore } from '@donotdev/functions/firebase/billing';

const store = createIdempotencyStore();

// Check if processed
const processed = await store.isProcessed('evt_123');

// Mark as processed
await store.markProcessed('evt_123');