logoPay4SaaS
Consumption

Access Control

Access control solves two questions: can this user use my product? and how much should be deducted?

Pay4SaaS provides two dimensions of access control:

  • Basic access (allowed) — yes/no, works across all four modes
  • Tier access (useTierAccess) — checks if the user's subscription plan is high enough for a specific feature, subscription modes only

The Hero section on the homepage (components/landing/Hero.tsx) contains complete frontend examples, including permission checks, consuming credits, and granting bonus credits.

Four Modes at a Glance

Before diving into the API, understand which mode your product uses — it determines how allowed and useService behave:

Modeallowed meansuseService behavior
CreditsHas purchased credits (purchased_credits > 0)Deducts credits per SERVICE_COSTS[serviceType]
Unlimited subscriptionHas active subscription (incl. trial)Logs usage only, no deduction; isUnlimited = true
Quota subscriptionHas active subscriptionDeducts in priority: quota → bonus credits → purchased credits; quota resets monthly
LifetimeHas lifetime purchaseLogs usage only, no deduction; isUnlimited = true

useAccess Hook

import { useAccess } from '@/hooks/useAccess'

function MyComponent() {
  const { status, loading, error, refreshStatus, useService } = useAccess()

  if (loading) return <div>Loading...</div>

  if (!status.allowed) {
    return <div>Please subscribe or purchase credits first</div>
  }

  return <div>Welcome!</div>
}

Backend equivalent:

import { checkUserAccess } from '@/lib/payment/access'

const access = await checkUserAccess(userId)

if (!access.allowed) {
  return Response.json({ error: 'No access' }, { status: 403 })
}

status Object

{
  allowed: boolean,          // Whether the user has access
  accessType: AccessType,    // Access type (see below)
  details: {
    hasSubscription: boolean,
    subscriptionPlan: string,        // basic / pro / max
    subscriptionStatus: string,
    subscriptionBillingCycle: string, // monthly / yearly
    subscriptionEndDate: string,
    subscriptionProvider: string,
    hasLifetime: boolean,
    lifetimePlan: string,
    availableCredits: number,
    quota: {                         // Quota subscription mode only
      monthlyLimit: number,
      used: number,
      remaining: number,
      resetDate: string
    },
    isUnlimited: boolean,
    hasUsedTrial: boolean,
  }
}

accessType Values

ValueDescription
subscription_unlimitedUnlimited subscription
subscription_quotaQuota subscription
lifetimeLifetime purchase
creditsCredits
noneNo access

useService — Consume a Service Unit

When a user clicks "Generate", "Use", etc., call useService to check access and deduct in one step:

const { useService } = useAccess()

async function handleGenerate() {
  const result = await useService(
    'article_generation',    // Service type (must match a key in SERVICE_COSTS)
    'Generated a blog post', // Description (optional)
    'article-123'            // Related ID (optional)
  )

  if (result.success) {
    console.log('Remaining credits:', result.remainingCredits)
  } else {
    console.log('Error:', result.message)
  }
}

Important: The frontend only passes serviceType — the actual deduction amount is determined server-side by SERVICE_COSTS in config/payment.ts. Users cannot tamper with it via DevTools.

Backend equivalent:

import { fastConsumeService } from '@/lib/payment/access'
import { SERVICE_COSTS } from '@/config/payment'

const amount = SERVICE_COSTS['article_generation']
const result = await fastConsumeService(
  userId,
  amount,               // Looked up from SERVICE_COSTS
  'article_generation', // Service type
  'Generate article',   // Description (optional)
  relatedId             // Related ID (optional)
)

if (!result.success) {
  return Response.json({ error: result.message }, { status: 403 })
}

fastConsumeService atomically deducts credits/quota via a database RPC. The deduction amount always comes from the server-side SERVICE_COSTS config.

SERVICE_COSTS — Pricing Table

All service costs are defined in one place:

// config/payment.ts
export const SERVICE_COSTS: Record<string, number> = {
  'demo-consume': 25,
  'article_generation': 10,  // Your custom service
}

To add a new service, just add a line here — no other changes needed.

useService Return Value

{
  success: boolean,
  accessType: AccessType,     // The actual access type used
  remainingCredits: number,
  message: string,
  error?: string
}

Tier Access — Feature Gating by Plan

Tier access answers a different question: not "does the user have credits?", but "is their subscription plan high enough for this feature?" It never deducts credits or records usage — it's a pure gate.

  • basic < pro < max (order derived from SUBSCRIPTION_PLANS declaration order)
  • Only applies to subscription modes — lifetime and credits don't have tiers
  • No subscription always fails any tier gate

useTierAccess — Frontend

import { useTierAccess } from '@/hooks/useTierAccess'

function ProFeatureButton() {
  const canUse = useTierAccess('pro')

  if (!canUse) {
    return (
      <button onClick={() => router.push('/pricing?plan=pro')}>
        Upgrade to Pro to unlock
      </button>
    )
  }

  return <button onClick={handleDoFeature}>Use Pro Feature</button>
}

hasTierAccess — Backend

Always verify on the server — frontend disabled is UX only and can be bypassed.

import { hasTierAccess } from '@/lib/payment/access'

export async function POST(req) {
  const userId = await getUser(req)

  if (!await hasTierAccess(userId, 'pro')) {
    return Response.json({ error: 'tier_required', required: 'pro' }, { status: 403 })
  }

  return doProFeature()
}

allowed vs useTierAccess — When to Use Which

status.alloweduseTierAccess('pro')
Question"Do they have something?" (credits, subscription, or lifetime)"Is their plan high enough for this feature?"
Applicable modesAll fourSubscription only

Practical guidance:

  • Credits modestatus.allowed + useService
  • Subscription modestatus.allowed for general access, useTierAccess for premium features
  • Hybrid → both: allowed for entry, useTierAccess for premium features

Docs home

Return to the full implementation guide.

Pricing

Review subscriptions, credits, and lifetime options.

Blog

Read more notes on SaaS payments and growth.

On this page