Skip to content

Licensing System

AuthVital's flexible license pool system for SaaS monetization.

Overview

AuthVital includes a sophisticated license pool system that enables:

  • Per-seat licensing (assign licenses to individual users)
  • Tenant-wide access (whole organization gets access)
  • Free tiers with automatic provisioning
  • Feature flags tied to license types
  • Subscription management with seat counts

Licensing Modes

Each application can be configured with one of three licensing modes:

Mode Description Use Case
FREE All users automatically get access Free products, open-source tools
PER_SEAT Each user needs an assigned license seat Traditional SaaS (Slack, Jira)
TENANT_WIDE Tenant subscribes, all members get access Team plans (Notion, Figma)

FREE Mode

┌─────────────────────────────────────────────┐
│                 Application                  │
│            licensingMode: FREE               │
├─────────────────────────────────────────────┤
│  User joins tenant → Automatic access ✓     │
│  No seat limits                              │
│  No subscription required                    │
└─────────────────────────────────────────────┘

Configuration:

// Application settings
{
  licensingMode: 'FREE',
  autoProvisionOnSignup: true, // Automatically grant access
}

PER_SEAT Mode

┌─────────────────────────────────────────────┐
│               Tenant: Acme Corp              │
├─────────────────────────────────────────────┤
│  Subscription: Pro Plan (10 seats)          │
│  ├─ Seat 1: alice@acme.com ✓                │
│  ├─ Seat 2: bob@acme.com ✓                  │
│  ├─ Seat 3: carol@acme.com ✓                │
│  ├─ Seats 4-10: Available                   │
│  └─ dave@acme.com: No seat (access denied)  │
└─────────────────────────────────────────────┘

Configuration:

{
  licensingMode: 'PER_SEAT',
  autoProvisionOnSignup: true,  // Create subscription on tenant signup
  defaultLicenseTypeId: 'license-type-pro',
  defaultSeatCount: 5,          // Initial seats
  autoGrantToOwner: true,       // Owner gets first seat automatically
}

TENANT_WIDE Mode

┌─────────────────────────────────────────────┐
│               Tenant: Acme Corp              │
├─────────────────────────────────────────────┤
│  Subscription: Enterprise Plan              │
│  All 50 members have access ✓               │
│  No individual seat assignments             │
└─────────────────────────────────────────────┘

Configuration:

{
  licensingMode: 'TENANT_WIDE',
  autoProvisionOnSignup: true,
  defaultLicenseTypeId: 'license-type-enterprise',
}

Data Model

erDiagram
    Application ||--o{ LicenseType : "defines"
    LicenseType ||--o{ AppSubscription : "purchased as"
    Tenant ||--o{ AppSubscription : "owns"
    AppSubscription ||--o{ LicenseAssignment : "grants"
    User ||--o{ LicenseAssignment : "receives"

    Application {
        string id PK
        string name
        enum licensingMode
        string defaultLicenseTypeId FK
        int defaultSeatCount
        json availableFeatures
    }

    LicenseType {
        string id PK
        string name
        string slug
        string applicationId FK
        json features
        int displayOrder
    }

    AppSubscription {
        string id PK
        string tenantId FK
        string licenseTypeId FK
        int quantityPurchased
        enum status
        datetime currentPeriodEnd
    }

    LicenseAssignment {
        string id PK
        string userId FK
        string subscriptionId FK
        string tenantId FK
        datetime assignedAt
    }

License Types

License types define tiers within an application:

interface LicenseType {
  id: string;
  name: string;           // "Pro Plan"
  slug: string;           // "pro"
  description: string;
  applicationId: string;
  features: Record<string, boolean>;  // Feature flags
  displayOrder: number;   // For UI ordering
}

// Example license types
const licenseTypes = [
  {
    name: 'Free',
    slug: 'free',
    features: {
      'basic-reports': true,
      'api-access': false,
      'sso': false,
    },
  },
  {
    name: 'Pro',
    slug: 'pro',
    features: {
      'basic-reports': true,
      'advanced-reports': true,
      'api-access': true,
      'sso': false,
    },
  },
  {
    name: 'Enterprise',
    slug: 'enterprise',
    features: {
      'basic-reports': true,
      'advanced-reports': true,
      'api-access': true,
      'sso': true,
      'audit-logs': true,
      'custom-branding': true,
    },
  },
];

Subscriptions

A subscription represents a tenant's purchase of license seats:

interface AppSubscription {
  id: string;
  tenantId: string;
  applicationId: string;
  licenseTypeId: string;
  quantityPurchased: number;  // Total seats purchased
  quantityAssigned: number;   // Seats currently assigned (computed)
  quantityAvailable: number;  // Seats remaining (computed)
  status: 'ACTIVE' | 'TRIALING' | 'PAST_DUE' | 'CANCELED' | 'EXPIRED';
  currentPeriodEnd: Date;     // When subscription renews/expires
}

Subscription Status

Status Description
ACTIVE Subscription is current and paid
TRIALING In trial period
PAST_DUE Payment failed, grace period
CANCELED Canceled but not yet expired
EXPIRED No longer valid

Feature Flags

Features are boolean flags tied to license types:

// Define available features in application settings
const availableFeatures = [
  { key: 'api-access', name: 'API Access', description: 'REST API access' },
  { key: 'sso', name: 'Single Sign-On', description: 'SAML/OIDC SSO' },
  { key: 'advanced-reports', name: 'Advanced Reports', description: 'Custom dashboards' },
  { key: 'audit-logs', name: 'Audit Logs', description: 'Activity tracking' },
];

// Check feature in your app
const { hasFeature } = await authvital.licenses.hasFeature(req, {
  applicationId: 'app-123',
  feature: 'advanced-reports',
});

if (!hasFeature) {
  return res.status(402).json({
    error: 'Feature not available',
    feature: 'advanced-reports',
    upgradeUrl: '/pricing',
  });
}

JWT Claims

License information is included in the JWT:

{
  "sub": "user-123",
  "email": "user@acme.com",
  "tenant_id": "tenant-456",

  "license": {
    "type": "pro",
    "name": "Pro Plan",
    "features": ["basic-reports", "advanced-reports", "api-access"]
  }
}

Access in your code:

const { user } = await authvital.getCurrentUser(req);

// Check license type
if (user.license?.type === 'enterprise') {
  // Premium features
}

// Check specific feature
if (user.license?.features.includes('sso')) {
  // SSO enabled
}

SDK Methods

Check License

// Check if user has any license
const { hasLicense, licenseType, features } = await authvital.licenses.check(req, {
  applicationId: 'app-123',
});

console.log(hasLicense);    // true
console.log(licenseType);   // "pro"
console.log(features);      // ["basic-reports", "api-access"]

Check Feature

const { hasFeature } = await authvital.licenses.hasFeature(req, {
  applicationId: 'app-123',
  feature: 'advanced-reports',
});

Get Full License Details

const license = await authvital.licenses.getUserLicense(req, {
  applicationId: 'app-123',
});
// {
//   type: "pro",
//   typeId: "lt-123",
//   name: "Pro Plan",
//   features: ["basic-reports", "api-access"],
//   assignedAt: "2024-01-15T...",
//   subscription: {
//     id: "sub-456",
//     status: "ACTIVE",
//     currentPeriodEnd: "2024-02-15T..."
//   }
// }

Admin: Grant License

await authvital.licenses.grant(req, {
  userId: 'user-123',
  applicationId: 'app-123',
  licenseTypeId: 'lt-pro',
  tenantId: 'tenant-456',
});

Admin: Revoke License

await authvital.licenses.revoke(req, {
  userId: 'user-123',
  applicationId: 'app-123',
  tenantId: 'tenant-456',
});

Get Tenant License Overview

const overview = await authvital.licenses.getTenantOverview(req, {
  tenantId: 'tenant-456',
});
// {
//   tenantId: "tenant-456",
//   subscriptions: [
//     {
//       id: "sub-1",
//       applicationName: "Project Manager",
//       licenseTypeName: "Pro Plan",
//       quantityPurchased: 10,
//       quantityAssigned: 7,
//       quantityAvailable: 3,
//       status: "ACTIVE",
//       features: { "api-access": true, "sso": false }
//     }
//   ],
//   totalSeatsOwned: 10,
//   totalSeatsAssigned: 7
// }

Middleware Examples

License Gate

const requireLicense = (applicationId: string) => async (req, res, next) => {
  const { hasLicense } = await authvital.licenses.check(req, {
    applicationId,
  });

  if (!hasLicense) {
    return res.status(402).json({
      error: 'License required',
      message: 'Please purchase a license to access this feature',
      upgradeUrl: '/pricing',
    });
  }

  next();
};

// Usage
app.use('/api/premium', requireLicense('app-123'));

Feature Gate

const requireFeature = (applicationId: string, feature: string) => {
  return async (req, res, next) => {
    const { hasFeature } = await authvital.licenses.hasFeature(req, {
      applicationId,
      feature,
    });

    if (!hasFeature) {
      return res.status(402).json({
        error: 'Feature not available',
        feature,
        message: `Upgrade your plan to access ${feature}`,
        upgradeUrl: '/pricing',
      });
    }

    next();
  };
};

// Usage
app.get('/api/analytics', 
  requireAuth,
  requireFeature('app-123', 'advanced-analytics'),
  analyticsHandler
);

React Integration

License Context

function PremiumFeature() {
  const { user } = useAuth();
  const hasFeature = user?.license?.features.includes('advanced-analytics');

  if (!hasFeature) {
    return (
      <div className="upgrade-prompt">
        <p>Upgrade to Pro to access Advanced Analytics</p>
        <a href="/pricing">View Plans</a>
      </div>
    );
  }

  return <AnalyticsDashboard />;
}

Feature Flag Component

function FeatureGate({ 
  feature, 
  children, 
  fallback 
}: { 
  feature: string;
  children: React.ReactNode;
  fallback?: React.ReactNode;
}) {
  const { user } = useAuth();
  const hasFeature = user?.license?.features.includes(feature);

  if (!hasFeature) {
    return fallback || null;
  }

  return <>{children}</>;
}

// Usage
<FeatureGate 
  feature="sso" 
  fallback={<UpgradePrompt feature="SSO" />}
>
  <SsoSettings />
</FeatureGate>

Webhook Events

License changes trigger webhook events:

Event When
license.assigned User granted a license
license.revoked User's license removed
license.changed User's license type changed
class MyEventHandler extends AuthVitalEventHandler {
  async onLicenseAssigned(event) {
    const { sub, license_type_slug, features } = event.data;

    // Provision resources
    await provisionUserStorage(sub, {
      quota: features.includes('unlimited-storage') ? 'unlimited' : '10GB',
    });

    // Send upgrade email
    await sendEmail(event.data.email, 'upgrade-complete', {
      plan: license_type_slug,
    });
  }

  async onLicenseRevoked(event) {
    // Cleanup or downgrade
    await revokeApiKeys(event.data.sub);
  }
}

Integration with Billing

AuthVital manages license assignments, not billing. Integrate with your billing system:

┌──────────────┐    Purchase    ┌──────────────┐    Update     ┌──────────────┐
│   Billing    │ ──────────────▶│   Your API   │ ─────────────▶│  AuthVital   │
│  (Stripe)    │                │              │               │              │
└──────────────┘                └──────────────┘               └──────────────┘
       │                               │                              │
       │ webhook                       │ create/update               │ stores
       └───────────────────────────────┘ subscription                │ assignments
                                                              ┌──────────────┐
                                                              │  JWT claims  │
                                                              │  (license)   │
                                                              └──────────────┘

Example: Stripe Webhook Handler

app.post('/webhooks/stripe', async (req, res) => {
  const event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object;

      // Create subscription in AuthVital
      await authvital.admin.createSubscription({
        tenantId: session.metadata.tenantId,
        applicationId: session.metadata.applicationId,
        licenseTypeId: session.metadata.licenseTypeId,
        quantityPurchased: session.metadata.seats,
        status: 'ACTIVE',
        currentPeriodEnd: new Date(session.current_period_end * 1000),
      });
      break;
    }

    case 'customer.subscription.updated': {
      const subscription = event.data.object;

      // Update seats in AuthVital
      await authvital.admin.updateSubscription(
        subscription.metadata.authvitalSubscriptionId,
        {
          quantityPurchased: subscription.items.data[0].quantity,
          status: subscription.status.toUpperCase(),
        }
      );
      break;
    }
  }

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

Best Practices

1. Cache License Checks

For performance, cache JWT claims (they're self-contained):

// License info is in the JWT - no API call needed!
const { user } = await authvital.getCurrentUser(req);
const hasFeature = user.license?.features.includes('advanced-reports');

2. Handle Grace Periods

Allow access during payment failures:

const subscription = await authvital.licenses.getSubscription(req, {
  applicationId: 'app-123',
});

if (subscription.status === 'PAST_DUE') {
  // Show warning but allow access
  res.set('X-License-Warning', 'Payment past due');
}

3. Provide Upgrade Paths

Always show users how to upgrade:

if (!hasFeature) {
  return res.status(402).json({
    error: 'Feature requires upgrade',
    currentPlan: user.license?.type || 'free',
    requiredPlan: 'pro',
    upgradeUrl: `/pricing?current=${user.license?.type}`,
  });
}