Integrate Stripe payment processing to enable subscription-based user tiers (Free/Pro/Enterprise) and one-time donations. This feature creates a new subscriptions feature capsule and a "Subscription" page in the settings area.
User Tiers
Tier
Price
Vehicle Limit
Features
Free
$0
2 vehicles
Basic features only
Pro
$1.99/mo or $19.99/year
3-5 vehicles
Full features
Enterprise
$4.99/mo or $49.99/year
Unlimited
Full features
Feature Gating by Tier
Feature
Free
Pro
Enterprise
Vehicles
2 max
3-5
Unlimited
VIN Decoding
No
Yes
Yes
OCR Functionality
No
Yes
Yes
API Access
No
Yes
Yes
Technical Requirements
Stripe Integration
Method: Stripe Elements (embedded in app, not Checkout redirect)
Mode: Live mode (production Stripe account already configured)
Webhooks: Required for subscription lifecycle events
Webhook Events to Handle
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
invoice.payment_succeeded
invoice.payment_failed
customer.subscription.trial_will_end (not used but good to handle)
Subscription Lifecycle
Trial: None
Grace Period: 30 days on payment failure
Downgrade Timing: End of current billing cycle
Payment Failure Notifications (Email)
Immediate: On first payment failure
Day 23: 7 days before grace period ends
Day 29: 1 day before access is revoked
Downgrade Flow
When downgrading from a higher tier violates vehicle limits:
Block downgrade until resolved
Present vehicle selection UI
User must choose which vehicles to disable (not delete)
Disabled vehicles remain in database but are hidden from active use
Re-enabling requires upgrade or removing other vehicles
Implement Stripe payment processing for subscription-based user tiers (Free/Pro/Enterprise) and one-time donations. This creates a new subscriptions feature capsule following the established feature pattern. The approach is layered backend-first to enable testable increments.
Key Architectural Decisions:
Separate subscriptions table (FK to user_profiles.id) - Stripe billing data isolated from user identity
Vehicle visibility via service-layer filtering - authorization concern, not data state
Public webhook endpoint with signature verification - industry standard, Stripe recommended
Planning Context
Decision Log
Decision
Reasoning Chain
Separate subscriptions table
Stripe IDs are billing implementation detail -> mixing with user_profiles conflates identity with billing -> separate table keeps concerns isolated and future-proofs for edge cases
Service-layer vehicle filtering
Vehicle "disable" on downgrade is authorization (like tier-guard) -> adding disabled_at column conflates authorization with data state -> service filtering allows "upgrade to unlock" UX without schema change
Public webhook + signature verification
Stripe webhook HMAC-SHA256 is cryptographically secure -> separate Fastify instance adds operational complexity without security benefit -> signature verification is sufficient for single-tenant app
Feature capsule structure
19 existing features follow api/data/domain/external/migrations pattern -> consistency enables AI context efficiency -> new subscriptions feature follows same structure
Layered Backend-First approach
Each layer (schema -> client -> service -> API -> frontend) testable independently -> monolithic approach would defer integration bugs to late stage -> layered reduces backtrack cost
Grace period 30 days
User-specified in issue -> Stripe default grace is configurable -> 30 days provides reasonable time for payment resolution
Email notifications day 0/23/29
User-specified in issue -> immediate notification on failure -> 7-day warning before grace ends -> 1-day final warning
Rejected Alternatives
Alternative
Why Rejected
Extend user_profiles with Stripe fields
Mixes user identity with billing concern. user_profiles.subscriptionTier is user state; Stripe IDs are billing implementation detail
disabled_at column on vehicles
Vehicle disable is authorization, not data state. Service-layer filtering allows "upgrade to unlock" UX without schema change
Separate Fastify instance for webhooks
Over-engineering for single-tenant app. Signature verification sufficient; low webhook volume
Stripe Checkout redirect
User-specified requirement: must use Stripe Elements embedded in app
Testing: Integration tests with Stripe test mode, npm test must pass
Known Risks
Risk
Mitigation
Anchor
Webhook delivery failures
Stripe retries failed webhooks; idempotency via stripe_event_id prevents duplicates
N/A - new code
Race condition: webhook before API response
Service checks subscription exists before processing webhook events
N/A - new code
Grace period calculation drift
Daily job at 2 AM; 23-hour max latency acceptable for 30-day window
Follows scheduler/index.ts pattern
Invisible Knowledge
Architecture
Frontend (Subscription Page)
|
v
Backend API (/api/subscriptions/*)
|
+----+----+
| |
v v
Stripe Webhook
Client Handler
| |
+----+----+
|
v
Subscriptions Service
|
v
Repository (subscriptions, subscription_events, donations, tier_vehicle_selections)
|
v
PostgreSQL
Data Flow
Upgrade Flow:
User -> SubscriptionPage -> POST /checkout -> StripeClient.createSubscription()
-> Stripe API -> webhook (subscription.created) -> update subscriptions
-> sync tier to user_profiles.subscription_tier
User -> select target tier -> if vehicle_count > limit:
-> VehicleSelectionDialog -> select vehicles to keep
-> POST /downgrade with vehicle_ids -> store selections
-> Stripe cancels at period end -> webhook updates tier
Why This Structure
subscriptions table separate from user_profiles: Billing lifecycle (grace periods, payment methods, Stripe sync) is distinct from user identity
tier_vehicle_selections table: Captures user's explicit choices during downgrade, enabling "upgrade to unlock" UX
Service-layer vehicle filtering: Vehicles aren't deleted or disabled; they're tier-gated like features
Mobile: Subscription section visible in mobile Settings screen
Both display current plan (FREE) with upgrade prompt and link to full subscription management
## Subscription Section Added to Settings Page
Added the missing Subscription section to both desktop and mobile Settings screens.
### Changes
**Desktop Settings Page** (`frontend/src/pages/SettingsPage.tsx`):
- Added Subscription section after My Vehicles
- Shows current tier badge (FREE/PRO/ENTERPRISE)
- Displays status indicator for non-active subscriptions (past_due, canceled)
- Descriptive text based on current tier
- "Manage" button links to `/garage/settings/subscription`
**Mobile Settings Screen** (`frontend/src/features/settings/mobile/MobileSettingsScreen.tsx`):
- Added matching Subscription section with mobile-friendly styling
- Same functionality as desktop
- "Manage" button navigates to Subscription screen
### Commits
- `feat: add Subscription section to Settings page (refs #55)`
- `fix: correct subscription description when data unavailable (refs #55)`
- `feat: add Subscription section to mobile Settings (refs #55)`
### Verified On Staging
- Desktop: Subscription section visible at https://staging.motovaultpro.com/garage/settings
- Mobile: Subscription section visible in mobile Settings screen
- Both display current plan (FREE) with upgrade prompt and link to full subscription management
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Integrate Stripe payment processing to enable subscription-based user tiers (Free/Pro/Enterprise) and one-time donations. This feature creates a new
subscriptionsfeature capsule and a "Subscription" page in the settings area.User Tiers
Feature Gating by Tier
Technical Requirements
Stripe Integration
Webhook Events to Handle
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_succeededinvoice.payment_failedcustomer.subscription.trial_will_end(not used but good to handle)Subscription Lifecycle
Payment Failure Notifications (Email)
Downgrade Flow
When downgrading from a higher tier violates vehicle limits:
Donation Feature
UI Requirements
New "Subscription" Page (Settings Area)
Upgrade Prompts
Database Schema (New Tables)
subscriptionsid(UUID, PK)user_id(UUID, FK to user_profiles)stripe_customer_id(VARCHAR)stripe_subscription_id(VARCHAR, nullable)tier(ENUM: 'free', 'pro', 'enterprise')billing_cycle(ENUM: 'monthly', 'yearly', nullable)status(ENUM: 'active', 'past_due', 'canceled', 'unpaid')current_period_start(TIMESTAMP)current_period_end(TIMESTAMP)grace_period_end(TIMESTAMP, nullable)created_at,updated_atsubscription_eventsid(UUID, PK)subscription_id(UUID, FK)event_type(VARCHAR)stripe_event_id(VARCHAR)payload(JSONB)created_atdonationsid(UUID, PK)user_id(UUID, FK)stripe_payment_intent_id(VARCHAR)amount_cents(INTEGER)currency(VARCHAR, default 'usd')status(ENUM: 'pending', 'succeeded', 'failed')created_atdisabled_vehicles(or adddisabled_atcolumn to vehicles)API Endpoints
Subscription Management
GET /api/subscriptions- Get current subscription statusPOST /api/subscriptions/checkout- Create Stripe checkout sessionPOST /api/subscriptions/cancel- Schedule cancellation at period endPOST /api/subscriptions/reactivate- Cancel pending cancellationPUT /api/subscriptions/payment-method- Update payment methodGET /api/subscriptions/invoices- Get billing historyWebhooks
POST /api/webhooks/stripe- Stripe webhook endpoint (public, signature verified)Donations
POST /api/donations- Create donation payment intentGET /api/donations- Get user's donation historyVehicle Management (Updates)
POST /api/vehicles/:id/disable- Disable vehicle (for downgrade)POST /api/vehicles/:id/enable- Re-enable vehicle (if tier allows)Acceptance Criteria
Tier System
Stripe Integration
subscription_eventsSubscription Lifecycle
Payment Failure Handling
Donation Feature
UI/UX
Security
Environment Variables (New)
Dependencies
stripenpm package (backend)@stripe/stripe-jsnpm package (frontend)@stripe/react-stripe-jsnpm package (frontend)Out of Scope
References
Plan: Stripe Integration with User Tiers
Phase: Planning | Agent: Planner | Status: AWAITING_REVIEW
Overview
Implement Stripe payment processing for subscription-based user tiers (Free/Pro/Enterprise) and one-time donations. This creates a new
subscriptionsfeature capsule following the established feature pattern. The approach is layered backend-first to enable testable increments.Key Architectural Decisions:
subscriptionstable (FK to user_profiles.id) - Stripe billing data isolated from user identityPlanning Context
Decision Log
Rejected Alternatives
Constraints & Assumptions
Known Risks
Invisible Knowledge
Architecture
Data Flow
Upgrade Flow:
Payment Failure Flow:
Downgrade Flow:
Why This Structure
Milestones
Milestone 1: Database Schema + Stripe Client
Files:
backend/src/features/subscriptions/migrations/001_subscriptions_tables.sqlbackend/src/features/subscriptions/external/stripe/stripe.client.tsbackend/src/features/subscriptions/external/stripe/stripe.types.tsbackend/src/features/subscriptions/data/subscriptions.repository.tsbackend/src/features/subscriptions/domain/subscriptions.types.tsbackend/src/features/subscriptions/index.tsRequirements:
Acceptance Criteria:
Tests:
backend/src/features/subscriptions/tests/subscriptions.repository.test.tsMilestone 2: Service Layer + Webhook Endpoint
Files:
backend/src/features/subscriptions/domain/subscriptions.service.tsbackend/src/features/subscriptions/api/webhooks.routes.tsbackend/src/features/subscriptions/api/webhooks.controller.tsDependencies: M1
Requirements:
Acceptance Criteria:
Tests:
backend/src/features/subscriptions/tests/webhooks.test.tsMilestone 3: API Endpoints + Grace Period Job
Files:
backend/src/features/subscriptions/api/subscriptions.routes.tsbackend/src/features/subscriptions/api/subscriptions.controller.tsbackend/src/features/subscriptions/jobs/grace-period.job.tsbackend/src/features/notifications/migrations/006_payment_email_templates.sqlbackend/src/core/scheduler/index.ts(modify)Dependencies: M2
Requirements:
Acceptance Criteria:
Tests:
backend/src/features/subscriptions/tests/subscriptions.routes.test.tsMilestone 4: Frontend Subscription Page
Files:
frontend/src/features/settings/subscription/SubscriptionPage.tsxfrontend/src/features/settings/subscription/MobileSubscriptionScreen.tsxfrontend/src/features/settings/subscription/hooks/useSubscription.tsfrontend/src/features/settings/subscription/api/subscription.api.tsfrontend/src/features/settings/subscription/components/TierCard.tsxfrontend/src/features/settings/subscription/components/PaymentMethodForm.tsxfrontend/src/features/settings/subscription/components/BillingHistory.tsxDependencies: M3
Requirements:
Acceptance Criteria:
Tests:
Milestone 5: Vehicle Selection + Downgrade Flow
Files:
frontend/src/features/vehicles/components/VehicleSelectionDialog.tsxfrontend/src/features/settings/subscription/components/DowngradeFlow.tsxbackend/src/features/subscriptions/domain/subscriptions.service.ts(modify)backend/src/features/vehicles/domain/vehicles.service.ts(modify)Dependencies: M4
Requirements:
Acceptance Criteria:
Tests:
backend/src/features/subscriptions/tests/downgrade.test.tsMilestone 6: Donations Feature
Files:
backend/src/features/subscriptions/domain/donations.service.tsbackend/src/features/subscriptions/api/donations.routes.tsbackend/src/features/subscriptions/api/donations.controller.tsfrontend/src/features/settings/subscription/components/DonationSection.tsxDependencies: M1 (can run parallel to M4-M5)
Requirements:
Acceptance Criteria:
Tests:
backend/src/features/subscriptions/tests/donations.test.tsMilestone 7: Documentation
Files:
backend/src/features/subscriptions/README.mdbackend/src/features/subscriptions/CLAUDE.mdfrontend/src/features/settings/subscription/README.mdDependencies: M1-M6
Requirements:
Acceptance Criteria:
Milestone Dependencies
Verdict: AWAITING_REVIEW | Next: QR plan-completeness review
Plan Review: Complete
Phase: Plan-Review | Agent: Quality Reviewer | Status: PASS
Review Summary
QR plan-completeness Findings (SHOULD_FIX)
QR plan-code Findings (Implementation Details)
{ rawBody: true }for signature verificationQR plan-docs Findings (Structure)
frontend/src/features/settings/subscription/CLAUDE.mdOverall Verdict
PLAN APPROVED - Ready for execution with the noted documentation improvements.
The architectural decisions are sound:
The 7 milestones are independently testable with clear acceptance criteria.
Verdict: APPROVED | Next: Create branch
issue-55-stripe-integrationand begin M1 executionMilestone: M1 Complete - Database Schema + Stripe Client
Phase: Execution | Agent: Feature Agent | Status: PASS
Files Created
migrations/001_subscriptions_tables.sqldomain/subscriptions.types.tsexternal/stripe/stripe.client.tsexternal/stripe/stripe.types.tsdata/subscriptions.repository.tsindex.tsREADME.mdSchema Overview
Quality Checks
Commit
88b820bfeat: add subscriptions feature capsule - M1 database schema and Stripe client (refs #55)Verdict: PASS | Next: M2 - Service Layer + Webhook Endpoint
Milestone: M2 Complete - Service Layer + Webhook Endpoint
Phase: Execution | Agent: Feature Agent | Status: PASS
Files Created
domain/subscriptions.service.tsapi/webhooks.controller.tsapi/webhooks.routes.tsService Methods
getSubscription(userId)createSubscription(userId, email)upgradeSubscription(userId, tier, cycle, paymentMethodId)cancelSubscription(userId)reactivateSubscription(userId)handleWebhookEvent(payload, signature)Webhook Events Handled
customer.subscription.created- Update subscription with Stripe subscription IDcustomer.subscription.updated- Sync status, tier, period datescustomer.subscription.deleted- Mark canceled, downgrade to freeinvoice.payment_succeeded- Clear grace periodinvoice.payment_failed- Set 30-day grace periodKey Features
Quality Checks
Commit
7a0c09bfeat: add subscriptions service layer and webhook endpoint - M2 (refs #55)Verdict: PASS | Next: M3 - API Endpoints + Grace Period Job
Milestone: M3 Complete - API Endpoints + Grace Period Job
Phase: Execution | Agent: Feature Agent | Status: PASS
Files Created/Modified
api/subscriptions.controller.tsapi/subscriptions.routes.tsjobs/grace-period.job.tsnotifications/.../006_payment_email_templates.sqlcore/scheduler/index.tsexternal/stripe/stripe.client.tsdomain/subscriptions.service.tsAPI Endpoints
/api/subscriptions/api/subscriptions/checkout/api/subscriptions/cancel/api/subscriptions/reactivate/api/subscriptions/payment-method/api/subscriptions/invoicesGrace Period Job
Email Templates Added
payment_failed_immediate- Sent on first payment failurepayment_failed_7day- Sent 7 days before grace endspayment_failed_1day- Sent 1 day before grace endsQuality Checks
Commit
e7461a4feat: add subscription API endpoints and grace period job - M3 (refs #55)Verdict: PASS | Next: M4 - Frontend Subscription Page
Milestone: M4 Complete - Frontend Subscription Page
Phase: Execution | Agent: Frontend Agent | Status: PASS
Files Created
types/subscription.types.tsapi/subscription.api.tshooks/useSubscription.tscomponents/TierCard.tsxcomponents/PaymentMethodForm.tsxcomponents/BillingHistory.tsxpages/SubscriptionPage.tsxmobile/SubscriptionMobileScreen.tsxconstants/plans.tsDesktop Layout
Mobile Layout
Dependencies Added
Routes Added (App.tsx)
/garage/settings/subscriptionSubscriptionscreenQuality Checks
Commit
94d1c67feat: add frontend subscription page - M4 (refs #55)Verdict: PASS | Next: M5 - Vehicle Selection + Downgrade Flow
Milestone: M5 Complete - Vehicle Selection + Downgrade Flow
Phase: Execution | Agent: Feature Agent | Status: PASS
Files Created
VehicleSelectionDialog.tsxDowngradeFlow.tsxFiles Modified
vehicles.types.tsVehicleWithTierStatusinterfacevehicles.service.tsgetUserVehiclesWithTierStatus()for tier-gated filteringsubscriptions.service.tsdowngradeSubscription()methodsubscriptions.controller.tsdowngrade()endpoint handlersubscriptions.routes.ts/subscriptions/downgraderoutesubscription.api.tsdowngrade()API calluseSubscription.tsuseDowngrade()hookSubscriptionPage.tsxDowngrade Flow
tier_vehicle_selectionsTier Limits
Quality Checks
Commit
6c1a100feat: add vehicle selection and downgrade flow - M5 (refs #55)Verdict: PASS | Next: M6 - Donations Feature
Milestone: M6 Complete - Donations Feature
Phase: Execution | Agent: Feature Agent | Status: PASS
Files Created
donations.service.tsdonations.controller.tsdonations.routes.tsDonationSection.tsxDonationSectionMobile.tsxFiles Modified
app.tssubscriptions.service.tsstripe.client.tsstripe.types.tssubscription.api.tsuseSubscription.tsSubscriptionPage.tsxSubscriptionMobileScreen.tsxAPI Endpoints
/api/donations/api/donationsFeatures
Quality Checks
Commit
56da99dfeat: add donations feature with one-time payments - M6 (refs #55)Verdict: PASS | Next: M7 - Documentation
Milestone: M7 Complete - Documentation
Phase: Execution | Agent: Technical Writer | Status: PASS
Files Updated
backend/.../subscriptions/README.mdbackend/.../subscriptions/CLAUDE.mdfrontend/.../subscription/README.mdDocumentation Contents
Quality Checks
Commit
1cf4b78docs: update subscription feature documentation - M7 (refs #55)All Milestones Complete
Verdict: ALL MILESTONES PASS | Next: Quality Agent review and PR
Fix: Stripe Secrets Deployment Configuration
Phase: Execution | Agent: Developer | Status: PASS
Problem Identified
Stripe secrets were not being picked up during deployment due to missing configuration in multiple locations:
VITE_STRIPE_PUBLISHABLE_KEYwas not passed to Dockerfile during buildinject-secrets.shdid not include Stripe secretsFiles Modified
frontend/DockerfileARGandENVforVITE_STRIPE_PUBLISHABLE_KEYdocker-compose.ymlVITE_STRIPE_PUBLISHABLE_KEYbuild arg, fixed:roflags on Stripe secret mountsscripts/inject-secrets.shSTRIPE_SECRET_KEYandSTRIPE_WEBHOOK_SECRETto secret injection.gitea/workflows/staging.yamlVITE_STRIPE_PUBLISHABLE_KEYbuild arg and Stripe secrets to inject step.gitea/workflows/production.yamlGitea Configuration Required
Before deployment will work, the following must be configured in Gitea:
Secrets (Settings > Secrets):
STRIPE_SECRET_KEY- Stripe API secret key (sk_live_...)STRIPE_WEBHOOK_SECRET- Stripe webhook signing secret (whsec_...)Variables (Settings > Variables):
VITE_STRIPE_PUBLISHABLE_KEY- Stripe publishable key (pk_live_...)Commit
254bed1fix: add Stripe secrets to CI/CD and build configuration (refs #55)Verdict: PASS | Next: Configure Gitea secrets/variables, then trigger staging deployment
Subscription Section Added to Settings Page
Added the missing Subscription section to both desktop and mobile Settings screens.
Changes
Desktop Settings Page (
frontend/src/pages/SettingsPage.tsx):/garage/settings/subscriptionMobile Settings Screen (
frontend/src/features/settings/mobile/MobileSettingsScreen.tsx):Commits
feat: add Subscription section to Settings page (refs #55)fix: correct subscription description when data unavailable (refs #55)feat: add Subscription section to mobile Settings (refs #55)Verified On Staging
Plan: Enhanced Billing Details Collection
Phase: Planning | Agent: Claude | Status: IN_PROGRESS
Problem Statement
Current
CardElementimplementation only collects card number, expiration, and CVC. Missing: Name, Address, City, State, Zip, Country.Solution: PaymentElement + AddressElement (Billing Mode)
This is the most user-friendly approach:
Implementation Plan
Milestone 1: Update PaymentMethodForm.tsx (Desktop)
CardElementwithPaymentElementAddressElementwithmode: 'billing'PaymentElementto skip billing address (collected by AddressElement)stripe.confirmSetup()or pass billing detailsMilestone 2: Update Mobile Payment Form
Milestone 3: Update DonationSection (Desktop)
CardElementwithPaymentElement+AddressElementconfirmCardPaymentto include billing detailsMilestone 4: Update DonationSectionMobile
Milestone 5: Quality Validation
Fields Collected
Technical Notes
@stripe/react-stripe-jspackage supports both elementsVerdict: APPROVED | Next: Execute Milestone 1
Milestone: Enhanced Billing Details Collection - Complete
Phase: Execution | Agent: Claude | Status: PASS
Changes Made
Milestone 1: PaymentMethodForm.tsx (Desktop Subscriptions)
CardElementwithPaymentElement+AddressElementmode: 'setup'Elements configuration for payment method creationMilestone 2: SubscriptionMobileScreen.tsx (Mobile Subscriptions)
paymentElementsOptionsPaymentMethodFormcomponentMilestone 3: DonationSection.tsx (Desktop Donations)
AddressElementfor billing address collectionconfirmCardPaymentto include full billing_detailsCardElementfor card input (works best with PaymentIntent flow)Milestone 4: DonationSectionMobile.tsx (Mobile Donations)
AddressElementwith mobile-friendly stylingFields Now Collected
Quality Validation
Files Modified
frontend/src/features/subscription/components/PaymentMethodForm.tsxfrontend/src/features/subscription/pages/SubscriptionPage.tsxfrontend/src/features/subscription/mobile/SubscriptionMobileScreen.tsxfrontend/src/features/subscription/components/DonationSection.tsxfrontend/src/features/subscription/components/DonationSectionMobile.tsxVerdict: PASS | Next: Commit and test in browser