Sassy provides identity, tenancy, plans, entitlements, and team management - everything you need to add subscription-based features to your application.
Secure user authentication with magic links, email verification, and JWT-based sessions. Works with your existing user database or standalone.
Full tenant isolation with automatic data segregation. Each app operates in its own tenant space with configurable settings.
Define subscription plans with flexible pricing. Support for monthly/yearly billing, free trials, and custom pricing tiers.
JSON-based entitlements for fine-grained feature access. Check user permissions with a simple API call.
Invite team members, manage roles, and control access. Built-in support for team-based subscriptions.
Native integration with Stripe and Paddle. Handle subscriptions, invoices, and payment methods automatically.
Get Sassy integrated into your application in under 5 minutes. Follow these steps to add authentication, plans, and entitlements to your app.
You'll need your API Public Key from the Developer Portal. Create an app in the portal to get your credentials.
Log in to the Developer Portal and create a new application. You'll receive a Public Key that identifies your app.
In the Developer Portal, create subscription plans with features and entitlements. For example:
// Start Plan:
{
"api_calls": 10000,
"storage_gb": 50,
"team_members": 10,
"features": ["advanced_analytics", "api_access", "priority_support"]
}
Add the Sassy SDK to your application. See the Server SDK and Client SDK sections below for detailed integration guides.
An App represents your application in Sassy. Each app has its own users, plans, and settings. You can create multiple apps for different environments (development, staging, production).
Users are authenticated individuals within your app. They can have a subscription plan and belong to teams. User data is isolated per-app.
Plans define subscription tiers with pricing and features. Each plan has:
Entitlements are the permissions and limits associated with a plan. They're stored as JSON and can represent anything: API call limits, storage quotas, feature flags, etc.
The Server SDK provides secure backend integration for user management, entitlement checks, and subscription handling.
# npm
npm install @sassy/server
# yarn
yarn add @sassy/server
# pnpm
pnpm add @sassy/server
import { Sassy } from '@sassy/server';
const sassy = new Sassy({
apiUrl: 'https://your-sassy-instance.com',
publicKey: 'pk_live_xxxxx' // From Developer Portal
});
Verify JWT tokens from authenticated users on your backend.
// Express middleware example
async function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const user = await sassy.verifyToken(token);
req.user = user;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
}
// Use in your routes
app.get('/api/protected', authMiddleware, (req, res) => {
res.json({ user: req.user });
});
Headless mode lets you build your own custom authentication UI while Sassy handles all backend auth logic, sessions, and security. Your server uses the SDK to forward auth requests to Sassy.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Your Frontend │ ──► │ Your Server │ ──► │ Sassy │
│ (Custom Auth │ │ (Uses Server │ │ (Auth Logic, │
│ UI/Pages) │ │ SDK) │ │ Sessions) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
All headless authentication operations are available via sassy.auth:
| Method | Description |
|---|---|
| sassy.auth.signup() | Create a new user account |
| sassy.auth.login() | Authenticate with email/password |
| sassy.auth.logout() | Invalidate a refresh token |
| sassy.auth.refreshToken() | Get new tokens using refresh token |
| sassy.auth.verifySession() | Verify token and get full session state |
| sassy.auth.forgotPassword() | Request a password reset email |
| sassy.auth.resetPassword() | Reset password with token |
| sassy.auth.verifyEmail() | Verify email address with token |
| sassy.auth.requestMagicLink() | Send a passwordless login link |
| sassy.auth.verifyMagicLink() | Authenticate via magic link token |
| sassy.auth.switchTenant() | Switch to a different tenant |
| sassy.auth.getFeatures() | Get enabled auth features for the app |
// Signup - create new user
app.post('/api/auth/signup', async (req, res) => {
const result = await sassy.auth.signup({
email: req.body.email,
password: req.body.password,
name: req.body.name,
});
res.json({
accessToken: result.accessToken,
refreshToken: result.refreshToken,
user: result.user,
tenantId: result.tenantId,
});
});
// Login - authenticate user
app.post('/api/auth/login', async (req, res) => {
const result = await sassy.auth.login({
email: req.body.email,
password: req.body.password,
});
res.json({
accessToken: result.accessToken,
refreshToken: result.refreshToken,
user: result.user,
memberships: result.memberships,
});
});
// Refresh tokens
app.post('/api/auth/refresh', async (req, res) => {
const result = await sassy.auth.refreshToken({
refreshToken: req.body.refreshToken,
});
res.json({
accessToken: result.accessToken,
refreshToken: result.refreshToken,
});
});
// Logout - invalidate refresh token
app.post('/api/auth/logout', async (req, res) => {
await sassy.auth.logout(req.body.refreshToken);
res.json({ success: true });
});
// Verify session - get full user/tenant/entitlements state
app.get('/api/auth/verify', async (req, res) => {
const accessToken = req.headers.authorization?.replace('Bearer ', '');
const session = await sassy.auth.verifySession({ accessToken });
if (session.valid) {
res.json({
user: session.user,
tenant: session.tenant,
entitlements: session.entitlements,
});
} else {
res.status(401).json({ error: session.reason });
}
});
// Request password reset - always succeeds (doesn't reveal if user exists)
app.post('/api/auth/forgot-password', async (req, res) => {
await sassy.auth.forgotPassword({ email: req.body.email });
res.json({ message: 'If an account exists, a reset link has been sent.' });
});
// Reset password with token from email
app.post('/api/auth/reset-password', async (req, res) => {
await sassy.auth.resetPassword({
token: req.body.token,
password: req.body.password,
});
res.json({ message: 'Password reset successfully' });
});
// Request magic link
app.post('/api/auth/magic-link', async (req, res) => {
await sassy.auth.requestMagicLink({ email: req.body.email });
res.json({ message: 'If an account exists, a magic link has been sent.' });
});
// Verify magic link token
app.post('/api/auth/verify-magic-link', async (req, res) => {
const result = await sassy.auth.verifyMagicLink({ token: req.body.token });
res.json({
accessToken: result.accessToken,
refreshToken: result.refreshToken,
user: result.user,
});
});
// Switch to a different tenant (for users with multiple tenants)
app.post('/api/auth/switch-tenant', async (req, res) => {
const accessToken = req.headers.authorization?.replace('Bearer ', '');
const result = await sassy.auth.switchTenant({
accessToken,
tenantId: req.body.tenantId,
});
res.json({
accessToken: result.accessToken, // New token scoped to target tenant
refreshToken: result.refreshToken,
});
});
Manage tenants, members, and settings via sassy.tenants:
| Method | Description |
|---|---|
| sassy.tenants.getCurrent() | Get current tenant with entitlements |
| sassy.tenants.listUserTenants() | List all tenants user belongs to |
| sassy.tenants.create() | Create a new tenant |
| sassy.tenants.updateCurrent() | Update current tenant name/slug |
| sassy.tenants.listMembers() | List tenant members with roles |
| sassy.tenants.updateMemberRole() | Change a member's role |
| sassy.tenants.removeMember() | Remove member from tenant |
Manage team invitations via sassy.invitations:
| Method | Description |
|---|---|
| sassy.invitations.list() | List pending invitations |
| sassy.invitations.create() | Invite user by email with role |
| sassy.invitations.getInfo() | Get public info for accept page |
| sassy.invitations.accept() | Accept invitation and join tenant |
| sassy.invitations.revoke() | Cancel pending invitation |
| sassy.invitations.resend() | Resend invitation email |
Retrieve subscription plans via sassy.plans:
| Method | Description |
|---|---|
| sassy.plans.list() | Get all available plans with pricing |
| sassy.plans.get() | Get specific plan by ID |
import {
ApiError,
AuthenticationError,
AccountLockedError,
UserExistsError,
InvalidInvitationError,
} from '@sassy/server-sdk';
app.post('/api/auth/signup', async (req, res) => {
try {
const result = await sassy.auth.signup({ ... });
res.json(result);
} catch (error) {
if (error instanceof UserExistsError) {
return res.status(409).json({ error: 'Email already registered' });
}
if (error instanceof AccountLockedError) {
return res.status(429).json({ error: 'Account temporarily locked' });
}
if (error instanceof AuthenticationError) {
return res.status(401).json({ error: 'Invalid credentials' });
}
res.status(500).json({ error: 'Internal server error' });
}
});
Headless mode gives you complete control over your auth UI while Sassy handles security, rate limiting, password hashing, token management, and session invalidation.
Fetch user data, update profiles, and manage subscriptions.
// Get user by ID
const user = await sassy.users.get(userId);
// Get user with their plan and entitlements
const userWithPlan = await sassy.users.getWithPlan(userId);
console.log(userWithPlan.plan); // { name: 'Pro', slug: 'pro', ... }
console.log(userWithPlan.entitlements); // { api_calls: 10000, ... }
// Update user's plan
await sassy.users.updatePlan(userId, 'enterprise');
Check if a user has access to specific features or enough quota.
// Check if user has a specific feature
const hasApiAccess = await sassy.entitlements.hasFeature(
userId,
'api_access'
);
if (!hasApiAccess) {
return res.status(403).json({
error: 'API access requires a Pro plan or higher'
});
}
// Check numeric entitlement
const apiCallLimit = await sassy.entitlements.getValue(
userId,
'api_calls'
);
console.log(`User can make ${apiCallLimit} API calls`);
// Get all entitlements at once
const entitlements = await sassy.entitlements.getAll(userId);
// { api_calls: 10000, storage_gb: 50, features: [...] }
Cache entitlements on the server for frequently accessed features. The SDK provides built-in caching support.
The Client SDK handles authentication flows and provides user state management for your frontend application.
Include the SDK via script tag or import it in your build:
<!-- Include from your Sassy instance -->
<script src="https://your-sassy-instance.com/sdk.js"></script>
<script>
const sassy = new SassySDK({
publicKey: 'pk_live_xxxxx'
});
</script>
import { SassySDK } from '@sassy/client';
const sassy = new SassySDK({
apiUrl: 'https://your-sassy-instance.com',
publicKey: 'pk_live_xxxxx'
});
The SDK supports passwordless magic link authentication out of the box.
// Request a magic link
async function requestMagicLink(email) {
try {
await sassy.auth.sendMagicLink(email);
showMessage('Check your email for the login link!');
} catch (err) {
showError(err.message);
}
}
// Handle the magic link callback (on page load)
async function handleAuthCallback() {
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
if (token) {
try {
await sassy.auth.verifyToken(token);
window.location.href = '/dashboard';
} catch (err) {
showError('Invalid or expired link');
}
}
}
// Logout
async function logout() {
await sassy.auth.logout();
window.location.href = '/';
}
Access the current user's data, plan, and entitlements from the client.
// Check if user is logged in
if (sassy.isAuthenticated()) {
const user = sassy.getUser();
console.log(`Welcome, ${user.name}!`);
}
// Get user's current plan
const plan = sassy.getPlan();
console.log(`You're on the ${plan.name} plan`);
// Check entitlements for UI
const entitlements = sassy.getEntitlements();
if (entitlements.features?.includes('advanced_analytics')) {
showAnalyticsDashboard();
} else {
showUpgradePrompt();
}
// Listen for auth state changes
sassy.onAuthStateChange((user) => {
if (user) {
updateUI(user);
} else {
showLoginScreen();
}
});
Client-side entitlement checks are for UI purposes only. Always verify entitlements on the server for protected resources.
Sassy follows a two-server model where Sassy is the authority and your backend is the enforcer:
┌─────────────────────────┐ ┌─────────────────────────┐
│ Sassy Server │ │ Your Backend │
│ (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 │
└─────────────────────────┘ └─────────────────────────┘
Your backend validates JWTs using Sassy's JWKS endpoint and receives webhooks when entitlements change. This means no per-request calls to Sassy - your app caches entitlements and validates tokens locally.
Sassy sends webhooks to your backend when important events occur. Configure your webhook endpoint in the Developer Portal.
import { webhookHandler } from '@sassy/server';
app.post('/webhooks/sassy',
express.raw({ type: 'application/json' }),
webhookHandler(),
(req, res) => {
const event = req.webhookEvent;
switch (event.event_type) {
case 'tenant.entitlements.updated':
// Update local entitlements cache
const { tenant_id, entitlements } = event.data;
break;
case 'membership.updated':
// Update user role cache
const { user_id, role } = event.data;
break;
case 'tenant.status.updated':
// Handle status change (active, past_due, suspended)
const { status } = event.data;
break;
}
res.json({ received: true });
}
);
| Event | Description |
|---|---|
| tenant.entitlements.updated | Tenant's entitlements changed (plan change, override added) |
| tenant.status.updated | Tenant status changed (active, past_due, suspended) |
| membership.updated | User's role in a tenant changed |
| membership.removed | User removed from a tenant |
Sassy issues JWT tokens with the following claims that your backend can use for authorization:
| Claim | Description |
|---|---|
| sub | User ID |
| tid | Tenant ID |
| roles | User's roles in the tenant (array) |
| ent_ver | Entitlements version (for cache invalidation) |
| tenant_status | Tenant status (active, past_due, suspended) |
Use the JWKS endpoint at /api/auth/.well-known/jwks.json to verify tokens. The Server SDK handles this automatically.
Full list of available API endpoints. All endpoints require authentication via the SDK or API key.
| Endpoint | Description |
|---|---|
| POST /api/auth/magic-link | Request a magic link for email authentication |
| POST /api/auth/verify | Verify a magic link token and get session |
| POST /api/auth/logout | End the current session |
| GET /api/auth/me | Get the current authenticated user |
| GET /api/auth/.well-known/jwks.json | Get public keys for JWT verification |
| Endpoint | Description |
|---|---|
| GET /api/users/:id | Get user by ID |
| PATCH /api/users/:id | Update user profile |
| GET /api/users/:id/entitlements | Get user's entitlements |
| Endpoint | Description |
|---|---|
| GET /api/plans | List all available plans |
| GET /api/plans/:slug | Get a specific plan by slug |
Current status of the Sassy server and API.
Quick links: