Skip to main content
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:
  1. Click “Upgrade to Pro” in your dashboard
  2. Select Pro Monthly (29/month)orProYearly(29/month) or Pro Yearly (290/year)
  3. Complete checkout via Autumn/Stripe
  4. Your subscription is automatically synced
  5. 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:
  1. Access the billing portal
  2. Cancel your Pro subscription
  3. Downgrade takes effect at end of billing period
  4. Project limit returns to 10 projects
  5. 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:
  1. Open the billing portal via /api/autumn/portal
  2. Click “Cancel subscription”
  3. Confirm cancellation
  4. Access continues until end of billing period
  5. 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
FunctionPurposeReference
createCheckout()Create checkout sessionsrc/lib/autumn-server.ts:22
openBillingPortal()Open customer portalsrc/lib/autumn-server.ts:31
getCustomer()Get customer datasrc/lib/autumn-server.ts:40
checkAccess()Check feature accesssrc/lib/autumn-server.ts:49
trackUsage()Track usage metricssrc/lib/autumn-server.ts:63

Convex Mutations

Location: convex/users.ts
MutationPurposeReference
updateSubscriptionUpdate subscription dataconvex/users.ts:122
startTrialStart trial periodconvex/users.ts:197
cancelTrialCancel trialconvex/users.ts:233

Convex Queries

Location: convex/users.ts
QueryPurposeReference
getSubscriptionGet subscription infoconvex/users.ts:59
getBillingStatusGet billing detailsconvex/users.ts:309
getProjectCountGet project countconvex/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

Build docs developers (and LLMs) love