Stripe + Clerk Subscription Integration
Howreflex-stripe and reflex-clerk-api work together to handle payment checkout, subscription lifecycle, and user entitlement in Syntropy-Journals.
Architecture
Subscription data lives in three systems kept in sync:| System | Role | Access |
|---|---|---|
| Stripe | Billing engine, subscription lifecycle | API + webhooks |
| PostgreSQL | Audit trail, backend queries (Subscription model) | db_utils/subscription.py |
Clerk public_metadata | Fast frontend entitlement check via JWT | ClerkUser.public_metadata["stripe"] |
Metadata Schema
Stored underpublic_metadata.stripe on the Clerk user:
None are removed, unprovided keys are preserved. 8 KB limit per metadata type.
Data Flow
1. Checkout (frontend → Stripe → DB + Clerk)
File:syntropy_journals/app/states/user/subscription.py
/subscription/success?session_id=..., the success handler runs:
2. Webhook (Stripe → DB + Clerk)
File:syntropy_journals/app/api/routes/stripe_webhook.py
Handles four lifecycle events:
| Event | Action |
|---|---|
checkout.session.completed | Provision: upsert DB row + set Clerk metadata |
customer.subscription.updated | Sync: update DB status + update Clerk metadata |
customer.subscription.deleted | Cancel: mark DB canceled + set Clerk status=canceled |
invoice.payment_failed | Degrade: mark DB past_due + set Clerk status=past_due |
clerk_backend_api.Clerk directly (not the Reflex state wrapper) since it runs in a FastAPI endpoint without a Reflex event context:
checkout.session.completed→session.client_reference_id(set during checkout)- Other events → reverse lookup:
stripe_customer_id→User.clerk_idvia DB
3. Reading subscription status (Clerk → AuthState)
File:syntropy_journals/app/states/shared/clerk_auth.py
On every authenticated page load, AuthState reads the Clerk metadata:
Key Libraries
reflex-clerk-api (v1.2.4)
Provides two ways to update Clerk user metadata:| API | Context | Usage |
|---|---|---|
clerk.update_user_metadata(self, public_metadata=...) | Reflex event handlers | SubscriptionState.handle_payment_success() |
clerk_backend_api.Clerk().users.update_metadata(...) | FastAPI endpoints, webhooks | stripe_webhook.py |
ClerkUser state exposes public_metadata, private_metadata, and unsafe_metadata as dict[str, Any] fields, populated automatically during load_user().
reflex-stripe (v0.1.1)
Provides the Embedded Checkout UI (embedded_checkout_session()) and Express Checkout (stripe_provider() + ExpressCheckoutBridge). The actual subscription management (creating sessions, handling webhooks) is done by the main app using the stripe Python SDK directly — reflex-stripe handles the frontend checkout form only.
Environment Variables
| Variable | Used by | Required for |
|---|---|---|
STRIPE_SECRET_KEY | subscription.py, stripe_webhook.py | Stripe API calls |
STRIPE_WEBHOOK_SECRET | stripe_webhook.py | Webhook signature verification |
CLERK_SECRET_KEY | stripe_webhook.py | Updating Clerk metadata from webhooks |
STRIPE_PUBLISHABLE_KEY | Frontend (reflex-stripe) | Checkout UI initialization |
Files
| File | Purpose |
|---|---|
syntropy_journals/app/states/user/subscription.py | SubscriptionState — checkout + payment success |
syntropy_journals/app/api/routes/stripe_webhook.py | Webhook endpoint — lifecycle sync |
syntropy_journals/app/states/shared/clerk_auth.py | AuthState — reads public_metadata.stripe |
syntropy_journals/app/functions/db_utils/subscription.py | DB helpers (upsert, status update, reverse lookup) |
syntropy_journals/app/models/admin/subscription.py | Subscription SQLModel |
syntropy_journals/app/models/admin/user.py | User model with clerk_id field |