Access Control
Access control answers the question: After a user has paid, how do you determine whether they have permission to use your service?
Pay4SaaS encapsulates this logic into the useAccess Hook and the useService method — just call them from the frontend.
The Hero section on the homepage (
components/landing/Hero.tsx) contains complete frontend examples, including permission checks, consuming credits, and granting bonus credits.
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>
}status Object
status contains the complete user access information:
{
allowed: boolean, // Whether the user has access
accessType: AccessType, // Access type
details: {
hasSubscription: boolean, // Whether the user has a subscription
subscriptionPlan: string, // Subscription plan (basic/pro/max)
subscriptionStatus: string, // Subscription status
subscriptionBillingCycle: string, // Billing cycle (monthly/yearly)
subscriptionEndDate: string, // Expiry date
subscriptionProvider: string, // Payment provider
hasLifetime: boolean, // Whether the user has lifetime access
lifetimePlan: string, // Lifetime plan
availableCredits: number, // Available credits
quota: { // Quota info (subscription_quota mode only)
monthlyLimit: number,
used: number,
remaining: number,
resetDate: string
},
isUnlimited: boolean, // Whether usage is unlimited
hasUsedTrial: boolean, // Whether trial has been used
}
}accessType Values
| Value | Description |
|---|---|
subscription_unlimited | Access via unlimited subscription |
subscription_quota | Access via quota subscription |
lifetime | Access via lifetime purchase |
credits | Access via credits |
none | No access |
useService — Consume One Service Unit
When a user clicks "Generate", "Use", etc., call useService to record one consumption:
const { useService } = useAccess()
async function handleGenerate() {
const result = await useService(
'article_generation', // Service type (custom string)
1, // Consumption amount (optional, default 1)
'Generated a blog post', // Description (optional)
'article-123' // Related ID (optional)
)
if (result.success) {
// Consumption successful, continue with business logic
console.log('Remaining credits:', result.remainingCredits)
} else {
// Consumption failed
console.log('Error:', result.message)
}
}useService return value:
{
success: boolean,
accessType: AccessType, // The actual access type used
remainingCredits: number, // Remaining credits
message: string, // Status message
error?: string // Error code
}Behavior Differences Across Four Modes
Credits Mode
allowed= credits > 0useServicededucts 1 credit- When credits run out,
allowedbecomesfalse
Unlimited Subscription (subscription_unlimited)
allowed= has an active subscription (including trial period)useServiceonly logs usage, does not deduct creditsisUnlimited=true
Quota Subscription (subscription_quota)
allowed= has remaining quota or has available creditsuseServicededucts in priority order: quota → bonus credits → purchased credits- Quota resets monthly
Lifetime
allowed= has a lifetime purchase recorduseServiceonly logs usage, does not deduct creditsisUnlimited=true
Frontend vs Backend Comparison
Check Access
Frontend page:
const { status } = useAccess()
if (!status.allowed) {
return <div>Please subscribe or purchase credits first</div>
}Backend API Route / Server Action:
import { checkUserAccess } from '@/lib/payment/access'
const access = await checkUserAccess(userId)
if (!access.allowed) {
return Response.json({ error: 'No access' }, { status: 403 })
}Consume Service (Check Access + Deduct Credits)
Frontend page:
const { useService } = useAccess()
const result = await useService('article_generation', 1)
if (result.success) {
console.log('Remaining credits:', result.remainingCredits)
}Backend API Route / Server Action:
import { fastConsumeService } from '@/lib/payment/access'
const result = await fastConsumeService(
userId,
1, // Consumption amount
'article_generation', // Service type
'Generate article', // Description (optional)
relatedId // Related ID (optional)
)
if (!result.success) {
return Response.json({ error: result.message }, { status: 403 })
}fastConsumeService is an optimized fast path that atomically deducts credits/quota directly via a database RPC for better performance.