Complete guide to subscription management — Stripe integration, webhook processing, tier management, LRS add-on lifecycle, payment failure handling, refund processing, and cache invalidation.
The Trinity Beast subscription lifecycle is managed by the trinity-beast-receipt Lambda function, which handles two distinct entry points:
Architecture Note: The Lambda is NOT in the VPC. It connects to Aurora via the public endpoint and invalidates API key caches by calling /admin/invalidate-key on the public ALB endpoints (api.cpmp-site.org and lrs.cpmp-site.org). This avoids the $32/month NAT gateway cost.
Idempotency: Both checkout and webhook paths are idempotent. Checkout sessions are deduplicated via ElastiCache (1-hour TTL). Webhook events are deduplicated by Stripe event ID in ElastiCache (24-hour TTL). Duplicate calls return cached responses without re-processing.
| Tier | Monthly Queries | Rate Limit (QPS) | Burst Limit | Min Wait (sec) | LRS Included |
|---|---|---|---|---|---|
| Free | 1,000 | 1 | 5 | 1.0 | 10 reports/month |
| Pro | 50,000 | 10 | 20 | 0.1 | 10 reports/month (unlimited with add-on) |
| Enterprise | 500,000 | 50 | 100 | 0.02 | 10 reports/month (unlimited with add-on) |
| Unlimited | Unlimited | 100 | 200 | 0.01 | Unlimited (included) |
| Lifetime | Unlimited | 100 | 200 | 0.01 | Unlimited (included) |
| AWS Partner | Unlimited | No limit | No limit | 0 | Unlimited (included) |
Token Bucket Rate Limiting: Each API key has a QPS limit enforced by a token bucket algorithm. The bucket refills at rate_limit_qps tokens per second, with a maximum burst of burst_limit tokens. The minimum_wait_seconds is the minimum time between requests when the bucket is empty.
AWS Partner Tier: The exchanges we depend on — Coinbase, Gemini, Kraken — share their price feeds with The Trinity Beast at no cost. We pass that generosity forward to the AWS community. If your AWS application needs live crypto prices, partner keys provide unlimited access with no rate limiting, no monthly caps, and no billing. Partners connect via AWS PrivateLink directly to containers — bypassing the ALB and public internet entirely. We receive freely, we give freely.
When a customer completes a Stripe Checkout session on the subscription page, the thank-you page calls the Lambda with the session ID and type.
sequenceDiagram
participant C as Customer
participant S as Stripe Checkout
participant TY as Thank-You Page
participant L as Lambda (receipt)
participant DB as Aurora
participant EC as ElastiCache
participant SES as SES Email
C->>S: Select tier & pay
S->>TY: Redirect with session_id
TY->>L: POST {session_id, type: "subscription"}
L->>EC: Check session dedup
EC-->>L: Not found (first call)
L->>S: Get checkout session
S-->>L: Session details (email, amount, tier, customer_id)
L->>DB: INSERT INTO users (upsert by email)
DB-->>L: user_id
L->>L: Generate API key
L->>DB: INSERT INTO api_keys (user_id, tier, limits)
L->>DB: UPDATE api_keys SET stripe_customer_id, stripe_subscription_id
L->>DB: INSERT INTO transactions
DB-->>L: transaction_id
L->>SES: Send SubscriptionReceipt email
L->>EC: Cache response (1hr dedup)
L-->>TY: {success, api_key, transaction_id}
TY->>C: Display API key & receipt
receipt:session:{id} with 1-hour TTL prevents double-processing if the thank-you page calls twice.users table by email, returns user_id. Stores preferred_lang from the Stripe checkout locale.{random}-{timestamp}.api_keys with tier-specific limits from the rate_limit_template table in Aurora (query_limit, rate_limit_qps, burst_limit, minimum_wait_seconds).stripe_customer_id and stripe_subscription_id on the API key row for webhook lookups.lrs_enabled = true (LRS included at no extra cost).cpmp-lang setting is passed to Stripe via the locale parameter and client_reference_id. The Lambda reads this from the Stripe session and stores it on users.preferred_lang and transactions.preferred_lang. This enables future localized communications in the subscriber's language.transactions table with all payment details including preferred language.SubscriptionReceipt template with API key, portal URL, and receipt link.The LRS (Listener Reporting Service) add-on upgrades a subscriber from 10 reports/month to unlimited reports. It's a separate Stripe subscription linked to the same customer.
users → api_keys join.lrs_enabled = true on the API key row.lrs-addon type transaction.stripe_lrs_subscription_id on the API key row (separate from the main subscription ID)./admin/invalidate-key on both api.cpmp-site.org and lrs.cpmp-site.org so the change takes effect immediately.LRSAddonReceipt template.Auto-detection: If the Stripe checkout session has lrs_addon: true in its metadata, the Lambda automatically routes to the LRS add-on handler regardless of the type parameter sent by the thank-you page.
Donations follow a simpler flow — no API key generation, no tier assignment. The Lambda creates a user record, records the transaction, and sends a DonationReceipt email.
100% of donation revenue funds freedom from brick kiln debt bondage in Pakistan through Cross Power Ministries of Pakistan (CPMP).
Stripe sends webhook events to POST /webhook via API Gateway → Lambda. Events are verified using the Stripe webhook signing secret and deduplicated by event ID in ElastiCache.
flowchart TD
S[Stripe Event] --> V{Verify Signature}
V -->|Invalid| R400[400 Invalid]
V -->|Valid| D{Duplicate Check}
D -->|Already processed| R200D[200 Already processed]
D -->|New event| Route{Event Type}
Route -->|customer.subscription.updated| SU[handleSubscriptionUpdated]
Route -->|customer.subscription.deleted| SD[handleSubscriptionDeleted]
Route -->|invoice.payment_failed| PF[handlePaymentFailed]
Route -->|invoice.paid| PR[handlePaymentRecovered]
Route -->|charge.refunded| RF[handleChargeRefunded]
Route -->|Other| Skip[Log & skip]
SU --> Mark[Mark processed in ElastiCache]
SD --> Mark
PF --> Mark
PR --> Mark
RF --> Mark
Mark --> R200[200 OK]
style S fill:#1e3a5f,stroke:#0f172a,color:#ffffff,font-weight:bold
style V fill:#7c3aed,stroke:#0f172a,color:#ffffff,font-weight:bold
style D fill:#7c3aed,stroke:#0f172a,color:#ffffff,font-weight:bold
style Route fill:#b45309,stroke:#0f172a,color:#ffffff,font-weight:bold
style SU fill:#065f46,stroke:#0f172a,color:#ffffff,font-weight:bold
style SD fill:#991b1b,stroke:#0f172a,color:#ffffff,font-weight:bold
style PF fill:#991b1b,stroke:#0f172a,color:#ffffff,font-weight:bold
style PR fill:#065f46,stroke:#0f172a,color:#ffffff,font-weight:bold
style RF fill:#991b1b,stroke:#0f172a,color:#ffffff,font-weight:bold
style Skip fill:#475569,stroke:#0f172a,color:#ffffff
style Mark fill:#1e3a5f,stroke:#0f172a,color:#ffffff,font-weight:bold
style R200 fill:#065f46,stroke:#0f172a,color:#ffffff,font-weight:bold
style R200D fill:#475569,stroke:#0f172a,color:#ffffff
style R400 fill:#991b1b,stroke:#0f172a,color:#ffffff,font-weight:bold
| Stripe Event | Handler | Action |
|---|---|---|
customer.subscription.updated |
handleSubscriptionUpdated | Tier change (upgrade/downgrade) or status change (past_due → active) |
customer.subscription.deleted |
handleSubscriptionDeleted | Cancellation — downgrade to free, disable LRS, clear Stripe IDs |
invoice.payment_failed |
handlePaymentFailed | Set status to past_due, record payment_failed_at timestamp |
invoice.paid |
handlePaymentRecovered | Restore status to active, clear payment_failed_at |
charge.refunded |
handleChargeRefunded | Revoke API key, set status to refunded, record refund transaction, invalidate cache |
Error Handling: All webhook handlers return HTTP 200 to Stripe even on processing errors. This prevents Stripe from retrying and creating duplicate events. Errors are logged for manual review.
When a subscriber changes their plan in the Stripe Customer Portal, Stripe sends a customer.subscription.updated event with the new tier in the subscription metadata.
rate_limit_template table in Aurora.lrs_enabled is automatically set to true.lrs_enabled is set to false — unless the subscriber has a separate LRS add-on subscription (stripe_lrs_subscription_id is not empty).When a subscription is cancelled (via Customer Portal or Stripe dashboard), Stripe sends a customer.subscription.deleted event.
free with free-tier limits.subscription_status set to canceled.lrs_enabled set to false.stripe_subscription_id and stripe_lrs_subscription_id cleared.subscription.id to stripe_lrs_subscription_id.lrs_enabled set to false.stripe_lrs_subscription_id cleared.
stateDiagram-v2
[*] --> Active: Subscription created
Active --> PastDue: invoice.payment_failed
PastDue --> Active: invoice.paid (recovered)
PastDue --> Blocked: Grace period expired
Blocked --> Active: invoice.paid (recovered)
Active --> Canceled: customer.subscription.deleted
PastDue --> Canceled: customer.subscription.deleted
Canceled --> [*]: Downgraded to free
classDef active fill:#065f46,stroke:#0f172a,color:#ffffff,font-weight:bold
classDef pastdue fill:#b45309,stroke:#0f172a,color:#ffffff,font-weight:bold
classDef blocked fill:#991b1b,stroke:#0f172a,color:#ffffff,font-weight:bold
classDef canceled fill:#475569,stroke:#0f172a,color:#ffffff,font-weight:bold
class Active active
class PastDue pastdue
class Blocked blocked
class Canceled canceled
invoice.payment_failed)subscription_status set to past_due.payment_failed_at set to current timestamp (only on first failure — uses COALESCE to preserve the original date).The LPO server checks the grace period on every price request for past_due subscribers:
payment_grace_period_days application parameter (default: 7 days).time.Since(payment_failed_at) < grace_period, the subscriber continues to have full access.invoice.paid)past_due.subscription_status restored to active.payment_failed_at cleared (set to NULL).When a charge is refunded via the Stripe Dashboard or mobile app, Stripe sends a charge.refunded webhook event. The Lambda automatically revokes the associated API key.
charge.refunded — triggered when you process a refund in the Stripe Dashboard or mobile app.customer ID to find the API key via lookupByStripeCustomer.revoked = true and subscription_status = 'refunded' on the API key row.'refund', the refunded amount, and the associated API key.invalidateAPIKeyCache on all LPO/LRS servers so the revoked key stops working immediately.Policy: All giving is non-refundable as stated on the site. Refunds are processed only in extenuating circumstances at the discretion of the administrator. The automated handler ensures that when a refund does occur, the system responds immediately — no manual API key cleanup required.
Partial vs. Full Refunds: The handler fires on any refund event regardless of amount. Both partial and full refunds result in API key revocation. If a partial refund should not revoke the key, the administrator should manually re-enable it in Aurora after the refund is processed.
Every lifecycle event that changes API key data triggers cache invalidation to ensure changes take effect immediately across all containers.
The Lambda calls GET /admin/invalidate-key?key={api_key} on both endpoints:
https://api.cpmp-site.org/admin/invalidate-key — LPO containershttps://lrs.cpmp-site.org/admin/invalidate-key — LRS containerEach endpoint removes the API key from:
apikey:{key} hashThe next request for that API key triggers a fresh read from Aurora, which now has the updated tier, limits, LRS status, and subscription status.
Why public endpoints? The Lambda is not in the VPC. Using the public ALB endpoints avoids the $32/month NAT gateway cost. The admin key header authenticates the request.
| Column | Type | Purpose |
|---|---|---|
stripe_customer_id | TEXT | Stripe customer ID for webhook lookups |
stripe_subscription_id | TEXT | Main LPO subscription ID |
stripe_lrs_subscription_id | TEXT | Separate LRS add-on subscription ID |
subscription_status | TEXT | active, past_due, canceled (default: active) |
tier_effective_date | TIMESTAMPTZ | When the current tier took effect |
payment_failed_at | TIMESTAMPTZ | First payment failure timestamp (NULL when healthy) |
lrs_enabled | BOOLEAN | Whether unlimited LRS reports are enabled |
tier | TEXT | free, pro, enterprise, unlimited, lifetime, partner |
query_limit | INTEGER | Monthly query limit for the tier |
rate_limit_qps | INTEGER | Queries per second limit |
burst_limit | INTEGER | Token bucket burst capacity |
burst_tokens | NUMERIC | Current token bucket balance |
minimum_wait_seconds | NUMERIC | Minimum time between requests when throttled |
| Index | Purpose |
|---|---|
idx_api_keys_stripe_customer_id | Fast lookup by Stripe customer ID (partial: WHERE stripe_customer_id IS NOT NULL) |
idx_api_keys_stripe_subscription_id | Fast lookup by Stripe subscription ID (partial: WHERE stripe_subscription_id IS NOT NULL) |
Each subscription receipt email includes a link to the Stripe Customer Portal, generated dynamically by the Lambda using billingportal.Session. The portal allows subscribers to:
Portal URL: Generated per-customer at receipt time. Each URL is a one-time session link that expires. The portal is hosted entirely by Stripe — no custom UI needed.