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

@crovver/sdk

v0.1.0

Published

Crovver SDK for subscription management

Readme

Crovver SDK

Official TypeScript/JavaScript SDK for integrating Crovver subscription management into your SaaS application.

Features

Complete API Coverage - All 7 public endpoints implemented
TypeScript Support - Full type definitions included
B2B & D2C Support - Works with both organization types
Error Handling - Built-in error handling with custom error types
Entitlement Checks - Feature access and usage limit verification
Subscription Management - Create checkouts, manage subscriptions
Tenant Management - Create and manage tenants (B2B)

Installation

npm install @crovver/sdk
# or
yarn add @crovver/sdk
# or
pnpm add @crovver/sdk

Quick Start

import { CrovverClient } from "@crovver/sdk";

// Initialize the client
const crovver = new CrovverClient({
  apiKey: "your-api-key-here",
  baseUrl: "https://your-crovver-instance.com", // Optional, defaults to localhost:3000
  timeout: 30000, // Optional, defaults to 30000ms
});

// Check if a user can access a feature
const canAccess = await crovver.canAccess("tenant-123", "advanced-analytics");

if (canAccess) {
  // Grant access to the feature
  console.log("Access granted!");
}

API Reference

Configuration

interface CrovverConfig {
  apiKey: string; // Your organization's API key (required)
  baseUrl?: string; // Crovver instance URL (optional)
  timeout?: number; // Request timeout in ms (optional)
}

Error Handling

The SDK throws CrovverError for all API errors:

import { CrovverClient, CrovverError } from "@crovver/sdk";

try {
  await crovver.canAccess("tenant-123", "premium-feature");
} catch (error) {
  if (error instanceof CrovverError) {
    console.error("Error:", error.message);
    console.error("Status Code:", error.statusCode);
    console.error("Original Error:", error.originalError);
  }
}

🏢 Tenant Management (B2B Only)

Create Tenant

B2B organizations only. For D2C organizations, tenants are automatically created during checkout.

const result = await crovver.createTenant({
  externalTenantId: "company-123", // Your app's company/workspace ID
  name: "Acme Corporation", // Company name
  slug: "acme-corp", // URL-friendly slug (optional)
  ownerExternalUserId: "user-456", // Owner's user ID from your app
  ownerEmail: "[email protected]", // Owner's email (optional)
  ownerName: "John Doe", // Owner's name (optional)
  metadata: {
    // Optional metadata
    industry: "Technology",
    size: "50-100",
  },
});

console.log("Tenant created:", result.tenant.id);
console.log("Owner:", result.owner.id);

Response:

{
  tenant: {
    id: "uuid",
    externalTenantId: "company-123",
    name: "Acme Corporation",
    slug: "acme-corp",
    isActive: true,
    metadata: { ... },
    createdAt: "2026-02-02T10:00:00Z"
  },
  owner: {
    id: "uuid",
    externalUserId: "user-456",
    email: "[email protected]",
    name: "John Doe"
  },
  message: "Tenant created successfully"
}

Get Tenant

const tenant = await crovver.getTenant("company-123");

💳 Plans

Get All Plans

Fetch all available plans for your organization. Use this to render pricing pages.

const { data } = await crovver.getPlans();

console.log(`Found ${data.plans.length} plans`);

data.plans.forEach((plan) => {
  console.log(
    `${plan.name}: ${plan.pricing.currency} ${plan.pricing.amount}/${plan.pricing.interval}`
  );
  console.log("Features:", plan.features);
  console.log("Limits:", plan.limits);
});

Response:

{
  success: true,
  data: {
    plans: [
      {
        id: "plan-uuid",
        name: "Pro Plan",
        description: "For growing teams",
        pricing: {
          amount: 99,
          currency: "USD",
          interval: "month"
        },
        trial: {
          days: 14,
          available: true
        },
        features: {
          "advanced-analytics": true,
          "priority-support": true,
          "custom-branding": false
        },
        limits: {
          "api-calls": 10000,
          "storage-gb": 100,
          "team-members": 25
        },
        product: {
          id: "product-uuid",
          name: "My SaaS Product",
          description: "..."
        },
        payment_providers: [...],
        created_at: "2026-01-01T00:00:00Z",
        updated_at: "2026-01-01T00:00:00Z"
      }
    ],
    organization: {
      id: "org-uuid",
      name: "My Organization",
      type: "b2b"
    }
  }
}

