Subscription Lifecycle Management — SDK Integration, Webhooks, Customer Portal
The Trinity Beast Stripe Implementation provides full subscription lifecycle management using the Stripe Go SDK. It replaces the previous raw HTTP Stripe API calls with type-safe SDK interactions and adds webhook-driven subscription state management.
Subscribers have two paths for managing their subscriptions:
Linked from every SES email receipt. Subscribers can cancel subscriptions, update payment methods, and view invoice history. Actions trigger the same webhook events as SDK operations.
The trinity-beast-receipt Lambda uses the Stripe Go SDK for checkout session processing, subscription creation, tier changes, and LRS add-on management.
Both paths converge on the same Stripe webhook events, processed by a single Lambda handler. No separate code paths exist for portal vs. SDK — the webhook is the single source of truth.
graph TB
subgraph Subscriber
Portal["Stripe Customer Portal"]
Website["cpmp-site.org"]
end
subgraph Stripe
StripeAPI["Stripe API"]
Webhooks["Stripe Webhooks"]
end
subgraph AWS
Lambda["trinity-beast-receipt Lambda"]
Aurora["Aurora PostgreSQL"]
ElastiCache["ElastiCache Valkey"]
SES["SES Email"]
Main["BeastMain"]
Mirror["BeastMirror"]
LRS["BeastLRS"]
end
Portal -->|cancel / update| StripeAPI
Website -->|checkout| StripeAPI
StripeAPI -->|events| Webhooks
Webhooks -->|POST /webhook| Lambda
Lambda -->|SDK calls| StripeAPI
Lambda -->|read/write| Aurora
Lambda -->|idempotency + cache| ElastiCache
Lambda -->|receipts| SES
Lambda -->|invalidate-key| Main
Lambda -->|invalidate-key| Mirror
Lambda -->|invalidate-key| LRS
SES -->|portal link| Portal
| Component | Role |
|---|---|
trinity-beast-receipt Lambda | Processes checkout sessions and webhook events. Uses Stripe Go SDK v82. |
| Aurora PostgreSQL | Authoritative source for api_keys table with Stripe metadata columns. |
| ElastiCache Valkey | Webhook idempotency keys (72h TTL), API key cache (24h TTL). |
| ECS Services (3) | BeastMain, BeastMirror, BeastLRS — receive cache invalidation calls. |
| SES | Sends receipts with Customer Portal link (portal_url template variable). |
stateDiagram-v2
[*] --> active: checkout.session.completed
active --> active: subscription.updated (upgrade)
active --> pending_downgrade: downgrade scheduled
active --> pending_cancel: cancel_at_period_end
active --> past_due: invoice.payment_failed
pending_downgrade --> active: period end (new tier)
pending_cancel --> canceled: subscription.deleted
past_due --> active: invoice.paid
past_due --> canceled: grace period expired
canceled --> active: re-subscribe
canceled --> [*]
pending_downgrade is tracked via tier_effective_date in Aurora. pending_cancel is tracked by Stripe's cancel_at_period_end flag. The subscription_status column stores Stripe-canonical values: active, past_due, canceled, trialing, incomplete.
The Lambda uses github.com/stripe/stripe-go/v82 for all Stripe API interactions:
| Operation | SDK Package | Replaces |
|---|---|---|
| Checkout session retrieval | checkout/session.Get() | Raw GET /v1/checkout/sessions/{id} |
| Customer Portal session | billingportal/session.New() | Raw POST /v1/billing_portal/sessions |
| Payment intent (receipt URL) | paymentintent.Get() | Raw GET /v1/payment_intents/{id} |
| Webhook signature verification | webhook.ConstructEvent() | N/A (new) |
| Subscription management | subscription.Update(), subscription.List() | N/A (new) |
Authentication: stripe.Key is set from STRIPE_SECRET_KEY in trinity-beast-secrets during Lambda init().
The Lambda exposes a /webhook endpoint that processes Stripe webhook events:
| Event Type | Handler | Action |
|---|---|---|
customer.subscription.updated | handleSubscriptionUpdated | Tier changes, status updates |
customer.subscription.deleted | handleSubscriptionDeleted | Cancellation (LPO or LRS addon) |
invoice.payment_failed | handlePaymentFailed | Mark as past_due |
invoice.paid | handlePaymentRecovered | Restore to active |
STRIPE_WEBHOOK_SECRETwebhook:event:{event_id})api_keys tableAll webhook events return HTTP 200 to Stripe (even on processing errors) to prevent retries. Errors are logged for manual review.
Every SES email receipt includes a portal_url linking to the Stripe Customer Portal. The portal is configured to allow:
Portal return URL: https://cpmp-site.org
Portal sessions are generated using the Stripe SDK: billingportal/session.New() with the subscriber's stripe_customer_id.
When a subscriber cancels through the portal, Stripe fires the same customer.subscription.updated and customer.subscription.deleted events — no separate handling required.
When a subscriber upgrades to a higher tier, the Stripe subscription is updated with proration_behavior: create_prorations. The subscriber is charged the pro-rated difference for the remainder of the billing period. New tier limits take effect immediately in Aurora and all caches are invalidated.
When a subscriber downgrades, the change is scheduled for the end of the billing period using proration_behavior: none. The tier_effective_date column is set in Aurora. The subscriber keeps their current (higher) tier limits until the period ends. When the customer.subscription.updated event fires at period end, the new lower tier is applied.
| Tier | Query Limit | QPS | Burst | Min Wait (s) | LRS Included |
|---|---|---|---|---|---|
| Free | 1,000 | 1 | 5 | 1.0 | No |
| Pro | 50,000 | 10 | 20 | 0.1 | No |
| Enterprise | 500,000 | 50 | 100 | 0.02 | No |
| Unlimited | 999,999,999 | 100 | 200 | 0.01 | Yes |
| Lifetime | 999,999,999 | 100 | 200 | 0.01 | Yes |
Cancellations are always at end of billing period — subscribers keep access until they've used what they paid for.
cancel_at_period_end = truecustomer.subscription.deleted fireslrs_enabled = false — LRS add-on auto-cancelledsub.ID == stripe_lrs_subscription_idlrs_enabled = false, clears stripe_lrs_subscription_idThe LRS add-on ($20/mo) enables unlimited LRS reports for Free, Pro, and Enterprise tiers. Unlimited and Lifetime tiers receive LRS included at no extra cost.
| Action | Flow |
|---|---|
| Purchase | Checkout → Lambda enables lrs_enabled, stores stripe_lrs_subscription_id |
| Cancel (LRS only) | Webhook → Lambda disables lrs_enabled, clears subscription ID |
| Cancel (LPO) | Webhook → Lambda disables LRS, cancels LRS addon in Stripe |
| Re-subscribe | New checkout → Lambda re-enables lrs_enabled, new subscription ID |
| Unlimited/Lifetime purchase | Rejected — LRS already included with tier |
Lifetime tier is a one-time Stripe payment — no recurring subscription. The stripe_subscription_id column remains NULL. LRS is automatically enabled (lrs_enabled = true). Downgrades are rejected — Lifetime subscriptions are permanent.
When invoice.payment_failed fires:
subscription_status set to past_duepayment_failed_at set to current timestamp (first failure only)payment_grace_period_days application parameterWhen invoice.paid fires for a past_due subscription:
subscription_status restored to activepayment_failed_at clearedIf the grace period expires, the LPO server returns HTTP 402: 🛑 [LPO] [us-east-2] [node] [402] Payment past due. Please update your payment method at the Stripe Customer Portal.
Every subscription state change triggers a 3-layer cache flush:
apikey:{api_key} hashhttps://api.cpmp-site.org/admin/invalidate-key?api_key={key}https://lrs.cpmp-site.org/admin/invalidate-key?api_key={key}If any invalidation call fails, the system logs a warning and continues. Eventual consistency is guaranteed by ElastiCache TTL (24h) and sync.Map TTL (5min).
Every webhook event is deduplicated via ElastiCache:
webhook:event:{stripe_event_id}api_keys| Column | Type | Default | Purpose |
|---|---|---|---|
stripe_customer_id | TEXT | NULL | Stripe cus_xxx ID |
stripe_subscription_id | TEXT | NULL | LPO tier subscription ID |
stripe_lrs_subscription_id | TEXT | NULL | LRS add-on subscription ID |
subscription_status | TEXT | 'active' | Mirrors Stripe status |
tier_effective_date | TIMESTAMPTZ | NULL | Pending downgrade date |
payment_failed_at | TIMESTAMPTZ | NULL | First payment failure timestamp |
idx_api_keys_stripe_customer_id — partial index on stripe_customer_id WHERE NOT NULLidx_api_keys_stripe_subscription_id — partial index on stripe_subscription_id WHERE NOT NULL| Key | Value | Description |
|---|---|---|
payment_grace_period_days | 7 | Days to allow access after payment failure |
URL: Lambda Function URL /webhook path
Events: customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed, invoice.paid, checkout.session.completed
Signing secret: Stored as STRIPE_WEBHOOK_SECRET in trinity-beast-secrets
Cancel subscription: Enabled (at end of billing period)
Update payment method: Enabled
View invoices: Enabled
Return URL: https://cpmp-site.org
| Product | Metadata Key | Value |
|---|---|---|
| Free | tier | free |
| Pro | tier | pro |
| Enterprise | tier | enterprise |
| Unlimited | tier | unlimited |
| Lifetime | tier | lifetime |
| LRS | tier | lrs |
All subscription lifecycle errors use the Trinity Beast Convention format:
🛑 [LPO] [us-east-2] [ReceiptLambda] [code] Message.
Success events use module-specific loggers:
logSubscription.Success — subscription activationslogLRSAddon.Success — LRS add-on activationslogLifecycle.Success — tier changes, cancellations, payment recoverylogWebhook.Info — webhook routing, idempotencyAll log messages include stripe_customer_id, stripe_subscription_id, and api_key for traceability.