Billing
Set up and manage billing with Autumn for per-seat pricing
Billing Integration
Billing is already integrated with Autumn for per-seat pricing. Organizations are automatically charged based on the number of members on the Pro plan.
Billing is organization-scoped. Each organization has its own subscription and is billed per user. Autumn can be removed if you don't need billing.
Overview
Unstack Pro uses Autumn for billing:
- Per-seat pricing: Charge per organization member
- Automatic billing: Adding members adjusts billing automatically
- Organization-scoped: Each organization has its own subscription
- Self-service: Users manage billing through Autumn's portal
How It Works
Billing Flow
- User creates an organization
- Organization starts on Free tier (no members allowed)
- Owner upgrades to Pro plan via Autumn
- Pro plan enables adding organization members
- Each member added increases the subscription cost
- Billing is handled automatically by Autumn
Plans
| Plan | Members | Features |
|---|---|---|
| Free | Owner only | Basic organization features |
| Pro | Unlimited | Full member management, teams, custom roles |
Organizations cannot add members until they are on the Pro plan.
Setting Up Autumn
Create an Autumn Account
- Go to useautumn.com
- Sign up for an account
- Create a new project
Get Your API Key
- In Autumn dashboard, go to Settings → API Keys
- Create a new API key
- Copy the key (starts with
am_sk_)
Configure Environment Variables
Add your Autumn API key to Convex:
npx convex env set AUTUMN_API_KEY "am_sk_your_api_key_here"Also add to .env.local:
AUTUMN_API_KEY="am_sk_your_api_key_here"Configure Your Product
In Autumn dashboard:
- Create a Product (e.g., "Pro Plan")
- Set pricing model to Per-seat
- Configure the price per seat
- Set billing interval (monthly/yearly)
Set Up Webhooks (Optional)
For real-time billing events:
- In Autumn, go to Webhooks
- Add your webhook endpoint:
https://yourdomain.convex.site/autumn-webhook - Select events to receive
Managing Subscriptions
Organization Billing Page
Organization owners and admins can manage billing at /organizations/[slug]/billing:
View Subscription:
- Current plan status
- Number of seats/members
- Price per seat
- Next billing date
- Total monthly cost
Manage Subscription:
- Upgrade to Pro
- Access Autumn billing portal
- View order history
- Download invoices
Autumn Customer Portal
Users are redirected to Autumn's hosted portal for:
- Updating payment method
- Viewing invoices
- Managing subscription
- Canceling subscription
// Example: Redirect to billing portal
const handleManageBilling = async () => {
const portalUrl = await getBillingPortalUrl(organizationId);
window.open(portalUrl, "_blank");
};Per-Seat Billing Logic
Adding Members
When a member is added to an organization:
- System checks organization has Pro plan
- If on Pro, member is added
- Autumn automatically updates seat count
- Next invoice reflects new seat
Removing Members
When a member is removed:
- Member is removed from organization
- Seat count decreases
- Prorated credit applied (depending on Autumn settings)
Example Pricing
| Members | Monthly Cost (at $10/seat) |
|---|---|
| 1 (owner) | $10 |
| 5 | $50 |
| 10 | $100 |
| 50 | $500 |
Implementation Details
Checking Subscription Status
import { getAutumnCustomer } from "@/lib/autumn";
export async function canAddMembers(organizationId: string): Promise<boolean> {
const customer = await getAutumnCustomer(organizationId);
return customer?.plan === "pro" && customer?.status === "active";
}Gating Features by Plan
"use client";
import { useOrganization } from "@/lib/hooks/use-organization";
export function AddMemberForm({ organizationId }: { organizationId: string }) {
const { subscription } = useOrganization(organizationId);
if (subscription?.plan !== "pro") {
return (
<div className="text-center p-8">
<h3>Upgrade to Pro</h3>
<p>Add team members by upgrading to the Pro plan.</p>
<UpgradeButton organizationId={organizationId} />
</div>
);
}
return (
// Member invitation form
);
}Organization-Scoped Customers
Organizations are mapped to Autumn customers:
// When organization is created
await autumn.customers.create({
id: organization.id, // Use org ID as customer ID
email: owner.email,
name: organization.name,
});
// When upgrading
await autumn.subscriptions.create({
customerId: organization.id,
productId: "pro-plan",
quantity: 1, // Initial seat for owner
});
// When adding member
await autumn.subscriptions.update({
customerId: organization.id,
quantity: memberCount + 1,
});Billing Routes
| Route | Purpose |
|---|---|
/organizations/[slug]/billing | Billing dashboard |
/organizations/[slug]/billing/upgrade | Upgrade to Pro |
/organizations/[slug]/billing/portal | Redirect to Autumn portal |
Handling Billing Events
Webhook Events
Common Autumn webhook events:
| Event | Description | Action |
|---|---|---|
subscription.created | New subscription | Enable Pro features |
subscription.updated | Seats changed | Update member limits |
subscription.canceled | Plan canceled | Disable Pro features |
payment.succeeded | Payment processed | Send receipt email |
payment.failed | Payment failed | Notify organization owner |
Webhook Handler
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
const http = httpRouter();
http.route({
path: "/autumn-webhook",
method: "POST",
handler: httpAction(async (ctx, request) => {
const body = await request.json();
const signature = request.headers.get("x-autumn-signature");
// Verify webhook signature
if (!verifySignature(body, signature)) {
return new Response("Invalid signature", { status: 401 });
}
switch (body.event) {
case "subscription.created":
await handleSubscriptionCreated(ctx, body.data);
break;
case "subscription.canceled":
await handleSubscriptionCanceled(ctx, body.data);
break;
// ... handle other events
}
return new Response("OK", { status: 200 });
}),
});
export default http;Removing Billing
If you don't need billing:
Remove Environment Variable
Remove AUTUMN_API_KEY from Convex and .env.local.
Update Organization Logic
Modify member addition to not check for Pro plan:
// Remove the plan check in add member logic
export async function addMember(organizationId: string, userId: string) {
// Skip: const canAdd = await canAddMembers(organizationId);
await db.insert("members", {
organizationId,
userId,
role: "member",
createdAt: Date.now(),
});
}Remove Billing UI Components
Remove billing-related components and references from:
- Organization sidebar
- Organization settings
- Dashboard cards
Troubleshooting
Subscription Not Updating
Check:
- Autumn API key is correct
- Webhook endpoint is configured
- Organization ID matches Autumn customer ID
Payment Failed
User should:
- Access billing portal
- Update payment method
- Retry payment
System should:
- Send notification to organization owner
- Gracefully handle expired subscriptions
- Provide clear upgrade path
Can't Add Members
Verify:
- Organization is on Pro plan
- Subscription is active (not canceled)
- No payment failures
Best Practices
- Clear pricing: Show per-seat cost clearly
- Upgrade prompts: Guide users to upgrade when needed
- Graceful degradation: Handle expired subscriptions gracefully
- Email notifications: Notify users of billing events
- Audit trail: Log billing-related actions
- Test in sandbox: Use Autumn's test mode before production