📊 Subscriptions

Get Subscriptions

Fetch active subscriptions for a tenant (B2B) or user (D2C).

// B2B: Use external tenant ID
const { data } = await crovver.getSubscriptions("company-123");

// D2C: Use external user ID
const { data } = await crovver.getSubscriptions("user-789");

console.log(
  `${data.tenant.name} has ${data.subscriptions.length} subscriptions`
);

data.subscriptions.forEach((sub) => {
  console.log(`Plan: ${sub.plan.name}`);
  console.log(`Status: ${sub.status}`);
  console.log(`Next billing: ${sub.billing.next_billing_date}`);

  if (sub.trial.is_active) {
    console.log(`Trial ends in ${sub.trial.days_remaining} days`);
  }

  if (sub.cancellation.is_canceled) {
    console.log(`Will end on: ${sub.cancellation.will_end_at}`);
  }
});

Response:

{
  success: true,
  data: {
    subscriptions: [
      {
        id: "sub-uuid",
        status: "active",
        provider_subscription_id: "stripe_sub_123",
        billing: {
          current_period_start: "2026-01-01T00:00:00Z",
          current_period_end: "2026-02-01T00:00:00Z",
          next_billing_date: "2026-02-01T00:00:00Z"
        },
        trial: {
          is_active: false,
          ends_at: null,
          days_remaining: 0
        },
        cancellation: {
          is_canceled: false,
          canceled_at: null,
          will_end_at: null
        },
        plan: { ... },
        product: { ... },
        payment_providers: [...],
        metadata: {},
        created_at: "2026-01-01T00:00:00Z",
        updated_at: "2026-01-01T00:00:00Z"
      }
    ],
    tenant: {
      id: "tenant-uuid",
      external_tenant_id: "company-123",
      name: "Acme Corporation",
      slug: "acme-corp",
      metadata: {}
    },
    organization: { ... }
  }
}

🛒 Checkout

Create Checkout Session

Create a payment checkout session. Redirects the user to the payment provider.

B2B Example:

const checkout = await crovver.createCheckoutSession({
  requestingUserId: "user-456", // User making the purchase
  requestingTenantId: "company-123", // Tenant (required for B2B)
  planId: "plan-uuid",
  provider: "stripe", // 'stripe' | 'khalti' | 'esewa'
  successUrl: "https://myapp.com/success",
  cancelUrl: "https://myapp.com/cancel",
  metadata: {
    source: "pricing-page",
    coupon: "SAVE20",
  },
});

// Redirect user to checkout
window.location.href = checkout.checkoutUrl;

D2C Example:

// For D2C, tenant is auto-created - provide user details
const checkout = await crovver.createCheckoutSession({
  requestingUserId: "user-789",
  userEmail: "[email protected]", // Required for D2C
  userName: "John Doe", // Required for D2C
  planId: "plan-uuid",
  provider: "stripe",
  successUrl: "https://myapp.com/success",
  cancelUrl: "https://myapp.com/cancel",
});

window.location.href = checkout.checkoutUrl;

Response:

{
  subscriptionId: "sub-uuid",
  checkoutUrl: "https://checkout.stripe.com/...",
  sessionId: "cs_test_..."
}

🔐 Entitlement Checks

Check Feature Access

Verify if a tenant/user has access to a specific feature.

// B2B: Check tenant access
const canAccess = await crovver.canAccess("company-123", "advanced-analytics");

if (canAccess) {
  // Show the feature
  console.log("Feature enabled!");
} else {
  // Show upgrade prompt
  console.log("Please upgrade to access this feature");
}

// D2C: Check user access
const canExport = await crovver.canAccess("user-789", "export-data");

Returns: boolean


📈 Usage Tracking

Record Usage

Track usage for metered features.

// Record API call
await crovver.recordUsage(
  "company-123", // Entity ID
  "api-calls", // Metric key
  1, // Value (default: 1)
  {
    // Metadata (optional)
    endpoint: "/api/v1/users",
    method: "GET",
    responseTime: 45,
  }
);

// Record storage usage
await crovver.recordUsage("company-123", "storage-gb", 5.2);

// Record custom event (increment by 1)
await crovver.recordUsage("company-123", "reports-generated");

Check Usage Limits

Check current usage against limits before allowing an action.

const usage = await crovver.checkUsageLimit("company-123", "api-calls");

console.log(`Used: ${usage.current} / ${usage.limit}`);
console.log(`Remaining: ${usage.remaining}`);
console.log(`Percentage: ${usage.percentage}%`);

if (!usage.allowed) {
  // Show upgrade prompt or limit reached message
  console.log("Usage limit reached! Please upgrade your plan.");
} else {
  // Allow the action
  await performApiCall();

  // Record the usage
  await crovver.recordUsage("company-123", "api-calls");
}

Response:

{
  allowed: true,
  current: 7500,
  limit: 10000,
  remaining: 2500,
  percentage: 75
}

🎯 Common Patterns

Feature Gate Pattern

async function withFeatureGate(
  entityId: string,
  feature: string,
  action: () => Promise<void>
) {
  const canAccess = await crovver.canAccess(entityId, feature);

  if (!canAccess) {
    throw new Error(`Feature '${feature}' not available on your plan`);
  }

  await action();
}

// Usage
await withFeatureGate("company-123", "advanced-analytics", async () => {
  // Your feature code here
  await generateAdvancedReport();
});

Usage Limit Guard

async function withUsageCheck(
  entityId: string,
  metric: string,
  action: () => Promise<void>
) {
  const usage = await crovver.checkUsageLimit(entityId, metric);

  if (!usage.allowed) {
    throw new Error(`Usage limit reached for '${metric}'`);
  }

  await action();

  // Record the usage after successful action
  await crovver.recordUsage(entityId, metric);
}

// Usage
await withUsageCheck("company-123", "api-calls", async () => {
  await makeApiCall();
});

Subscription Status Check

async function requireActiveSubscription(entityId: string) {
  const { data } = await crovver.getSubscriptions(entityId);

  const hasActive = data.subscriptions.some(
    (sub) => sub.status === "active" || sub.status === "trial"
  );

  if (!hasActive) {
    throw new Error("No active subscription found");
  }

  return data.subscriptions[0]; // Return the first active subscription
}

🔄 Organization Types

Crovver supports two organization types:

B2B (Business-to-Business)

  • Tenant-centric: Multiple tenants (companies/workspaces) per organization
  • Manual tenant creation: Call createTenant() when a company signs up
  • Subscriptions belong to tenants: Each tenant has its own subscription
  • Use requestingTenantId: Pass tenant ID for entitlement checks
// Create tenant first
await crovver.createTenant({
  externalTenantId: "company-123",
  name: "Acme Corp",
  ownerExternalUserId: "user-456",
});

// Then check access using tenant ID
const canAccess = await crovver.canAccess("company-123", "feature");

D2C (Direct-to-Consumer)

  • User-centric: One tenant per user
  • Auto tenant creation: Tenant created automatically during checkout
  • Subscriptions belong to users: Each user has their own subscription
  • Use requestingUserId: Pass user ID for entitlement checks
// No manual tenant creation needed
// Just create checkout - tenant auto-created
const checkout = await crovver.createCheckoutSession({
  requestingUserId: "user-789",
  userEmail: "[email protected]",
  userName: "John Doe",
  planId: "plan-uuid",
  provider: "stripe",
  successUrl: "https://myapp.com/success",
  cancelUrl: "https://myapp.com/cancel",
});

