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
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.
| Bucket | Database field | Who uses it | Purpose |
|---|---|---|---|
| Paid personal tokens | User.tokenBalance | Users with paid purchases or subscriptions | Persistent or resettable paid credits |
| Free-plan tokens | User.freeTokenBalance | Users on the free plan | Free credits with configurable renewal behavior |
| Organization shared balance | Organization.tokenBalance | Teams using SHARED_FOR_ORG | One workspace-wide pool used by members |
| Organization member allocation | OrganizationMembership.sharedTokenBalance | Teams using ALLOCATED_PER_MEMBER | Per-member workspace balance |
Token Spending
Token spending can happen through two routes depending on who is initiating the action:
| Route | Use case |
|---|---|
| POST /api/internal/spend-tokens | Server-to-server product logic using INTERNAL_API_TOKEN |
| POST /api/user/spend-tokens | First-party authenticated app flows initiated by the current user |
{
"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.
- In an organization workspace, auto always targets the shared workspace bucket.
- In a personal workspace, auto tries paid first and then free.
- 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 value | Behavior |
|---|---|
| auto | Use shared only in organization context, or paid then free in personal context |
| paid | Spend only from personal paid balance |
| free | Spend only from free-plan balance |
| shared | Spend from organization context |
Note
INTERNAL_API_TOKEN. The user route is for first-party authenticated flows.Tip
/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:
ALLOCATED_PER_MEMBER strategy
Each active member gets their own workspace balance on the membership record instead of drawing from one shared pool.
| Field | Purpose |
|---|---|
| OrganizationMembership.sharedTokenBalance | Per-member allocated balance |
| OrganizationMembership.memberTokenUsage | Per-member usage tracking |
| OrganizationMembership.memberTokenCapOverride | Per-member cap exception |
| OrganizationMembership.memberTokenUsageWindowStart | Start 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.
| Setting | What it controls |
|---|---|
| FREE_PLAN_TOKEN_LIMIT | How many free-plan tokens are granted |
| FREE_PLAN_RENEWAL_TYPE | daily, monthly, one-time, or effectively unlimited behavior |
| FREE_PLAN_TOKEN_NAME | Free-plan-specific label override |
| DEFAULT_TOKEN_LABEL | Fallback token label used when no custom free-plan label is set |
Paid Token Reset and Expiry Behavior
Paid token behavior is also settings-driven. The app distinguishes expiry and renewal behavior for one-time versus recurring subscriptions.
| Setting | Purpose |
|---|---|
| TOKENS_RESET_ON_EXPIRY_ONE_TIME | Whether paid balance resets when a one-time subscription expires |
| TOKENS_RESET_ON_EXPIRY_RECURRING | Whether paid balance resets when a recurring subscription expires |
| TOKENS_RESET_ON_RENEWAL_ONE_TIME | Whether one-time renewal-style flows reset paid balance |
| TOKENS_RESET_ON_RENEWAL_RECURRING | Whether recurring renewals reset paid balance |
| TOKENS_NATURAL_EXPIRY_GRACE_HOURS | Grace period for owner and team subscription validity checks |
| ORGANIZATION_EXPIRY_MODE | Whether expired team access suspends the workspace or dismantles it |
The lifecycle helpers in settings and paid-token modules apply these rules automatically during signup, dashboard visits, renewals, expiry, and certain billing mutations.
Admins manage these controls from the paid token operations area inside /admin/settings.
For vibecoders
What the UI Actually Exposes
| Route | What users or admins can see |
|---|---|
| /dashboard/billing | Current plan status, paid/free token presentation, billing actions, and invoice access |
| /dashboard/team | Invites, members, cap strategy details, per-member overrides, and workspace token context |
| /admin/organizations | Org balances, cap strategies, seat limits, pending invites, and token pool strategy visibility |
| /admin/plans | Plan token limits, recurring status, and organization-support metadata |
| /admin/settings | Free 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.
| Goal | Use this | Why |
|---|---|---|
| Hide or show UI | FeatureGate component | It keeps premium UI out of the rendered page when the user lacks access. |
| Protect server-side business logic | Programmatic feature check | UI hiding alone is not enough when an API or server action must enforce access. |
| Support both personal plans and team plans | Either approach through the shared feature system | The 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.
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:
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
| Endpoint | Purpose |
|---|---|
| POST /api/user/spend-tokens | User-scoped token spending for first-party app flows |
| POST /api/internal/spend-tokens | Server-to-server token spending using INTERNAL_API_TOKEN |
| POST /api/internal/track-visit | Records a visit log entry |
| POST /api/internal/payment-scripts | Payment-related script operations |

