JSPM

  • Created
  • Published
  • Downloads 181
  • Score
    100M100P100Q89298F
  • License MIT

Official JavaScript/TypeScript SDK for StackBE - the billing backend for your side project

Package Exports

  • @stackbe/sdk

Readme

@stackbe/sdk

Official JavaScript/TypeScript SDK for StackBE - the billing backend for your side project.

Installation

npm install @stackbe/sdk

Quick Start

import { StackBE } from '@stackbe/sdk';

const stackbe = new StackBE({
  apiKey: process.env.STACKBE_API_KEY!,
  appId: process.env.STACKBE_APP_ID!,
});

// Track usage
await stackbe.usage.track('customer_123', 'api_calls');

// Check entitlements
const { hasAccess } = await stackbe.entitlements.check('customer_123', 'premium_export');

// Create checkout session
const { url } = await stackbe.checkout.createSession({
  customer: 'cust_123',
  planId: 'plan_pro',
  successUrl: 'https://myapp.com/success',
});

// Get subscription
const subscription = await stackbe.subscriptions.get('cust_123');

// Send magic link
await stackbe.auth.sendMagicLink('user@example.com');

Modules

Usage Tracking

Track billable usage events for your customers:

// Track a single event
await stackbe.usage.track('customer_123', 'api_calls');

// Track multiple units
await stackbe.usage.track('customer_123', 'tokens', { quantity: 1500 });

// Check if within limits
const { allowed, remaining } = await stackbe.usage.check('customer_123', 'api_calls');
if (!allowed) {
  throw new Error('Usage limit exceeded');
}

// Get full usage summary
const usage = await stackbe.usage.get('customer_123');
console.log(usage.metrics);

// Track and check in one call
const result = await stackbe.usage.trackAndCheck('customer_123', 'api_calls');
if (!result.allowed) {
  // Handle limit exceeded
}

Entitlements

Check feature access based on customer's plan:

// Check single feature
const { hasAccess } = await stackbe.entitlements.check('customer_123', 'premium_export');

// Get all entitlements
const { entitlements, planName } = await stackbe.entitlements.getAll('customer_123');
// { premium_export: true, api_access: true, max_projects: 10 }

// Check multiple features at once
const features = await stackbe.entitlements.checkMany('customer_123', [
  'premium_export',
  'advanced_analytics',
]);

// Require a feature (throws if not available)
await stackbe.entitlements.require('customer_123', 'premium_export');

Checkout

Create Stripe checkout sessions:

// With existing customer ID
const { url } = await stackbe.checkout.createSession({
  customer: 'cust_123',
  planId: 'plan_pro_monthly',
  successUrl: 'https://myapp.com/success',
  cancelUrl: 'https://myapp.com/pricing',
});

// Redirect to checkout
res.redirect(url);

// With new customer (will be created)
const { url } = await stackbe.checkout.createSession({
  customer: { email: 'user@example.com', name: 'John' },
  planId: 'plan_pro_monthly',
  successUrl: 'https://myapp.com/success',
  trialDays: 14,
});

// Get checkout URL directly
const checkoutUrl = await stackbe.checkout.getCheckoutUrl({
  customer: 'cust_123',
  planId: 'plan_pro',
  successUrl: 'https://myapp.com/success',
});

Subscriptions

Manage customer subscriptions:

// Get current subscription
const subscription = await stackbe.subscriptions.get('cust_123');
if (subscription) {
  console.log(`Plan: ${subscription.plan.name}`);
  console.log(`Status: ${subscription.status}`);
}

// Check if customer has active subscription
const isActive = await stackbe.subscriptions.isActive('cust_123');

// Cancel subscription (at end of billing period)
await stackbe.subscriptions.cancel('sub_123');

// Cancel immediately
await stackbe.subscriptions.cancel('sub_123', { immediate: true });

// Update subscription (change plan)
await stackbe.subscriptions.update('sub_123', {
  planId: 'plan_enterprise',
  prorate: true,
});

// Reactivate canceled subscription
await stackbe.subscriptions.reactivate('sub_123');

// List all subscriptions
const subscriptions = await stackbe.subscriptions.list('cust_123');

Authentication

Passwordless authentication with magic links:

// Send magic link
await stackbe.auth.sendMagicLink('user@example.com');

