Polaris IDE provides self-service subscription management through the Autumn billing portal, allowing you to upgrade, downgrade, update payment methods, and view invoices.
Managing Your Subscription
All subscription management is handled through the Autumn customer portal, which provides a secure interface for:
Upgrading or downgrading plans
Updating payment methods
Viewing invoices and payment history
Canceling subscriptions
Reactivating canceled subscriptions
The billing portal is powered by Autumn and Stripe, ensuring secure payment processing and PCI compliance.
Accessing the Billing Portal
You can access the billing portal through the Polaris IDE dashboard or by calling the portal API endpoint.
Portal API Endpoint
Endpoint: POST /api/autumn/portal
Implementation:
// From src/app/api/autumn/portal/route.ts:5-29
export async function POST ( request : NextRequest ) {
const { user , userId , response } = await requireAuth ();
if ( ! user ) {
return response ;
}
let returnUrl : string | undefined ;
try {
const body = await request . json ();
returnUrl = body . returnUrl ;
} catch {
returnUrl = undefined ;
}
try {
const portal = await openBillingPortal ( userId , {
return_url: returnUrl || process . env . NEXT_PUBLIC_APP_URL ,
});
return NextResponse . json ({ portalUrl: portal . url });
} catch ( error ) {
console . error ( 'Autumn portal error:' , error );
return new NextResponse ( 'Failed to create portal session' , { status: 500 });
}
}
Request:
{
"returnUrl" : "https://your-app.com/dashboard" // Optional
}
Response:
{
"portalUrl" : "https://billing.autumn.sh/portal/..."
}
Backend Implementation
// From src/lib/autumn-server.ts:31-38
export async function openBillingPortal ( customerId : string , params ?: BillingPortalParams ) {
const autumn = getAutumn ();
const result = await autumn . customers . billingPortal ( customerId , params );
if ( result . error ) {
throw result . error ;
}
return result . data ;
}
Upgrading Your Plan
Upgrading from Free to Pro
When you upgrade from Free to Pro, you gain immediate access to unlimited projects.
Steps:
Click “Upgrade to Pro” in your dashboard
Select Pro Monthly (29 / m o n t h ) o r P r o Y e a r l y ( 29/month) or Pro Yearly ( 29/ m o n t h ) or P ro Y e a r l y ( 290/year)
Complete checkout via Autumn/Stripe
Your subscription is automatically synced
Start creating unlimited projects
Checkout Flow
Endpoint: POST /api/autumn/checkout
Implementation:
// From src/app/api/autumn/checkout/route.ts:14-70
export async function POST ( request : NextRequest ) {
const { user , userId , response } = await requireAuth ();
if ( ! user ) {
return response ;
}
let tier : Tier ;
try {
const body = await request . json ();
tier = body . tier ;
} catch {
return new NextResponse ( 'Malformed JSON' , { status: 400 });
}
if ( ! tier || ! [ 'pro_monthly' , 'pro_yearly' ]. includes ( tier )) {
return new NextResponse ( 'Invalid tier' , { status: 400 });
}
const productId = getProductIdForTier ( tier );
if ( ! productId ) {
return new NextResponse ( 'Product ID not configured' , { status: 500 });
}
try {
const email = user . primaryEmail || undefined ;
const name = user . displayName || undefined ;
const checkout = await createCheckout ({
customer_id: userId ,
product_id: productId ,
success_url: ` ${ process . env . NEXT_PUBLIC_APP_URL } /billing/success` ,
customer_data: {
email ,
name ,
},
});
if ( ! checkout . url ) {
return NextResponse . json ({ checkoutUrl: null , status: 'no_checkout_url' });
}
return NextResponse . json ({ checkoutUrl: checkout . url });
} catch ( error ) {
console . error ( 'Autumn checkout error:' , error );
return new NextResponse ( 'Checkout failed' , { status: 500 });
}
}
Request:
{
"tier" : "pro_monthly" // or "pro_yearly"
}
Response:
{
"checkoutUrl" : "https://checkout.stripe.com/..."
}
Product ID resolution:
// From src/app/api/autumn/checkout/route.ts:7-12
function getProductIdForTier ( tier : Tier ) : string {
if ( tier === 'pro_monthly' ) {
return process . env . NEXT_PUBLIC_AUTUMN_PRO_MONTHLY_PRODUCT_ID || '' ;
}
return process . env . NEXT_PUBLIC_AUTUMN_PRO_YEARLY_PRODUCT_ID || '' ;
}
After successful checkout, you’ll be redirected to /billing/success. The subscription sync happens automatically.
Switching Between Pro Plans
You can switch between Pro Monthly and Pro Yearly at any time through the billing portal.
Monthly to Yearly:
Pro-rated credit for unused monthly time
Immediate access to yearly pricing
Save 17% annually
Yearly to Monthly:
Change takes effect at end of current billing period
No refund for unused annual time
Prevents loss of prepaid time
Downgrading Your Plan
Downgrading from Pro to Free
When downgrading from Pro to Free:
Access the billing portal
Cancel your Pro subscription
Downgrade takes effect at end of billing period
Project limit returns to 10 projects
Existing projects beyond limit remain accessible (read-only)
If you have more than 10 projects when downgrading, you won’t be able to create new projects until you’re under the limit. Existing projects remain accessible.
Subscription Sync
Polaris automatically syncs your subscription status from Autumn to ensure accurate billing and access control.
Sync Endpoint
Endpoint: POST /api/autumn/sync
Implementation:
// From src/app/api/autumn/sync/route.ts:84-123
export async function POST () {
const { user , userId , response } = await requireAuth ();
if ( ! user ) {
return response ;
}
try {
const customer = await getCustomer ( userId );
const subscription = deriveSubscription ( customer );
await convex . mutation ( api . users . updateSubscription , {
stackUserId: userId ,
autumnCustomerId: customer . id ?? userId ,
... subscription ,
});
return NextResponse . json ({ synced: true , subscription });
} catch ( error ) {
if ( isAutumnNotFoundError ( error )) {
const subscription = {
subscriptionStatus: 'free' as const ,
subscriptionTier: 'free' as const ,
subscriptionPlanId: undefined ,
trialEndsAt: undefined ,
projectLimit: FREE_PROJECT_LIMIT ,
};
await convex . mutation ( api . users . updateSubscription , {
stackUserId: userId ,
autumnCustomerId: userId ,
... subscription ,
});
return NextResponse . json ({ synced: true , subscription });
}
console . error ( 'Autumn sync error:' , error );
return new NextResponse ( 'Failed to sync subscription' , { status: 500 });
}
}
Subscription Derivation
// From src/app/api/autumn/sync/route.ts:39-69
function deriveSubscription ( customer : Customer ) {
const products = customer . products ?? [];
const paidProduct = products . find (( product ) => {
return product . id === getProductIdForTier ( 'pro_monthly' ) ||
product . id === getProductIdForTier ( 'pro_yearly' );
});
if ( ! paidProduct ) {
return {
subscriptionStatus: 'free' as const ,
subscriptionTier: 'free' as const ,
subscriptionPlanId: undefined ,
trialEndsAt: undefined ,
projectLimit: FREE_PROJECT_LIMIT ,
};
}
const subscriptionStatus = mapStatus ( paidProduct . status );
const subscriptionTier = getTierFromProductId ( paidProduct . id );
const trialEndsAt = paidProduct . trial_ends_at ?? undefined ;
const projectLimit = subscriptionStatus === 'active' ||
subscriptionStatus === 'trialing' ||
subscriptionStatus === 'past_due'
? - 1
: FREE_PROJECT_LIMIT ;
return {
subscriptionStatus ,
subscriptionTier ,
subscriptionPlanId: paidProduct . id ,
trialEndsAt ,
projectLimit ,
};
}
Status Mapping
// From src/app/api/autumn/sync/route.ts:30-37
function mapStatus ( status ?: string ) : SubscriptionStatus {
if ( status === 'trialing' ) return 'trialing' ;
if ( status === 'past_due' ) return 'past_due' ;
if ( status === 'active' ) return 'active' ;
if ( status === 'scheduled' ) return 'active' ;
if ( status === 'expired' ) return 'canceled' ;
return 'free' ;
}
Database Updates
When your subscription changes, Polaris updates your user record in the Convex database.
Mutation:
// From convex/users.ts:122-192
export const updateSubscription = mutation ({
args: {
stackUserId: v . string (),
autumnCustomerId: v . optional ( v . string ()),
subscriptionStatus: v . optional (
v . union (
v . literal ( "free" ),
v . literal ( "trialing" ),
v . literal ( "active" ),
v . literal ( "paused" ),
v . literal ( "canceled" ),
v . literal ( "past_due" )
)
),
subscriptionTier: v . optional (
v . union (
v . literal ( "free" ),
v . literal ( "pro_monthly" ),
v . literal ( "pro_yearly" )
)
),
subscriptionPlanId: v . optional ( v . string ()),
trialEndsAt: v . optional ( v . number ()),
projectLimit: v . optional ( v . number ()),
},
handler : async ( ctx , args ) => {
const user = await ctx . db
. query ( 'users' )
. withIndex ( 'by_stack_user' , ( q ) => q . eq ( 'stackUserId' , args . stackUserId ))
. first ();
if ( ! user ) {
throw new Error ( 'User not found' );
}
const updateData = {
updatedAt: Date . now (),
... args
};
await ctx . db . patch ( user . _id , updateData );
return await ctx . db . get ( user . _id );
},
});
Viewing Billing Status
You can check your current subscription status and billing information.
Query:
// From convex/users.ts:309-342
export const getBillingStatus = query ({
args: {},
handler : async ( ctx ) => {
const identity = await verifyAuth ( ctx );
const user = await ctx . db
. query ( 'users' )
. withIndex ( 'by_stack_user' , ( q ) => q . eq ( 'stackUserId' , identity . subject ))
. first ();
if ( ! user ) {
return null ;
}
const projectCount = await ctx . db
. query ( 'projects' )
. withIndex ( 'by_owner' , ( q ) => q . eq ( 'ownerId' , identity . subject ))
. collect ();
return {
subscriptionStatus: user . subscriptionStatus ,
subscriptionTier: user . subscriptionTier ,
trialEndsAt: user . trialEndsAt ,
trialDaysRemaining: user . trialEndsAt
? Math . max ( 0 , Math . ceil (( user . trialEndsAt - Date . now ()) / ( 1000 * 60 * 60 * 24 )))
: 0 ,
projectLimit: user . projectLimit ,
projectCount: projectCount . length ,
remainingProjects: user . projectLimit === - 1 ? 'unlimited' : user . projectLimit - projectCount . length ,
autumnCustomerId: user . autumnCustomerId ,
createdAt: user . createdAt ,
};
},
});
Response:
{
"subscriptionStatus" : "active" ,
"subscriptionTier" : "pro_monthly" ,
"trialEndsAt" : null ,
"trialDaysRemaining" : 0 ,
"projectLimit" : -1 ,
"projectCount" : 25 ,
"remainingProjects" : "unlimited" ,
"autumnCustomerId" : "cus_..." ,
"createdAt" : 1234567890000
}
Canceling Your Subscription
To cancel your subscription:
Open the billing portal via /api/autumn/portal
Click “Cancel subscription”
Confirm cancellation
Access continues until end of billing period
After period ends, account reverts to Free tier
Cancellation takes effect at the end of your current billing period. You’ll retain Pro access until then.
Failed Payments
If a payment fails, your subscription enters past_due status:
You retain access during grace period
Automatic retry attempts
Email notifications sent
Update payment method in billing portal
Subscription cancels if not resolved
Grace period access:
// From src/app/api/autumn/sync/route.ts:58-60
const projectLimit = subscriptionStatus === 'active' ||
subscriptionStatus === 'trialing' ||
subscriptionStatus === 'past_due' // Still has access
? - 1
: FREE_PROJECT_LIMIT ;
API Reference
Autumn Server Functions
Location: src/lib/autumn-server.ts
Function Purpose Reference createCheckout()Create checkout session src/lib/autumn-server.ts:22openBillingPortal()Open customer portal src/lib/autumn-server.ts:31getCustomer()Get customer data src/lib/autumn-server.ts:40checkAccess()Check feature access src/lib/autumn-server.ts:49trackUsage()Track usage metrics src/lib/autumn-server.ts:63
Convex Mutations
Location: convex/users.ts
Mutation Purpose Reference updateSubscriptionUpdate subscription data convex/users.ts:122startTrialStart trial period convex/users.ts:197cancelTrialCancel trial convex/users.ts:233
Convex Queries
Location: convex/users.ts
Query Purpose Reference getSubscriptionGet subscription info convex/users.ts:59getBillingStatusGet billing details convex/users.ts:309getProjectCountGet project count convex/users.ts:95
Next Steps
View Plans Compare pricing and features across all subscription tiers
Billing Overview Learn about the Autumn integration and billing flow