// Check access using user ID
const canAccess = await crovver.canAccess("user-789", "feature");

🛠️ Framework Integration Examples

Next.js API Route

// app/api/check-feature/route.ts
import { CrovverClient } from "@crovver/sdk";
import { NextResponse } from "next/server";

const crovver = new CrovverClient({
  apiKey: process.env.CROVVER_API_KEY!,
  baseUrl: process.env.CROVVER_BASE_URL,
});

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

  try {
    const canAccess = await crovver.canAccess(tenantId, feature);
    return NextResponse.json({ canAccess });
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to check feature access" },
      { status: 500 }
    );
  }
}

Express.js Middleware

// middleware/checkFeature.ts
import { CrovverClient, CrovverError } from "@crovver/sdk";
import { Request, Response, NextFunction } from "express";

const crovver = new CrovverClient({
  apiKey: process.env.CROVVER_API_KEY!,
  baseUrl: process.env.CROVVER_BASE_URL,
});

export function requireFeature(featureKey: string) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const tenantId = req.user?.tenantId; // Your auth logic

    if (!tenantId) {
      return res.status(401).json({ error: "Unauthorized" });
    }

    try {
      const canAccess = await crovver.canAccess(tenantId, featureKey);

      if (!canAccess) {
        return res.status(403).json({
          error: "Feature not available",
          message: `Your plan does not include ${featureKey}`,
        });
      }

      next();
    } catch (error) {
      if (error instanceof CrovverError) {
        return res.status(error.statusCode || 500).json({
          error: error.message,
        });
      }
      next(error);
    }
  };
}

// Usage
app.get("/api/analytics", requireFeature("advanced-analytics"), (req, res) => {
  // Your analytics endpoint
});

React Hook

// hooks/useCrovver.ts
import { useState, useEffect } from "react";
import { CrovverClient } from "@crovver/sdk";

const crovver = new CrovverClient({
  apiKey: process.env.NEXT_PUBLIC_CROVVER_API_KEY!,
  baseUrl: process.env.NEXT_PUBLIC_CROVVER_BASE_URL,
});

export function useFeatureAccess(tenantId: string, feature: string) {
  const [canAccess, setCanAccess] = useState<boolean>(false);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function checkAccess() {
      try {
        setLoading(true);
        const access = await crovver.canAccess(tenantId, feature);
        if (!cancelled) {
          setCanAccess(access);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err as Error);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    checkAccess();

    return () => {
      cancelled = true;
    };
  }, [tenantId, feature]);

  return { canAccess, loading, error };
}

// Usage in component
function AnalyticsPage() {
  const { canAccess, loading } = useFeatureAccess(
    "company-123",
    "advanced-analytics"
  );

  if (loading) return <div>Loading...</div>;

  if (!canAccess) {
    return <UpgradePrompt feature="advanced-analytics" />;
  }

  return <AdvancedAnalyticsDashboard />;
}

📝 TypeScript Support

The SDK is written in TypeScript and includes full type definitions. All request and response types are exported:

import {
  CrovverClient,
  CrovverError,
  CrovverConfig,
  CreateTenantRequest,
  CreateTenantResponse,
  GetPlansResponse,
  GetSubscriptionsResponse,
  CreateCheckoutSessionRequest,
  CreateCheckoutSessionResponse,
  CheckUsageLimitResponse,
  Plan,
  Subscription,
  Tenant,
} from "@crovver/sdk";

🐛 Error Handling

All errors thrown by the SDK are instances of CrovverError:

class CrovverError extends Error {
  statusCode?: number; // HTTP status code (if available)
  originalError?: any; // Original error object
}

Common Error Codes:

  • 401 - Invalid API key or unauthorized
  • 403 - Forbidden (e.g., D2C org trying to create tenant manually)
  • 404 - Resource not found (tenant, subscription, etc.)
  • 409 - Conflict (e.g., duplicate tenant)
  • 500 - Internal server error

🤝 Support


📄 License

MIT License - see LICENSE file for details