// With redirect URL
await stackbe.auth.sendMagicLink('user@example.com', {
  redirectUrl: 'https://myapp.com/dashboard',
});

// For localhost development
await stackbe.auth.sendMagicLink('user@example.com', {
  useDev: true,
});

// Verify magic link token (in your /verify route)
const { token } = req.query;
const result = await stackbe.auth.verifyToken(token);

// result includes tenant and org context:
// - customerId, email, sessionToken
// - tenantId (your StackBE tenant)
// - organizationId, orgRole (if in org context)
res.cookie('session', result.sessionToken, { httpOnly: true });
res.redirect('/dashboard');

// Get session from token (includes tenant context)
const session = await stackbe.auth.getSession(sessionToken);
if (session) {
  console.log(session.customerId);
  console.log(session.email);
  console.log(session.tenantId);       // Tenant context
  console.log(session.organizationId); // Org context (if applicable)
  console.log(session.subscription);
  console.log(session.entitlements);
}

// Check if authenticated
const isAuthenticated = await stackbe.auth.isAuthenticated(sessionToken);

Plans & Products

List available pricing plans for your pricing page:

// List all plans
const plans = await stackbe.plans.list();

// List active plans sorted by price
const plans = await stackbe.plans.listByPrice();
// [Free ($0), Starter ($9), Pro ($29), Enterprise ($99)]

// Filter by product
const plans = await stackbe.plans.list({ productId: 'prod_123' });

// Get a specific plan
const plan = await stackbe.plans.get('plan_123');
console.log(plan.name, plan.priceCents, plan.entitlements);

// List products
const products = await stackbe.products.list();

// Get product details
const product = await stackbe.products.get('prod_123');

Dynamic Pricing Page Example

// Next.js pricing page
export default async function PricingPage() {
  const plans = await stackbe.plans.listByPrice();

  return (
    <div className="grid grid-cols-3 gap-4">
      {plans.map((plan) => (
        <div key={plan.id} className="border p-4 rounded">
          <h3>{plan.name}</h3>
          <p className="text-2xl">
            ${plan.priceCents / 100}/{plan.interval}
          </p>
          <ul>
            {Object.entries(plan.entitlements).map(([key, value]) => (
              <li key={key}>
                {key}: {value === true ? '✓' : value}
              </li>
            ))}
          </ul>
          <a href={`/checkout?plan=${plan.id}`}>
            {plan.priceCents === 0 ? 'Start Free' : 'Subscribe'}
          </a>
        </div>
      ))}
    </div>
  );
}

Customer Management

// Get customer
const customer = await stackbe.customers.get('cust_123');

// Get by email
const customer = await stackbe.customers.getByEmail('user@example.com');

// Create customer
const newCustomer = await stackbe.customers.create({
  email: 'user@example.com',
  name: 'John Doe',
  metadata: { source: 'api' },
});

// Get or create (idempotent)
const customer = await stackbe.customers.getOrCreate({
  email: 'user@example.com',
  name: 'John Doe',
});

// Update customer
await stackbe.customers.update('cust_123', { name: 'Jane Doe' });

Organizations (B2B Multi-User)

Manage customer organizations for B2B apps:

// Create organization with customer as owner
const org = await stackbe.organizations.create({
  name: 'Acme Corp',
  ownerId: customer.id,
});

// List all organizations
const orgs = await stackbe.organizations.list();

// Get organization by ID
const org = await stackbe.organizations.get('org_123');

// Update organization
await stackbe.organizations.update('org_123', { name: 'New Name' });

// Delete organization (must have no active subscriptions)
await stackbe.organizations.delete('org_123');

// Add member to organization
await stackbe.organizations.addMember('org_123', {
  customerId: 'cust_456',
  role: 'member', // 'admin' | 'member'
});

// Remove member
await stackbe.organizations.removeMember('org_123', 'member_456');

// Update member role
await stackbe.organizations.updateMember('org_123', 'member_456', {
  role: 'admin',
});

// Invite by email
await stackbe.organizations.invite('org_123', {
  email: 'newuser@company.com',
  role: 'member',
});

// List pending invites
const invites = await stackbe.organizations.listInvites('org_123');

// Cancel invite
await stackbe.organizations.cancelInvite('org_123', 'invite_456');

B2B Signup Flow

