Billing Architecture
smo1 intermediate 7 min read
ELI5
SMO1 uses a payment service called Polar to handle subscriptions. Think of Polar as the cash register: it collects money, prints receipts, and tells SMO1 “this person is now a Pro member.” SMO1 listens for these messages via webhooks — automatic notifications sent whenever something changes (payment succeeds, subscription cancels, card expires). To avoid charging someone twice by accident, every webhook has a unique ID that SMO1 remembers forever.
Technical Deep Dive
Billing Flow
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#e8f4f8', 'primaryTextColor': '#2d3748', 'primaryBorderColor': '#90cdf4', 'lineColor': '#718096', 'secondaryColor': '#f0fff4', 'tertiaryColor': '#fefcbf'}}}%%sequenceDiagram autonumber actor U as User participant M as meow-web participant P as purr-api participant Pol as Polar participant DB as PostgreSQL
U->>M: Click "Upgrade to Pro" M->>P: POST /api/billing/checkout P->>Pol: Create checkout session Pol-->>P: Checkout URL P-->>M: Checkout URL M-->>U: Redirect to Polar checkout U->>Pol: Enter payment details Pol->>Pol: Process payment Pol->>P: Webhook: subscription.created P->>DB: UPSERT subscription P-->>Pol: 200 OK Pol-->>U: Payment confirmation U->>M: Return to dashboard M->>P: GET /api/user P-->>M: Updated tier: proSubscription Tiers
| Tier | Price | Links/month | Rate limit | Features |
|---|---|---|---|---|
| free | $0 | 10 | 60/hr | Basic links, basic analytics |
| pro | $9/mo or $90/yr | 100 | 600/hr | Custom slugs, QR codes, advanced analytics |
| business | $49/mo or $490/yr | 1,000 | 6,000/hr | Teams, API access, priority support |
| early_adopter | Legacy | 50 | 1,000/hr | Grandfathered users from beta |
Polar Integration
SDK: Polar Go SDK (github.com/polarsource/polar-go)
Operations:
CreateCheckout— generates a Polar-hosted checkout URLGetSubscription— retrieves current subscription statusCreateCustomerPortal— generates a self-service portal URL (update card, cancel, view invoices)CancelSubscription— initiates cancellation (end of period)ReactivateSubscription— reverses a pending cancellation
Webhook Processing
Endpoint: POST /api/webhooks/polar
Security:
- Verify HMAC-SHA256 signature of the request body against
POLAR_WEBHOOK_SECRET - Parse event JSON
- Check
webhook_eventstable for existingevent_id - If exists: return 200 (idempotent, already processed)
- If new: process event, insert
event_idintowebhook_events, return 200
Webhook types handled:
| Event | Action |
|---|---|
subscription.created | Create subscription record, set tier |
subscription.active | Confirm activation, update status |
subscription.updated | Update tier, period end, cancel flag |
subscription.canceled | Mark as canceled, schedule downgrade |
subscription.revoked | Immediate revocation, downgrade to free |
Idempotency Table
CREATE TABLE webhook_events ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), event_id TEXT UNIQUE NOT NULL, -- Polar's event ID event_type TEXT NOT NULL, processed_at TIMESTAMPTZ DEFAULT NOW(), payload JSONB);The event_id uniqueness constraint guarantees exactly-once processing even if Polar retries the webhook.
Tier Update Logic
When a webhook is processed, purr-api maps the Polar price_id to a SMO1 tier:
var priceToTier = map[string]string{ "price_pro_monthly": "pro", "price_pro_yearly": "pro", "price_business_monthly": "business", "price_business_yearly": "business",}The user’s subscription_tier, subscription_status, subscription_period_end, and polar_subscription_id are updated atomically.
Development Fallback
When POLAR_API_KEY is not configured (kitten environment):
- Checkout creation returns a fake URL (
https://example.com/checkout/fake) - Webhook verification is skipped
- All users remain on
freetier unless manually overridden in the database
This allows full frontend development without a live payment provider.
Key Terms
- Polar → Payment provider (alternative to Stripe) with subscription management, checkout, and customer portal
- Webhook → HTTP callback sent by Polar to purr-api when subscription events occur
- Idempotency → Guarantee that processing the same event twice has the same effect as processing it once
- HMAC-SHA256 → Cryptographic signature verifying the webhook genuinely came from Polar
- Customer portal → Self-service URL where users manage cards, view invoices, and cancel subscriptions
- Price-to-tier mapping → Configuration mapping Polar price IDs to SMO1 subscription tiers
Q&A
Q: Why Polar instead of Stripe? A: Polar is designed for SaaS subscriptions with simpler pricing models and lower fees for small businesses. It also provides built-in customer portals and webhook handling comparable to Stripe.
Q: What happens if a webhook fails? A: Polar retries with exponential backoff. purr-api returns 200 only after successful processing. If processing fails (e.g., database error), purr-api returns 5xx, and Polar retries. The idempotency table ensures retries are safe.
Q: Can a user have multiple active subscriptions? A: No. The schema enforces one subscription per user. Upgrading from Pro to Business cancels the old subscription and creates a new one.
Q: How does the frontend know when a user’s tier changes?
A: The dashboard polls /api/user on navigation. After a successful checkout, the user is redirected back to the dashboard, which re-fetches user data and shows the updated tier badge.
Examples
Think of the billing system like a gym membership:
- Polar is the gym’s corporate office — they handle payments, print membership cards, and manage contracts
- Checkout is the front desk sign-up form — you fill in your details and hand over your credit card
- Webhook is the email the gym sends the local branch saying “Alex just signed up for Premium”
- Idempotency is the front desk checking their logbook: “We already processed Alex’s sign-up email yesterday, so we ignore the duplicate”
- Customer portal is the member website where you update your card, download invoices, or cancel your membership
- Tier mapping is the price list on the wall: $30/month = Basic, $60/month = Premium, $100/month = Elite
neighbors on the map
- Core Data Models designing a new feature that touches users, links, or achievements
- Tier-Based Rate Limiting debugging 429 errors for specific users