Overview
The subscriptions module provides functions for managing user subscriptions through both Stripe (recurring plans) and local database (lifetime plans).
Files :
server/subscription.ts - Subscription status checking
server/billing.ts - Stripe subscription management
Subscription Status
getSubscriptionDetails
Get the current user’s subscription status and plan details.
server/subscription.ts:22
export async function getSubscriptionDetails () : Promise < SubscriptionInfo | null >
Whether the subscription is currently active
Subscription plan name (e.g., ‘Plus’, ‘Pro’, ‘FREE’)
Subscription status (‘ACTIVE’, ‘TRIAL’, ‘CANCELLED’, etc.)
When the subscription ends (null for lifetime)
When the trial period ends (null if not on trial)
Example
import { getSubscriptionDetails } from '@/server/subscription'
const subscription = await getSubscriptionDetails ()
if ( ! subscription ) {
console . log ( 'No subscription found' )
} else if ( subscription . isActive ) {
console . log ( `Active ${ subscription . plan } plan` )
if ( subscription . status === 'TRIAL' && subscription . trialEndsAt ) {
console . log ( `Trial ends: ${ subscription . trialEndsAt } ` )
}
} else {
console . log ( 'Subscription expired or cancelled' )
}
Special Cases :
Rithmic Users : Users with @rithmic.com emails automatically get Plus plan access
Lifetime Plans : Stored in local database, not Stripe
Trial Status : Only active if trial end date is in the future
This function checks user email from middleware headers first, falling back to Supabase if needed.
Stripe Subscription Management
getSubscriptionData
Get detailed subscription data including Stripe information, invoices, and payment history.
export async function getSubscriptionData () : Promise < SubscriptionWithPrice | null >
Subscription ID (prefixed with ‘lifetime_’ for lifetime plans)
Stripe subscription status
Unix timestamp when current period ends
Unix timestamp when current period started
Unix timestamp when subscription was created
Whether subscription is set to cancel at period end
Plan details:
id: Price ID
name: Plan name
amount: Price in cents
interval: Billing interval (‘month’, ‘quarter’, ‘year’, ‘lifetime’)
Active promotion/coupon details (if any)
Payment history including invoices and one-time payments
Example
import { getSubscriptionData } from '@/server/billing'
const subscription = await getSubscriptionData ()
if ( ! subscription ) {
console . log ( 'No active subscription' )
} else {
console . log ( `Plan: ${ subscription . plan . name } ` )
console . log ( `Status: ${ subscription . status } ` )
console . log ( `Amount: $ ${ subscription . plan . amount / 100 } ` )
console . log ( `Interval: ${ subscription . plan . interval } ` )
if ( subscription . cancel_at_period_end ) {
const endDate = new Date ( subscription . current_period_end * 1000 )
console . log ( `Cancels on: ${ endDate . toLocaleDateString () } ` )
}
// Show promotion if active
if ( subscription . promotion ) {
console . log ( `Discount: ${ subscription . promotion . code } ` )
}
// Show invoice history
subscription . invoices ?. forEach ( invoice => {
console . log ( ` ${ new Date ( invoice . created * 1000 ). toLocaleDateString () } : $ ${ invoice . amount_paid / 100 } ` )
})
}
Subscription Priority :
Lifetime plans (local database) - Highest priority
Active Stripe subscriptions - Recurring plans
Trialing Stripe subscriptions - Trial period
Lifetime subscriptions include payment history from Stripe (invoices, payment intents, and charges).
updateSubscription
Pause, resume, or cancel a Stripe subscription.
export async function updateSubscription (
action : 'pause' | 'resume' | 'cancel' ,
subscriptionId : string
) : Promise <{ success : boolean ; error ?: string >
action
'pause' | 'resume' | 'cancel'
required
Action to perform on the subscription
Whether the operation was successful
Error message if operation failed
Example
import { updateSubscription , getSubscriptionData } from '@/server/billing'
const subscription = await getSubscriptionData ()
if ( subscription && ! subscription . id . startsWith ( 'lifetime_' )) {
// Cancel at period end
const result = await updateSubscription ( 'cancel' , subscription . id )
if ( result . success ) {
console . log ( 'Subscription will cancel at period end' )
}
// Resume a cancelled subscription
await updateSubscription ( 'resume' , subscription . id )
}
Both ‘pause’ and ‘cancel’ actions set cancel_at_period_end: true. The subscription remains active until the current period ends.
switchSubscriptionPlan
Switch to a different subscription plan with automatic proration.
export async function switchSubscriptionPlan (
newLookupKey : string
) : Promise <{
success : boolean ;
error ?: string ;
requiresCheckout ?: boolean ;
lookupKey ?: string ;
subscription ?: Stripe . Subscription ;
message ?: string ;
}>
Stripe price lookup key for the new plan
Whether the plan switch was successful
Error message if operation failed
True if switching to lifetime plan (requires checkout)
subscription
Stripe.Subscription | undefined
Updated subscription object
Example
import { switchSubscriptionPlan } from '@/server/billing'
// Switch from monthly to annual
const result = await switchSubscriptionPlan ( 'plus_annual' )
if ( result . requiresCheckout ) {
// Redirect to checkout for lifetime plan
console . log ( 'Requires checkout session' )
} else if ( result . success ) {
console . log ( 'Plan switched successfully' )
console . log ( result . message )
} else {
console . error ( 'Failed to switch plan:' , result . error )
}
Behavior :
Same plan : Returns error if already on the requested plan
Lifetime upgrade : Cancels current subscription and requires checkout
Recurring to recurring : Switches immediately with proration
Discounts removed : Any active coupons are removed when switching
Stripe automatically calculates prorated charges/credits when switching between recurring plans.
collectSubscriptionFeedback
Collect user feedback about subscription events (cancellation, etc.).
export async function collectSubscriptionFeedback (
event : string ,
cancellationReason ?: string ,
feedback ?: string
) : Promise <{ success : boolean ; error ?: string >
The event type (e.g., ‘cancellation’, ‘pause’)
Example
import { collectSubscriptionFeedback } from '@/server/billing'
await collectSubscriptionFeedback (
'cancellation' ,
'too_expensive' ,
'Great product but need to cut costs right now'
)
Subscription Types
SubscriptionInfo
Local subscription status (from database).
interface SubscriptionInfo {
isActive : boolean
plan : string | null
status : string
endDate : Date | null
trialEndsAt : Date | null
}
SubscriptionWithPrice
Detailed Stripe subscription with pricing and invoices.
interface SubscriptionWithPrice {
id : string
status : string
current_period_end : number
current_period_start : number
created : number
cancel_at_period_end : boolean
cancel_at : number | null
canceled_at : number | null
trial_end : number | null
trial_start : number | null
plan : {
id : string
name : string
amount : number
interval : 'month' | 'quarter' | 'year' | 'lifetime'
}
promotion ?: {
code : string
amount_off : number
percent_off : number | null
duration : {
duration_in_months : number | null
duration : 'forever' | 'once' | 'repeating' | null
}
}
invoices ?: Array <{
id : string
amount_paid : number
status : string
created : number
invoice_pdf : string | null
hosted_invoice_url : string | null
>
}
Billing Intervals
The system supports multiple billing intervals:
Monthly : Standard monthly billing
Quarterly : Every 3 months (detected by interval_count === 3)
Annual : Yearly billing
Lifetime : One-time payment, no recurring charges
const interval = price . recurring ?. interval_count === 3
? 'quarter'
: price . recurring ?. interval || 'month'
Invoice History
The system aggregates payment history from multiple Stripe sources:
Subscription Invoices : Regular recurring payments
Payment Intents : One-time payments (e.g., lifetime purchases)
Charges : Fallback for payment tracking
Duplicate detection ensures each payment appears only once.
Example
import { getSubscriptionData } from '@/server/billing'
const subscription = await getSubscriptionData ()
subscription ?. invoices ?. forEach ( invoice => {
console . log ( `Date: ${ new Date ( invoice . created * 1000 ). toLocaleDateString () } ` )
console . log ( `Amount: $ ${ invoice . amount_paid / 100 } ` )
console . log ( `Status: ${ invoice . status } ` )
if ( invoice . hosted_invoice_url ) {
console . log ( `View: ${ invoice . hosted_invoice_url } ` )
}
})
Database Synchronization
When subscription changes occur in Stripe, the local database is updated:
await prisma . subscription . update ({
where: { email: user . email },
data: {
plan: subscriptionPlan ,
interval: interval ,
endDate: new Date ( currentPeriodEnd * 1000 ),
status: 'ACTIVE'
}
})
This ensures consistency between Stripe and the application database.
Best Practices
Check subscription priority
Always check lifetime subscriptions first, then Stripe: // Lifetime check happens first in getSubscriptionData
const localSub = await prisma . subscription . findUnique ( ... )
if ( localSub ?. status === 'ACTIVE' && localSub ?. interval === 'lifetime' ) {
return createLifetimeSubscriptionData ( localSub , invoices )
}
// Then check Stripe
Handle one-time vs recurring
Lifetime plans require different handling: if ( newPrice . type === 'one_time' ) {
// Cancel current subscription
// Return requiresCheckout: true
return { requiresCheckout: true , lookupKey }
}
Account Management Manage trading accounts and payouts
Authentication User authentication and management