async function signup(email: string, orgName: string) {
  // 1. Get or create customer
  const customer = await stackbe.customers.getOrCreate({ email });

  // 2. Create organization with customer as owner
  const org = await stackbe.organizations.create({
    name: orgName,
    ownerId: customer.id,
  });

  // 3. Send magic link
  await stackbe.auth.sendMagicLink(email);

  return { customer, org };
}

Express Middleware

Track Usage Automatically

app.use(stackbe.middleware({
  getCustomerId: (req) => req.user?.customerId,
  metric: 'api_calls',
  skip: (req) => req.path === '/health',
}));

Require Feature Entitlements

app.get('/api/export',
  stackbe.requireFeature({
    getCustomerId: (req) => req.user?.customerId,
    feature: 'premium_export',
    onDenied: (req, res) => {
      res.status(403).json({ error: 'Upgrade to Pro' });
    },
  }),
  exportHandler
);

Enforce Usage Limits

app.use('/api',
  stackbe.enforceLimit({
    getCustomerId: (req) => req.user?.customerId,
    metric: 'api_calls',
    onLimitExceeded: (req, res, { current, limit }) => {
      res.status(429).json({ error: 'Rate limit exceeded', current, limit });
    },
  })
);

Authenticate Requests

app.use('/dashboard',
  stackbe.auth.middleware({
    getToken: (req) => req.cookies.session,
    onUnauthenticated: (req, res) => res.redirect('/login'),
  })
);

app.get('/dashboard', (req, res) => {
  // req.customer, req.subscription, req.entitlements are available
  res.json({ email: req.customer.email });
});

Next.js Integration

API Routes (App Router)

// app/api/generate/route.ts
import { StackBE } from '@stackbe/sdk';
import { NextResponse } from 'next/server';

const stackbe = new StackBE({
  apiKey: process.env.STACKBE_API_KEY!,
  appId: process.env.STACKBE_APP_ID!,
});

export async function POST(request: Request) {
  const { customerId } = await request.json();

  // Check limits
  const { allowed, remaining } = await stackbe.usage.check(customerId, 'generations');
  if (!allowed) {
    return NextResponse.json({ error: 'Limit reached' }, { status: 429 });
  }

  // Track usage
  await stackbe.usage.track(customerId, 'generations');

  // Do work...
  return NextResponse.json({ success: true, remaining: remaining! - 1 });
}

Server Actions

'use server';

import { StackBE } from '@stackbe/sdk';

const stackbe = new StackBE({
  apiKey: process.env.STACKBE_API_KEY!,
  appId: process.env.STACKBE_APP_ID!,
});

export async function exportData(customerId: string) {
  const { hasAccess } = await stackbe.entitlements.check(customerId, 'data_export');
  if (!hasAccess) {
    throw new Error('Upgrade to Pro to export data');
  }
  // Perform export...
}

Error Handling

The SDK provides typed error codes for specific error handling:

import { StackBE, StackBEError } from '@stackbe/sdk';

try {
  await stackbe.auth.verifyToken(token);
} catch (error) {
  if (error instanceof StackBEError) {
    // Handle specific error types
    switch (error.code) {
      case 'TOKEN_EXPIRED':
        return res.redirect('/login?error=expired');
      case 'TOKEN_ALREADY_USED':
        return res.redirect('/login?error=used');
      case 'SESSION_EXPIRED':
        return res.redirect('/login');
      case 'CUSTOMER_NOT_FOUND':
        return res.status(404).json({ error: 'Customer not found' });
      case 'USAGE_LIMIT_EXCEEDED':
        return res.status(429).json({ error: 'Limit exceeded' });
      default:
        return res.status(500).json({ error: 'Something went wrong' });
    }

    // Or use helper methods
    if (error.isAuthError()) {
      return res.redirect('/login');
    }
    if (error.isNotFoundError()) {
      return res.status(404).json({ error: error.message });
    }
  }
}

Error Codes

