The Trinity Beast — Subscription Lifecycle

Complete guide to subscription management — Stripe integration, webhook processing, tier management, LRS add-on lifecycle, payment failure handling, refund processing, and cache invalidation.

Lambda: trinity-beast-receipt Webhooks: Stripe → API Gateway → Lambda Updated: April 29, 2026 Version: v15

Table of Contents

  1. Overview
  2. Subscription Tiers
  3. New Subscription Flow
  4. LRS Add-On Activation
  5. Donation Processing
  6. Stripe Webhook Events
  7. Tier Upgrades & Downgrades
  8. Cancellation Flow
  9. Payment Failure & Grace Period
  10. Refund Processing
  11. Cache Invalidation
  12. Database Schema
  13. Stripe Customer Portal

List of Diagrams

  1. Diagram 3.1 — New Subscription Flow
  2. Diagram 6.1 — Webhook Event Routing
  3. Diagram 9.1 — Payment Failure & Recovery

1. Overview

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.

2. Subscription Tiers

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.

3. New Subscription Flow

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.

Diagram 3.1 — New Subscription Flow
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
        

Processing Steps

  1. Session dedup check — ElastiCache key receipt:session:{id} with 1-hour TTL prevents double-processing if the thank-you page calls twice.
  2. Read Stripe session — Retrieves customer email, name, amount, payment status, customer ID, and metadata (tier, LRS addon flag, locale).
  3. Create/update user — Upserts into users table by email, returns user_id. Stores preferred_lang from the Stripe checkout locale.
  4. Generate API key — Creates a unique key in format {random}-{timestamp}.
  5. Insert API key — Writes to api_keys with tier-specific limits from the rate_limit_template table in Aurora (query_limit, rate_limit_qps, burst_limit, minimum_wait_seconds).
  6. Link Stripe IDs — Stores stripe_customer_id and stripe_subscription_id on the API key row for webhook lookups.
  7. Lifetime tier — Automatically sets lrs_enabled = true (LRS included at no extra cost).
  8. Preferred language — The visitor's 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.
  9. Record transaction — Inserts into transactions table with all payment details including preferred language.
  10. Send receipt — SES email using the SubscriptionReceipt template with API key, portal URL, and receipt link.
  11. Cache response — Stores the full response in ElastiCache for session dedup.

4. LRS Add-On Activation

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.

Activation Steps

  1. Look up subscriber — Finds the active API key by email via usersapi_keys join.
  2. Validate eligibility — Rejects if tier is Unlimited/Lifetime (LRS already included) or if LRS is already enabled.
  3. Enable LRS — Sets lrs_enabled = true on the API key row.
  4. Record transaction — Inserts lrs-addon type transaction.
  5. Link LRS subscription — Stores stripe_lrs_subscription_id on the API key row (separate from the main subscription ID).
  6. Invalidate cache — Calls /admin/invalidate-key on both api.cpmp-site.org and lrs.cpmp-site.org so the change takes effect immediately.
  7. Send receipt — SES email using the 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.

5. Donation Processing

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).

6. Stripe Webhook Events

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.

Diagram 6.1 — Webhook Event Routing
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.

7. Tier Upgrades & Downgrades

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.

Upgrade Logic

Downgrade Logic

8. Cancellation Flow

When a subscription is cancelled (via Customer Portal or Stripe dashboard), Stripe sends a customer.subscription.deleted event.

LPO Subscription Cancelled

  1. Tier is set to free with free-tier limits.
  2. subscription_status set to canceled.
  3. lrs_enabled set to false.
  4. stripe_subscription_id and stripe_lrs_subscription_id cleared.
  5. If an LRS add-on subscription exists, it is automatically cancelled in Stripe via the API.
  6. API key cache invalidated on all containers.

LRS Add-On Cancelled (separately)

  1. Identified by matching subscription.id to stripe_lrs_subscription_id.
  2. lrs_enabled set to false.
  3. stripe_lrs_subscription_id cleared.
  4. The main LPO subscription remains active and unaffected.
  5. API key cache invalidated.

9. Payment Failure & Grace Period

Diagram 9.1 — Payment Failure & Recovery
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
        

Payment Failure (invoice.payment_failed)

Grace Period

The LPO server checks the grace period on every price request for past_due subscribers:

Payment Recovery (invoice.paid)

10. Refund Processing

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.

Refund Flow

  1. Stripe fires charge.refunded — triggered when you process a refund in the Stripe Dashboard or mobile app.
  2. Look up customer — Uses the charge's customer ID to find the API key via lookupByStripeCustomer.
  3. Revoke API key — Sets revoked = true and subscription_status = 'refunded' on the API key row.
  4. Record transaction — Inserts a refund transaction record with type 'refund', the refunded amount, and the associated API key.
  5. Invalidate cache — Calls 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.

11. Cache Invalidation

Every lifecycle event that changes API key data triggers cache invalidation to ensure changes take effect immediately across all containers.

Invalidation Path

The Lambda calls GET /admin/invalidate-key?key={api_key} on both endpoints:

  • https://api.cpmp-site.org/admin/invalidate-key — LPO containers
  • https://lrs.cpmp-site.org/admin/invalidate-key — LRS container

Each endpoint removes the API key from:

  1. Local sync.Map cache — per-container in-memory cache
  2. ElastiCache — shared apikey:{key} hash

The 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.

12. Database Schema

api_keys — Subscription Lifecycle Columns

ColumnTypePurpose
stripe_customer_idTEXTStripe customer ID for webhook lookups
stripe_subscription_idTEXTMain LPO subscription ID
stripe_lrs_subscription_idTEXTSeparate LRS add-on subscription ID
subscription_statusTEXTactive, past_due, canceled (default: active)
tier_effective_dateTIMESTAMPTZWhen the current tier took effect
payment_failed_atTIMESTAMPTZFirst payment failure timestamp (NULL when healthy)
lrs_enabledBOOLEANWhether unlimited LRS reports are enabled
tierTEXTfree, pro, enterprise, unlimited, lifetime, partner
query_limitINTEGERMonthly query limit for the tier
rate_limit_qpsINTEGERQueries per second limit
burst_limitINTEGERToken bucket burst capacity
burst_tokensNUMERICCurrent token bucket balance
minimum_wait_secondsNUMERICMinimum time between requests when throttled

Indexes for Webhook Lookups

IndexPurpose
idx_api_keys_stripe_customer_idFast lookup by Stripe customer ID (partial: WHERE stripe_customer_id IS NOT NULL)
idx_api_keys_stripe_subscription_idFast lookup by Stripe subscription ID (partial: WHERE stripe_subscription_id IS NOT NULL)

13. Stripe Customer Portal

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.