Raxx · internal docs

internal · gated

SOP — Stripe Founders Setup (Product, Price, Webhook, Vault, Heroku Wiring)

Owner: Operator (Kristerpher) Last updated: 2026-05-31 UTC Refs: #204 (Founders Promo epic), #2743 (PA SaaS sales-tax — CPA determination), docs/architecture/stripe-customer-billing.md, docs/architecture/queue-stripe-webhook-design-2026-05-14.md


When to run this

You need this runbook the first time the Founders product is wired up in a Stripe account — once for Test mode to validate, then again for Live mode when the operator is ready to accept paying customers.

This runbook covers:

iOS subscriptions are out of scope — those go through Apple IAP per the project memory lock. This runbook is web/desktop only.

Referral bonuses are out of scope — attribution only at v1 per the project memory lock; no referral coupons or promo codes are configured here.


Pre-conditions

Before starting, confirm:


Step 1 — Create the Product

Stripe Dashboard → Products → Add product.

Field Value
Name Raxx Founders
Description Founders cohort — every feature in every tier, pricing locked for 6 months.
Image Leave blank (brand placeholder period per project_brand_six_month_defer.md)
Statement descriptor RAXX FOUNDERS (22-char limit; Stripe will reject longer)
Tax behavior Tax is calculated on the price — leave the price-level tax-behavior setting to Step 3
Unit label (blank)
Metadata cohort=founders, tier=all_features, lock_months=6

After save, copy the prod_... ID. You will write it to vault in Step 7.


Step 2 — Create the Price

On the Product page → Add another price.

Field Value
Pricing model Standard pricing
Price 29.00 USD
Billing period Monthly, recurring
Trial period None — Founders is paid-on-signup, no free trial (per operator pricing lock 2026-05-28)
Lookup key founders_29_monthly_v1
Tax behavior Exclusive if PA collection is required (Step 3 outcome); Inclusive only if CPA advises otherwise
Metadata lock_months=6, converts_to=pro_standard_after_lock

The 6-month minimum commitment is not a Stripe-side configuration. Stripe Billing does not natively express "no cancellation before month N." Per docs/architecture/founders-grace-transition.md, the 6-month lock is enforced application-side by Raptor (and Queue after migration): the subscription is created with cancel_at_period_end=false and any customer-initiated cancellation before month 6 is routed through a custom dunning flow rather than firing subscription.delete on Stripe. Document this as a non-obvious gotcha when handing off.

After save, copy the price_... ID. You will write it to vault in Step 7 and to Heroku in Step 8.


Step 3 — Configure tax (PA SaaS posture — CPA review pending)

PA Act 84 of 2016 taxes "canned computer software" at 6% state (plus 1% Allegheny / 2% Philadelphia local). #2743 tracks the CPA determination on whether Raxx must collect from the first PA-resident subscriber as a domestic PA LLC.

Until CPA written guidance is on file (link the guidance memo in this section once it lands), set Stripe as follows:

  1. Enable Stripe Tax at Settings → Tax → Settings. Use it as the calculation engine — do not commit to remitting via Stripe Tax until CPA approves.
  2. Add Pennsylvania as a registered jurisdiction in Settings → Tax → Registrations once myPATH registration is complete (PA Department of Revenue). Per #2743, the registration step is a separate operator action.
  3. Set the product tax category to txcd_10000000 (General — SaaS / digital services). Confirm with CPA whether txcd_10103000 (SaaS) is a better fit; Stripe's PA-specific guidance flips occasionally.
  4. Leave California, New York, Texas, Washington disabled at v1 — economic-nexus thresholds are not crossed and the registration overhead is not warranted pre-launch.
  5. Tax is shown to the buyer at checkout. Customer-facing copy says "tax calculated at checkout" on the pricing page.

Until #2743 closes, treat Stripe Tax in "calculate but do not remit automatically" mode — the operator manually files the PA-3 sales-tax return monthly. Switch to Stripe Tax auto-remit only after CPA sign-off.


Step 4 — Webhook endpoint

The webhook endpoint lives in Raptor at https://api.raxx.app/webhooks/stripe (per docs/architecture/queue-stripe-webhook-design-2026-05-14.md §3). Note: this endpoint moves to Queue post-migration; Raxx-side path stays stable, the implementing service changes.

Stripe Dashboard → Developers → Webhooks → Add endpoint.