Category Codes
Auth TOKEN_EXPIRED, TOKEN_ALREADY_USED, TOKEN_INVALID, SESSION_EXPIRED, SESSION_INVALID, UNAUTHORIZED
Resources NOT_FOUND, CUSTOMER_NOT_FOUND, SUBSCRIPTION_NOT_FOUND, PLAN_NOT_FOUND, APP_NOT_FOUND
Usage USAGE_LIMIT_EXCEEDED, METRIC_NOT_FOUND
Entitlements FEATURE_NOT_AVAILABLE, NO_ACTIVE_SUBSCRIPTION
Validation VALIDATION_ERROR, MISSING_REQUIRED_FIELD, INVALID_EMAIL
Network TIMEOUT, NETWORK_ERROR, UNKNOWN_ERROR

Configuration

const stackbe = new StackBE({
  apiKey: 'sk_live_...',           // Required: Your API key
  appId: 'app_...',                // Required: Your App ID
  baseUrl: 'https://api.stackbe.io', // Optional: API base URL
  timeout: 30000,                   // Optional: Request timeout in ms

  // Session caching (reduces API calls)
  sessionCacheTTL: 120,            // Optional: Cache sessions for 2 minutes

  // Environment-aware redirects
  devCallbackUrl: 'http://localhost:3000/auth/callback', // Optional: Auto-use in dev
});

Session Caching

Enable session caching to reduce API calls on every request:

const stackbe = new StackBE({
  apiKey,
  appId,
  sessionCacheTTL: 120, // Cache for 2 minutes
});

// Cached calls won't hit the API
const session1 = await stackbe.auth.getSession(token); // API call
const session2 = await stackbe.auth.getSession(token); // From cache

// Manually invalidate cache
stackbe.auth.invalidateSession(token);
stackbe.auth.clearCache(); // Clear all

Recommended approach: Pass the callback URL explicitly for maximum reliability:

// Determine callback URL based on your environment
const callbackUrl = process.env.NODE_ENV === 'production'
  ? 'https://myapp.com/auth/callback'
  : 'http://localhost:3000/auth/callback';

await stackbe.auth.sendMagicLink('user@example.com', {
  redirectUrl: callbackUrl,
});

Alternative: Use devCallbackUrl for automatic detection (uses devCallbackUrl when NODE_ENV !== 'production'):

const stackbe = new StackBE({
  apiKey,
  appId,
  devCallbackUrl: 'http://localhost:3000/auth/callback',
});

// In development, uses devCallbackUrl automatically
await stackbe.auth.sendMagicLink('user@example.com');

// In production, falls back to app settings callback URL
// Explicit redirectUrl always takes priority if provided

Notes:

  • devCallbackUrl only activates when NODE_ENV !== 'production'
  • Explicit redirectUrl parameter always takes priority
  • For predictable behavior, use the explicit approach above

Webhooks

Typed webhook payloads for handling StackBE events:

import type {
  AnyWebhookEvent,
  SubscriptionCreatedEvent,
  SubscriptionCancelledEvent,
  PaymentFailedEvent,
} from '@stackbe/sdk';

// In your webhook handler
app.post('/webhooks/stackbe', (req, res) => {
  const event = req.body as AnyWebhookEvent;

  switch (event.type) {
    case 'subscription_created':
      const sub = event as SubscriptionCreatedEvent;
      console.log(`New subscription: ${sub.data.planName}`);
      break;

    case 'subscription_cancelled':
      const cancelled = event as SubscriptionCancelledEvent;
      console.log(`Cancelled: ${cancelled.data.id}`);
      break;

    case 'payment_failed':
      const payment = event as PaymentFailedEvent;
      console.log(`Payment failed: ${payment.data.failureReason}`);
      // Send dunning email
      break;
  }

  res.json({ received: true });
});

Webhook Event Types

Event Payload
subscription_created SubscriptionWebhookPayload
subscription_updated SubscriptionWebhookPayload
subscription_cancelled SubscriptionWebhookPayload
subscription_renewed SubscriptionWebhookPayload
trial_started SubscriptionWebhookPayload
trial_ended SubscriptionWebhookPayload
payment_succeeded PaymentWebhookPayload
payment_failed PaymentWebhookPayload
customer_created CustomerWebhookPayload
customer_updated CustomerWebhookPayload

TypeScript

Full type definitions included:

import type {
  Customer,
  Subscription,
  SubscriptionWithPlan,
  CheckoutSessionResponse,
  SessionResponse,
  TrackUsageResponse,
  CheckEntitlementResponse,
  StackBEErrorCode,
  WebhookEventType,
  AnyWebhookEvent,
} from '@stackbe/sdk';

License

MIT