Skip to content

Repository files navigation

SassyEL - SaaS Runtime Layer

A complete SaaS Enablement Layer providing identity, multi-tenancy, plans, entitlements, team management, webhooks, and an embedded control panel UI for your applications.

Overview

SassyEL (SaaS Enablement Layer) is a self-hostable backend service that handles all the common SaaS infrastructure concerns, allowing you to focus on building your product. It acts as the authority for users, tenants, memberships, plans, and entitlements, while your remote app enforces these rules and handles domain-specific logic.

Two-Server Architecture

┌─────────────────────────┐         ┌─────────────────────────┐
│     SassyEL Server      │         │      Your Remote App    │
│       (Authority)       │◄───────►│       (Enforcer)        │
├─────────────────────────┤         ├─────────────────────────┤
│ • User management       │         │ • App domain data       │
│ • Tenant management     │ Webhooks│ • Business logic        │
│ • Plans & entitlements  │ ───────►│ • Enforce entitlements  │
│ • Team invitations      │         │ • Credit metering       │
│ • JWT issuance & JWKS   │ JWT     │ • Validate tokens (JWKS)│
│ • Embedded control panel│ ───────►│ • Apply Sassy webhooks  │
└─────────────────────────┘         └─────────────────────────┘

Features

Identity & Authentication

  • User signup, login, and password reset
  • Email verification (optional, via Resend)
  • Magic link passwordless authentication
  • JWT access tokens with JWKS publishing
  • Refresh token rotation
  • Multi-tenant session management

Multi-Tenancy

  • Automatic tenant creation on signup
  • Multiple tenants per user
  • Tenant switching
  • Tenant status management (active, past_due, suspended)

Team Management

  • Role-based access control (owner, admin, member, billing)
  • Email invitations with expiry
  • Invitation accept/revoke workflow
  • Team member listing and management

Plans & Entitlements

  • Flexible plan definitions with pricing tiers
  • Payment provider price IDs (Stripe/Paddle) per plan
  • Support for monthly, yearly, and one-time pricing
  • Feature flags (boolean entitlements)
  • Numeric and string limits
  • Per-tenant entitlement overrides (set, add, remove)
  • Entitlement versioning for cache invalidation
  • Computed entitlement snapshots

Webhooks

  • Server-to-server webhook delivery
  • HMAC signature verification
  • Exponential backoff retries
  • Delivery logging and monitoring
  • Events: tenant.entitlements.updated, membership.updated, tenant.status.updated

Browser SDK

  • Lightweight JavaScript SDK
  • Docked side panel UI
  • Event-driven state management
  • Feature checking helpers
  • Automatic session persistence

Server SDK

  • TypeScript/JavaScript SDK for backend integration
  • JWT/JWKS token verification
  • Webhook signature verification
  • Entitlement checking helpers
  • Express.js middleware
  • Headless mode for custom auth UI with Sassy backend

Developer Portal

  • Multi-app management
  • Per-app configuration
  • Secret management (Resend, Stripe, Paddle keys)
  • Plan management per app
  • Usage statistics

Quick Start

Prerequisites

  • Node.js 18+
  • npm or yarn

Installation

# Clone the repository
git clone https://github.com/adamlauzon/sassy.git
cd sassy

# Install dependencies
npm install

# Initialize the database
npm run init-db

# Generate JWT signing keys
npm run generate-keys

# Start the server
npm start

The server will start at http://localhost:8080.

Available Endpoints

Endpoint Description
http://localhost:8080/health Health check
http://localhost:8080/sdk.js Browser SDK
http://localhost:8080/panel/frame Control panel iframe
http://localhost:8080/developer/ Developer dashboard

Configuration

Environment Variables

Variable Description Default
PORT Server port 8080
SASSY_ALLOWED_ORIGINS Comma-separated CORS origins http://localhost:3000,http://localhost:5173
NODE_ENV Environment mode development

Database

SassyEL uses SQLite (via better-sqlite3) for data persistence. The database file is stored at data/sassy.db.