Field Value (Test mode) Value (Live mode)
Endpoint URL https://api-staging.raxx.app/webhooks/stripe https://api.raxx.app/webhooks/stripe
Description Raxx Founders test webhook Raxx Founders prod webhook
API version Match the Stripe SDK pinned in backend_v2/requirements.txt (verify before save)
Events to send See list below

Events to send (subset of Founders-relevant events; do NOT enable "send all events" — webhook noise breaks idempotency dashboards):

After save, click Reveal signing secret and copy the whsec_... value — visible exactly once. You will write it to vault in Step 7.


Step 5 — Test-mode validation

Before flipping live mode:

  1. In Stripe Test mode dashboard → Developers → Customers → Add test customer. Use email test+founders@raxx.app and the Stripe test card 4242 4242 4242 4242 (any future exp, any CVC, any ZIP).
  2. Subscribe the test customer to the Founders price.
  3. Confirm webhook delivery — Stripe Dashboard → Developers → Webhooks → [endpoint] → Recent attempts. Both customer.subscription.created and invoice.paid should show HTTP 200 from staging.
  4. Confirm Raptor staging logs (heroku logs --tail --app raxx-api-staging | grep stripe) show signature-verified events and a row in the billing-event audit table.
  5. Cancel the test subscription in the dashboard. Confirm customer.subscription.deleted arrives and the staging app downgrades the test user.
  6. Trigger a payment-failure scenario with test card 4000 0000 0000 0341 (charge succeeds, subsequent invoice fails). Confirm invoice.payment_failed lands and a dunning email queues.

If any step fails, fix in staging before moving to Step 6.


Step 6 — Live mode swap

Once Step 5 is clean:

  1. Toggle the Stripe dashboard from Test to Live mode (top-right).
  2. Repeat Step 1, Step 2, Step 3, Step 4 in Live mode. The lookup key is the same (founders_29_monthly_v1) but the prod_..., price_..., and whsec_... IDs will be different from the test values. Live IDs start with the same prefix; the difference is the Stripe account context.
  3. Confirm the Live API key starts with sk_live_... (operator never copies this anywhere — Stripe stores it; the restricted key Raxx uses starts with rk_live_...).
  4. Update vault (Step 7) with the live values.
  5. Update Heroku config (Step 8) for raxx-api-prod.
  6. Smoke-test one real subscription with the operator's own card — refund immediately after the webhook lands and the subscription record is written to Raptor.

Step 7 — Write vault entries

Per feedback_secrets_in_vault_sop.md and feedback_no_inline_secrets_in_repo.md, every secret lives in Infisical and is read at runtime — never inlined into repo files, never echoed to logs.

Vault folder per operator instruction: /raxx/stripe/. The folder must exist before any secret write (per feedback_vault_folder_must_exist.md). The legacy path /MooseQuest/stripe/ and the Queue-future path /Raxx/Queue/Billing/Stripe/ both also exist in the vault per docs/architecture/queue-stripe-webhook-design-2026-05-14.md §I-10 — coordinate with the Queue cutover plan before re-pathing existing secrets.

Create the folder first if missing:

curl -X POST "https://app.infisical.com/api/v1/folders" \
  -H "Authorization: Bearer $INFISICAL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"workspaceId":"<wsid>","environment":"prod","name":"stripe","path":"/raxx"}' \
  >/dev/null 2>&1

Then write the three secrets (use Infisical CLI or REST — values from Step 1, Step 2, Step 4):

Secret name Path Value source
STRIPE_FOUNDERS_PRODUCT_ID /raxx/stripe/ Step 1 prod_...
STRIPE_FOUNDERS_PRICE_ID /raxx/stripe/ Step 2 price_...
STRIPE_FOUNDERS_WEBHOOK_SECRET /raxx/stripe/ Step 4 whsec_...
STRIPE_RESTRICTED_KEY /raxx/stripe/ The rk_live_... restricted key minted in pre-conditions

For Live mode, label each secret with the prod environment in Infisical. For Test mode, write to the staging environment with the test-mode IDs and rk_test_... key.

Never paste any of these values into a chat, ticket, PR description, or commit message. Per feedback_heroku_config_set_echoes_secrets.md, even reading them back through tooling can leak — always silence stdout.


Step 8 — Heroku config

Set the flag and the price ID on raxx-api-prod. The webhook secret and restricted key are read from vault at runtime (no Heroku config value); only the price ID and feature flag need to be set as Heroku config because they are read at boot rather than per-request.

heroku config:set FLAG_STRIPE_FOUNDERS=1 STRIPE_FOUNDERS_PRICE_ID=price_xxx \
  --app raxx-api-prod >/dev/null 2>&1

Replace price_xxx with the Live-mode price_... from Step 2. The >/dev/null 2>&1 is mandatory per feedback_heroku_config_set_echoes_secrets.mdheroku config:set echoes the value back by default, and even non-secret IDs in shell history are a hygiene cost.

Same pattern for staging with the test-mode price ID:

heroku config:set FLAG_STRIPE_FOUNDERS=1 STRIPE_FOUNDERS_PRICE_ID=price_xxx \
  --app raxx-api-staging >/dev/null 2>&1

To verify the flag is set without exposing the value to history:

heroku config:get FLAG_STRIPE_FOUNDERS --app raxx-api-prod
# Expected output: 1

Note: this still echoes the value (which is 1, not a secret). For secret values, use wc -c to confirm length only, never reveal contents.


Step 9 — Console flag promotion (same-PR migration)

Per feedback_new_flag_needs_b1_migration_same_pr.md, every new entry in feature_flags.yaml requires a console_flag_promotions migration in the same PR. If FLAG_STRIPE_FOUNDERS does not yet exist in YAML, the engineering PR that lands the Founders signup wiring must add both the YAML row and the promotion migration — this runbook step is informational, not the PR's job.


Common pitfalls

  1. 6-month minimum commitment is application-side, not Stripe-side. Stripe Billing does not enforce a minimum-commitment window. The customer can cancel any time in the Stripe customer portal. Application-side logic (Raptor today, Queue post-migration) must intercept cancellation requests pre-month-6 and route them through the dunning UX rather than firing subscription.delete. Without this enforcement, the Founders pricing-lock promise breaks silently.

  2. Automatic conversion to regular Pro after 6 months. Stripe Billing does not natively switch a subscription from one Price to another after N billing periods. The 6-month-to-Pro transition is implemented as a scheduled job (scheduled_subscription_change) in Stripe's Subscription Schedules API. When the regular Pro/Pro+ prices are finalized (BLR research pending per project_pricing_tiers_locked.md), create the schedule at signup so the transition is queued from day one rather than retrofitted month 5.

  3. Refund window is operator policy, not a Stripe setting. Stripe allows refunds up to 180 days after a charge. Raxx's published refund policy lives in the Terms of Service draft (docs/legal/terms-of-service-draft-2026-05-14.md); the operator approves or denies refunds case-by-case through the dashboard. There is no automatic refund-on-cancel for Founders subscriptions — the value proposition is the locked $29 price, not a 30-day money-back guarantee.

  4. Test-mode webhook signature secret is different from Live-mode. When swapping from Test to Live in Step 6, the whsec_... must be re-fetched from the Live-mode endpoint page. Reusing the Test secret in prod will cause every webhook to fail signature verification and you will spend an hour debugging "401 Unauthorized" before realizing.

  5. Stripe Tax registration is per-state, not per-account. Enabling Stripe Tax for PA does not register MooseQuest LLC with the PA Department of Revenue — that is a separate myPATH registration (#2743). Stripe will calculate tax but cannot file the PA-3 return without operator action.

  6. Restricted-key scope is "deny-by-default." When minting the rk_live_..., every permission must be explicitly granted. If Webhook Endpoints is left at None, the application cannot reconcile webhook subscriptions in code. Audit the scope list against the application's actual needs before clicking save.

  7. iOS subscribers do not appear in Stripe. Per project_ios_billing_iap.md, iOS sub revenue flows through Apple IAP. The Stripe dashboard will only show web/desktop subscribers. Cohort analytics in console.raxx.app must aggregate from both sources — the Stripe-only view will undercount.


Rollback

If the live-mode swap turns out to be premature:

heroku config:set FLAG_STRIPE_FOUNDERS=0 --app raxx-api-prod >/dev/null 2>&1

This disables the Founders signup path application-side. Existing subscriptions in Stripe remain billable — the flag gates new signups, not existing customers. To pause billing for everyone, navigate each subscription to "Pause collection" in the Stripe dashboard (operator action; not scripted).


Refs