SaaSyBase
SaaSyBase

Tokens & Features

SaaSyBase includes a built-in credit system for metered usage and a feature gating layer for premium access. The token story spans personal balances, free-plan balances, and organization-managed balances, so this page covers all three.

Note

Plain-English translation: a token is just a usage credit, and a feature gate is just an on-or-off access rule. You can rename the token concept for your own product, but the mechanics stay the same.

Token System Overview

There is not just one token balance in SaaSyBase. Depending on the user and workspace context, tokens can come from personal paid balance, free-plan balance, or an organization-managed balance.

BucketDatabase fieldWho uses itPurpose
Paid personal tokensUser.tokenBalanceUsers with paid purchases or subscriptionsPersistent or resettable paid credits
Free-plan tokensUser.freeTokenBalanceUsers on the free planFree credits with configurable renewal behavior
Organization shared balanceOrganization.tokenBalanceTeams using SHARED_FOR_ORGOne workspace-wide pool used by members
Organization member allocationOrganizationMembership.sharedTokenBalanceTeams using ALLOCATED_PER_MEMBERPer-member workspace balance

Token Spending

Token spending can happen through two routes depending on who is initiating the action:

RouteUse case
POST /api/internal/spend-tokensServer-to-server product logic using INTERNAL_API_TOKEN
POST /api/user/spend-tokensFirst-party authenticated app flows initiated by the current user
POST /api/internal/spend-tokens
{
  "amount": 10,
  "bucket": "auto",          // auto | paid | free | shared
  "feature": "image_export", // optional for tracking
  "organizationId": "org_123" // optional shared-context hint
}

There is also a user-scoped route at /api/user/spend-tokens for first-party app flows where the caller is the authenticated end user rather than an internal service.

What auto bucket selection really does

When bucket="auto", the app first decides whether the request is happening in a personal workspace or an organization workspace.

  1. In an organization workspace, auto always targets the shared workspace bucket.
  2. In a personal workspace, auto tries paid first and then free.
  3. Auto never crosses those boundaries: organization context does not spill into personal buckets, and personal context does not reach into shared workspace balance.

The active workspace context is resolved at spend time from the current authenticated request and active-organization state. Switching workspaces later does not retroactively move an already-recorded spend into a different bucket.

Bucket valueBehavior
autoUse shared only in organization context, or paid then free in personal context
paidSpend only from personal paid balance
freeSpend only from free-plan balance
sharedSpend from organization context

Note

The internal route requires a Bearer token matching INTERNAL_API_TOKEN. The user route is for first-party authenticated flows.

Tip

Use /api/user/spend-tokens when the logged-in user clicked a button in your own app. Use /api/internal/spend-tokens only for trusted server-to-server product logic.

Organization Token Pools

When a user belongs to a team plan, the organization has its own token system with two distinct strategies:

SHARED_FOR_ORG strategy

The workspace maintains one shared balance in Organization.tokenBalance. All members draw from the same pool.

FieldPurpose
Organization.tokenBalanceShared workspace balance
Organization.memberTokenCapOptional per-member spending limit
Organization.memberCapStrategySOFT, HARD, or DISABLED enforcement
Organization.memberCapResetIntervalHoursHow often per-member usage windows reset
Organization.ownerExemptFromCapsWhether the workspace owner bypasses caps

In SOFT mode the app can warn while still allowing overage. In HARD mode the spend can be blocked once the member exceeds the current cap window.

ALLOCATED_PER_MEMBER strategy

Each active member gets their own workspace balance on the membership record instead of drawing from one shared pool.

FieldPurpose
OrganizationMembership.sharedTokenBalancePer-member allocated balance
OrganizationMembership.memberTokenUsagePer-member usage tracking
OrganizationMembership.memberTokenCapOverridePer-member cap exception
OrganizationMembership.memberTokenUsageWindowStartStart of the current cap window

Renewals can reset these balances, and one-time top-ups can credit each active member rather than a shared pool.

Those resets follow the workspace subscription lifecycle. A member joining mid-cycle receives the current allocation when the membership is provisioned, and the next workspace renewal recalculates everyone from the active plan.

Owner subscription gate and grace period

Shared organization spending is gated by the workspace owner's team subscription. If the owner is too far beyond expiry, members cannot continue spending from workspace balance.

The grace window is controlled by TOKENS_NATURAL_EXPIRY_GRACE_HOURS. After that window, organization access follows ORGANIZATION_EXPIRY_MODE, which defaults to suspension and can be changed to dismantling.

Free Plan Renewal and Labels

Free-plan credits are configurable. They are not hardcoded to one monthly reset pattern.

SettingWhat it controls
FREE_PLAN_TOKEN_LIMITHow many free-plan tokens are granted
FREE_PLAN_RENEWAL_TYPEdaily, monthly, one-time, or effectively unlimited behavior
FREE_PLAN_TOKEN_NAMEFree-plan-specific label override
DEFAULT_TOKEN_LABELFallback token label used when no custom free-plan label is set

What the UI Actually Exposes

RouteWhat users or admins can see
/dashboard/billingCurrent plan status, paid/free token presentation, billing actions, and invoice access
/dashboard/teamInvites, members, cap strategy details, per-member overrides, and workspace token context
/admin/organizationsOrg balances, cap strategies, seat limits, pending invites, and token pool strategy visibility
/admin/plansPlan token limits, recurring status, and organization-support metadata
/admin/settingsFree plan settings plus the paid-token operations panel for expiry, renewal, and grace-hour behavior

Feature Gating

Feature gating checks both personal subscriptions and organization-backed entitlement. A user may have access because of their own plan, because of workspace membership, or both.

GoalUse thisWhy
Hide or show UIFeatureGate componentIt keeps premium UI out of the rendered page when the user lacks access.
Protect server-side business logicProgrammatic feature checkUI hiding alone is not enough when an API or server action must enforce access.
Support both personal plans and team plansEither approach through the shared feature systemThe gate already checks both personal and organization-backed entitlement.

Defining features

Features are defined in lib/features.ts using the FeatureId enum. The shipped feature IDs (like WATERMARK_REMOVAL, FOV_ADJUST) are examples from the original product — replace them with your own.

lib/features.ts
export enum FeatureId {
  WATERMARK_REMOVAL = 'WATERMARK_REMOVAL',
  YOUR_FEATURE = 'YOUR_FEATURE',
  // Add your own feature IDs here
}

Using the FeatureGate component

Wrap any UI that should be restricted to paid users in the FeatureGate server component:

Usage in a page or component
import { FeatureGate } from '@/lib/featureGate';
import { FeatureId } from '@/lib/features';

<FeatureGate feature={FeatureId.YOUR_FEATURE}>
  <YourPremiumComponent />
</FeatureGate>

Content inside the gate is only rendered when the current user has an active subscription (personal or via team membership) that grants access to the specified feature.

In practical terms, use this for things like premium export buttons, advanced settings panels, or entire paid-only dashboard sections.

Programmatic checks

For server-side logic (API routes, server components), use isProFeature() to check feature access without rendering a component:

import { isProFeature } from '@/lib/features';

const hasAccess = await isProFeature(userId, FeatureId.YOUR_FEATURE);

This is the safer choice for API routes, server actions, and background jobs, because it protects the actual operation instead of only hiding the button that triggers it.

Other Internal Endpoints

EndpointPurpose
POST /api/user/spend-tokensUser-scoped token spending for first-party app flows
POST /api/internal/spend-tokensServer-to-server token spending using INTERNAL_API_TOKEN
POST /api/internal/track-visitRecords a visit log entry
POST /api/internal/payment-scriptsPayment-related script operations