# Initialize a fresh database with default plans
npm run init-db

# Run migrations (for multi-tenant schema)
npm run migrate

Production Database Recommendation

SQLite is suitable for development and small deployments, but for production environments we strongly recommend using PostgreSQL:

SQLite PostgreSQL
Concurrent writes Limited Excellent
Horizontal scaling No Yes (read replicas)
Backup strategies File copy pg_dump, streaming
High availability Manual Built-in/managed
Connection pooling N/A PgBouncer, built-in

Recommended options:

  1. Supabase (managed PostgreSQL) - Enable "Supabase as Primary Datasource" in Integrations
  2. Neon - Serverless PostgreSQL with branching
  3. Railway/Render - Simple managed PostgreSQL
  4. Self-hosted PostgreSQL - For full control

Note: PostgreSQL support is planned. Currently, enabling "Supabase as Primary Datasource" syncs data to your Supabase project while Sassy's SQLite remains the source of truth for authentication operations.

Integration Guide

1. Add the SDK to Your App

<script src="https://your-sassy-server.com/sdk.js"></script>
<script>
  sassy.init({
    apiUrl: 'https://your-sassy-server.com',
    appId: 'pk_your_app_public_key',
    showLauncher: true,
    panelPosition: 'right', // or 'left'
  });
</script>

2. Listen for Authentication Events

// Wait for SDK to be ready
await sassy.ready;

// Listen for auth changes
sassy.on('auth:changed', ({ user }) => {
  if (user) {
    console.log('User logged in:', user);
  } else {
    console.log('User logged out');
  }
});

// Listen for entitlements updates
sassy.on('entitlements:updated', (entitlements) => {
  console.log('Entitlements updated:', entitlements);
});

// Listen for tenant changes
sassy.on('tenant:changed', ({ tenant }) => {
  console.log('Switched to tenant:', tenant);
});

3. Check Entitlements

// Check feature flags
if (sassy.hasFeature('pdf.generate')) {
  // Enable PDF generation feature
}

// Get limits
const maxTeamMembers = sassy.getLimit('limit.max_team_members');
const monthlyCredits = sassy.getLimit('limit.monthly_credits');

4. Control the Panel

// Open/close the control panel
sassy.ui.open();
sassy.ui.close();
sassy.ui.toggle();

// Check panel state
if (sassy.ui.isOpen()) {
  // Panel is open
}

5. Backend Token Validation

Your backend should validate JWTs using the JWKS endpoint:

// Fetch JWKS from: GET /api/auth/.well-known/jwks.json
// Validate JWT signature using the public keys

// JWT Claims:
// - sub: User ID
// - tid: Tenant ID
// - roles: Array of roles in the tenant
// - ent_ver: Entitlements version (for cache invalidation)
// - exp: Expiration timestamp

6. Handle Webhooks

Configure a webhook endpoint to receive events from SassyEL:

app.post('/webhooks/sassy', (req, res) => {
  // Verify HMAC signature
  const signature = req.headers['x-sassy-signature'];

  const { type, payload, eventId, entVer } = req.body;

  switch (type) {
    case 'tenant.entitlements.updated':
      // Update cached entitlements if entVer > local version
      break;
    case 'membership.updated':
      // Update team member cache
      break;
    case 'tenant.status.updated':
      // Handle status change (active, past_due, suspended)
      break;
  }

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

API Reference

Authentication

Method Endpoint Description
POST /api/auth/signup Create account
POST /api/auth/login Login
POST /api/auth/refresh Refresh access token
POST /api/auth/logout Logout
POST /api/auth/forgot-password Request password reset
POST /api/auth/reset-password Reset password
POST /api/auth/verify-email Verify email address
POST /api/auth/magic-link Request magic link
POST /api/auth/verify-magic-link Verify magic link
POST /api/auth/switch-tenant Switch tenant context
GET /api/auth/.well-known/jwks.json Get JWKS for token verification

Tenants

Method Endpoint Description
GET /api/tenants/current Get current tenant with entitlements
GET /api/tenants/members List tenant members
PATCH /api/tenants/current Update tenant

Invitations

Method Endpoint Description
GET /api/invitations List tenant invitations
POST /api/invitations Create invitation
POST /api/invitations/:id/accept Accept invitation
DELETE /api/invitations/:id Revoke invitation

Plans

Method Endpoint Description
GET /api/plans List available plans with entitlements and price IDs
GET /api/plans/:planId Get a specific plan

Plans API Response

The Plans API returns all public plans with their entitlements and payment provider price IDs:

GET /api/plans
X-Sassy-App-Id: pk_your_app_public_key
{
  "plans": [
    {
      "id": "plan_abc123",
      "name": "Pro",
      "description": "For growing teams",
      "priceMonthly": 2900,
      "priceYearly": 29000,
      "isPublic": true,
      "isDefault": false,
      "priceIdMonthly": "price_1ABC...",
      "priceIdYearly": "price_2DEF...",
      "priceIdOnetime": null,
      "entitlements": {
        "features": {
          "pdf.generate": true,
          "excel.to_json": true,
          "api.access": false
        },
        "limits": {
          "limit.max_file_mb": 100,
          "limit.max_team_members": 10,
          "limit.monthly_credits": 1000
        }
      }
    }
  ],
  "plansEnabled": true
}

Payment Provider Price IDs

Plans include price IDs that link to your Stripe or Paddle prices. These are set in the Developer Portal and returned via the API so your host app can create checkout sessions.

Field Description
priceIdMonthly Stripe/Paddle price ID for monthly subscriptions
priceIdYearly Stripe/Paddle price ID for yearly subscriptions
priceIdOnetime Stripe/Paddle price ID for one-time/lifetime purchases

Host app usage:

// Fetch plans from Sassy
const response = await fetch('https://sassy.example.com/api/plans', {
  headers: { 'X-Sassy-App-Id': 'pk_your_app_public_key' }
});
const { plans } = await response.json();

// Find the plan user wants to purchase
const plan = plans.find(p => p.id === selectedPlanId);

// Determine which price ID to use based on billing interval
const priceId = billingInterval === 'once' ? plan.priceIdOnetime
              : billingInterval === 'year' ? plan.priceIdYearly
              : plan.priceIdMonthly;

// Create Stripe checkout session
const checkout = await stripe.checkout.sessions.create({
  line_items: [{ price: priceId, quantity: 1 }],
  mode: billingInterval === 'once' ? 'payment' : 'subscription',
  success_url: 'https://yourapp.com/success',
  cancel_url: 'https://yourapp.com/pricing',
  metadata: {
    tenant_id: tenantId,
    plan_id: plan.id,
  },
});

Configuring price IDs:

  1. Create prices in your Stripe/Paddle dashboard
  2. Go to Developer Portal → Your App → Plans
  3. Edit each plan and enter the corresponding price IDs
  4. Your host app will receive these IDs when fetching plans

Webhooks

Method Endpoint Description
POST /api/webhooks/endpoints Register webhook endpoint
GET /api/webhooks/endpoints List webhook endpoints
DELETE /api/webhooks/endpoints/:id Remove webhook endpoint

Database Schema

Core Tables

  • users - User accounts
  • tenants - Organizations/workspaces
  • memberships - User-tenant relationships with roles
  • plans - Subscription plan definitions
  • plan_entitlements - Default entitlements per plan
  • tenant_overrides - Per-tenant entitlement overrides
  • entitlement_snapshots - Computed entitlements cache

Auth Tables

  • refresh_tokens - Active refresh tokens
  • password_resets - Password reset tokens
  • api_keys - Programmatic API access

Team Management

  • invitations - Team invitations

Webhooks

  • webhook_endpoints - Configured webhook URLs
  • webhook_deliveries - Delivery logs and retry queue

Security

  • jwks_keys - JWT signing keys (supports rotation)

Multi-App Support

  • developers - Developer accounts
  • apps - Registered applications
  • app_secrets - Per-app API keys and secrets
  • app_settings - Per-app configuration

Default Plans

SassyEL comes with three pre-configured plans:

Free Plan

  • PDF generation: enabled
  • Excel to JSON: disabled
  • Image resize: enabled
  • Max file size: 10 MB
  • Max team members: 3
  • Monthly credits: 100

Pro Plan ($29/month)

  • PDF generation: enabled
  • Excel to JSON: enabled
  • Image resize: enabled
  • Max file size: 100 MB
  • Max team members: 10
  • Monthly credits: 1,000

Enterprise Plan ($99/month)

  • All Pro features
  • API access: enabled
  • SSO: enabled
  • Max file size: 500 MB
  • Max team members: unlimited
  • Monthly credits: 10,000

Development

NPM Scripts

npm start              # Start production server
npm run dev            # Start with auto-reload
npm run init-db        # Initialize database
npm run migrate        # Run all migrations
npm run migrate:billing  # Run billing price IDs migration only
npm run generate-keys  # Generate JWT signing keys

Project Structure

sassy/
├── src/
│   ├── server.js           # Express app entry point
│   ├── routes/
│   │   ├── auth.js         # Authentication endpoints
│   │   ├── tenants.js      # Tenant management
│   │   ├── invitations.js  # Team invitations
│   │   ├── plans.js        # Plan listing
│   │   ├── webhooks.js     # Webhook management
│   │   └── developer.js    # Developer portal API
│   ├── services/
│   │   ├── auth.js         # Auth business logic
│   │   ├── jwt.js          # JWT/JWKS handling
│   │   ├── tenants.js      # Tenant operations
│   │   ├── entitlements.js # Entitlement computation
│   │   ├── invitations.js  # Invitation workflow
│   │   ├── webhooks.js     # Webhook delivery
│   │   └── email.js        # Email via Resend
│   ├── middleware/
│   │   ├── auth.js         # JWT validation
│   │   └── app-context.js  # Multi-app resolution
│   ├── scripts/
│   │   ├── init-db.js      # Database initialization
│   │   ├── migrate-multi-tenant.js  # Multi-app support migration
│   │   ├── migrate-billing-price-ids.js  # Payment provider price IDs
│   │   └── generate-keys.js
│   ├── utils/
│   │   └── db.js           # Database connection
│   └── public/
│       ├── js/sdk.js       # Browser SDK
│       ├── css/panel.css   # Panel styles
│       ├── panel.html      # Control panel
│       └── developer/      # Developer dashboard
├── data/
│   └── sassy.db            # SQLite database
├── demo.html               # Integration demo
├── package.json
└── SPEC.json               # Full specification

Integration Patterns

SassyEL supports two integration patterns depending on your application architecture.

Pattern 1: Apps WITH a Backend (Recommended)

If your application has a backend server, it acts as the enforcer. Your backend validates JWTs directly using Sassy's JWKS endpoint - no additional components needed.

┌──────────────┐       ┌──────────────────────┐       ┌─────────────┐
│   Frontend   │       │    Your Backend      │       │   Sassy     │
│   (SDK)      │──────▶│    (Enforcer)        │       │   Server    │
└──────────────┘       └──────────────────────┘       └─────────────┘
                              │                              │
                              │ 1. Validate JWT via JWKS ───▶│
                              │◀─────────────────────────────│
                              │                              │
                              │ 2. Receive webhooks ◀────────│
                              │                              │

Your backend responsibilities:

  • Fetch and cache JWKS from /api/auth/.well-known/jwks.json
  • Validate JWT signature on incoming requests
  • Extract claims (sub, tid, roles, ent_ver)
  • Enforce entitlements before serving protected resources
  • Handle webhooks for cache invalidation

Helper libraries (planned) will simplify this:

Package Language Status
@sassy/node Node.js Planned
sassy-python Python Planned
sassy-go Go Planned
sassy-php PHP Planned

These are thin wrappers around standard JWT libraries that understand Sassy's token format and provide convenience methods like hasFeature() and getLimit().

Pattern 2: Frontend-Only Apps

For applications without a backend (static sites, SPAs), the SDK can call Sassy directly for verification. This provides protection through Sassy acting as the enforcer.

┌──────────────┐                     ┌─────────────┐
│   Frontend   │ ──── /api/verify ──▶│   Sassy     │
│   (SDK)      │ ◀─── allow/deny ────│   Server    │
└──────────────┘                     └─────────────┘

SDK Configuration:

sassy.init({
  apiUrl: 'https://your-sassy-server.com',
  appId: 'pk_your_app_public_key',
  enforceMode: 'strict',    // Require Sassy verification
  verifyInterval: 300,      // Re-verify every 5 minutes (seconds)
});

Enforce modes:

Mode Behavior
soft SDK works without verification (UI gating only)
strict SDK requires successful Sassy verification to enable features

Note: Frontend-only enforcement can be bypassed by determined users modifying JavaScript. For applications where security is critical, use Pattern 1 with a backend.

Verify Endpoint

For frontend-only apps, Sassy provides a verification endpoint:

GET /api/verify
Authorization: Bearer <access_token>

Response (200 OK):
{
  "valid": true,
  "user": {
    "id": "usr_xxx",
    "email": "user@example.com",
    "name": "User Name"
  },
  "tenant": {
    "id": "ten_xxx",
    "name": "Acme Corp",
    "status": "active",
    "plan_name": "Pro"
  },
  "entitlements": {
    "features": { "pdf.generate": true, "excel.to_json": true },
    "limits": { "limit.monthly_credits": 1000 }
  },
  "ent_ver": 42
}

Response (401 Unauthorized):
{
  "valid": false,
  "reason": "token_expired" | "token_invalid" | "tenant_suspended"
}

Key Design Principles

  1. Two-Server Model: SassyEL is the authority; your app (backend) is the enforcer
  2. No Per-Request Calls: Apps cache entitlements and validate tokens locally via JWKS
  3. Credits Stay Local: Your app owns credit metering and enforcement
  4. Tenant-Scoped Entitlements: No user-scoped overrides in v1
  5. Versioned Entitlements: ent_ver enables efficient cache invalidation
  6. Server-to-Server Webhooks: Browsers don't receive webhooks directly
  7. SDK is Not a Security Boundary: Backend enforcement is required for true security
  8. Frontend-Only Option: Sassy can act as direct verifier for apps without backends

Third-Party Integrations

Resend (Email)

  • Password reset emails
  • Email verification
  • Team invitations
  • Magic link authentication

Stripe / Paddle (Billing)

Sassy stores payment provider price IDs per plan, which your host app uses to create checkout sessions.

Setup:

  1. In Stripe/Paddle: Create products and prices matching your Sassy plans
  2. In Sassy Developer Portal: Enter the price IDs for each plan (monthly, yearly, one-time)
  3. In your host app: Fetch plans via /api/plans, use the price IDs to create checkouts

Supported billing models:

Model How it works
Monthly subscription Use priceIdMonthly
Yearly subscription Use priceIdYearly
Lifetime / One-time Use priceIdOnetime
Per-seat pricing Set limit.max_team_members in entitlements, enforce in your app
Usage credits Set limit.monthly_credits in entitlements, meter in your app

Webhook flow (your host app handles this):

Stripe/Paddle ──webhook──► Your Host App ──API call──► Sassy
                               │
                               ├─► Update tenant.billing_customer_id
                               ├─► Update tenant.billing_subscription_id
                               ├─► Update tenant.plan_id (on upgrade/downgrade)
                               └─► Update tenant.status (active/past_due/suspended)

Note: Sassy does not handle payment processing directly. Your host app:

  1. Creates checkout sessions using price IDs from Sassy
  2. Receives webhooks from Stripe/Paddle
  3. Calls Sassy's API to update tenant billing info and plan

License

MIT

Support

About

Self-hostable SaaS enablement layer for multi-tenant auth, plans & entitlements, team management, and webhooks, with an embeddable control-panel SDK.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages