Payments (Stripe) — Technical Reference
Where this lives
Backend
apps/backend/graphql/resolvers/payment-transaction.resolver.js— resolvesmyPaymentTransactions,paymentTransaction,paymentTransactionStatsapps/backend/graphql/resolvers/payment-methods.resolvers.js— resolvesmyPaymentMethods,addPaymentMethod,setDefaultPaymentMethod,removePaymentMethod,syncPaymentMethodsapps/backend/graphql/types/payment-transaction.type.js—PaymentTransactionGraphQL schemaapps/backend/graphql/types/payment-method.type.js—PaymentMethodGraphQL schemaapps/backend/managers/payment-managers/payment-transaction.manager.js— transaction business logic, including Stripe webhook-driven status updatesapps/backend/managers/payment-managers/payment-method.manager.js— attaches/detaches/syncs saved payment methods with Stripeapps/backend/services/stripe/payment-methods.stripe.js— low-level Stripe API calls for payment methods/cardsapps/backend/services/stripe/payment-intents.stripe.js— low-level Stripe API calls for PaymentIntents (coin purchase / checkout flow)
Frontend
apps/frontend-nextjs/src/app/payments/page.tsx—/paymentsnow just redirects to/settings/payment-methodsapps/frontend-nextjs/src/page-components/settings/PaymentMethodsPage.tsx— billing / saved payment methods UI at/settings/payment-methods, uses Stripe Elements directlyapps/frontend-nextjs/src/page-components/settings/PaymentHistoryPage.tsx— general Stripe payment history UI at/settings/payment-history, queriesmyPaymentTransactions/paymentTransactionStatsdirectlyapps/frontend-nextjs/src/components/payment/StripeCheckout.tsx— Stripe Elements checkout component used on the coin purchase page for the flow described above
The old apps/frontend-nextjs/src/page-components/PaymentsPage.tsx is no longer wired to any route (/payments redirects instead of rendering it) — it has been superseded by the two settings/ pages above.
Technical implementation checklist
-
addPaymentMethod— resolver wired inpayment-methods.resolvers.js; called fromPaymentMethodsPage.tsx -
myPaymentMethods— query wired;PaymentMethodsPage.tsxlists cards via ApollouseQuery -
purchaseCoinsWithPayment— resolver wired incoin-purchase.resolver.js, backed by the Stripe PaymentIntent service; called fromStripeCheckout.tsx -
myPaymentTransactions/paymentTransactionStats— resolvers wired inpayment-transaction.resolver.js; queried byPaymentHistoryPage.tsxat/settings/payment-history, distinct from the coin-purchase-only history shown byCoinTransactionsPage.tsx/myCoinPurchases -
removePaymentMethod— resolver wired; called fromPaymentMethodsPage.tsx; dispatches to PayPal's delete-payment-token API forprovider: 'paypal'methods -
createPaypalVaultSetupToken/confirmPaypalVaultSetup— save a PayPal account as a reusable payment method; wired inpayment-methods.resolvers.js+PaymentMethodsPage.tsx's "Connect PayPal" flow
PaymentTransaction model
| Field | Description |
|---|---|
provider | Always stripe |
providerPaymentIntentId | Stripe PaymentIntent ID |
type | coin_purchase / subscription / tip / other |
purpose | Human-readable purpose |
amount / currency | Amount and currency |
feeAmount / netAmount | Stripe fee and net received |
status | pending / processing / completed / failed / refunded / cancelled |
failureCode / failureMessage | Failure details |
refundAmount / refundReason | Refund info |
Payment method types
enum PaymentMethodType {
credit_card · debit_card · paypal · stripe · apple_pay · google_pay
}
Queries
myPaymentTransactions returns the authenticated user's payment history. Filter by status (e.g. completed) or purpose to narrow results. Use this to power an "Order history" screen.
paymentTransaction fetches a single transaction by ID — useful for a post-purchase confirmation screen or support flows where failureMessage needs to be surfaced.
paymentTransactionStats returns aggregate counters without paginating through every record. Use it for the "total spent" summary on the billing page.
query MyPaymentTransactions($status: String, $purpose: String, $limit: Int, $offset: Int) {
myPaymentTransactions(status: $status, purpose: $purpose, limit: $limit, offset: $offset) {
id type purpose amount currency status createdAt
providerPaymentIntentId
}
}
query PaymentTransaction($id: ID!) {
paymentTransaction(transactionId: $id) { id amount status failureMessage }
}
query PaymentStats {
paymentTransactionStats {
totalTransactions totalAmount completedTransactions failedTransactions
}
}
Payment methods
Users can save Stripe payment methods for faster future purchases. The billingAddress block is required by Stripe for card verification.
myPaymentMethods lists all saved methods. Pass includeInactive: true to also show expired or removed cards (useful in support flows).
addPaymentMethod attaches a Stripe PaymentMethod ID (from the client-side Stripe.js flow) to the user's account. Pass setAsDefault: true to make it the default immediately.
setDefaultPaymentMethod changes which saved card is charged by default on future purchases.
removePaymentMethod detaches the method from both Closegram and Stripe. Can't remove the default method if others exist — set a new default first.
syncPaymentMethods resolves drift between the local database and Stripe's records. Call it if a user reports a card showing as valid when it was deleted in Stripe.
query MyPaymentMethods($includeInactive: Boolean) {
myPaymentMethods(includeInactive: $includeInactive) {
id type cardBrand cardLast4 cardExpMonth cardExpYear
isDefault isActive billingName billingEmail
billingAddress { line1 line2 city state postalCode country }
}
}
mutation AddPaymentMethod($paymentMethodId: String!, $setAsDefault: Boolean) {
addPaymentMethod(paymentMethodId: $paymentMethodId, setAsDefault: $setAsDefault) {
success message paymentMethod { id cardBrand cardLast4 isDefault }
}
}
mutation SetDefaultPaymentMethod($paymentMethodId: ID!) {
setDefaultPaymentMethod(paymentMethodId: $paymentMethodId) { success }
}
mutation RemovePaymentMethod($paymentMethodId: ID!) {
removePaymentMethod(paymentMethodId: $paymentMethodId) { success message }
}
# Sync stored methods with Stripe (resolves drift between local DB and Stripe)
mutation SyncPaymentMethods { syncPaymentMethods { success count } }
PayPal as a saved payment method (Vault API v3)
myPaymentMethods isn't Stripe-only: a user can also save a PayPal account (type: paypal), using PayPal's Vault API v3 instead of Stripe's payment-methods API. It reuses the same PaymentMethod/PaymentCustomer models — provider: 'paypal' on both — so myPaymentMethods, setDefaultPaymentMethod, and removePaymentMethod all work on a saved PayPal account exactly like they do on a card. removePaymentMethod dispatches by provider: for a PayPal method it calls PayPal's delete-payment-token endpoint instead of Stripe's detach.
Saving one is a two-step flow, driven from PaymentMethodsPage.tsx's "Connect PayPal" button:
createPaypalVaultSetupToken— starts a PayPal setup token (no purchase attached). The frontend passes itssetupTokenIdinto<PayPalButtons createVaultSetupToken={...}>(@paypal/react-paypal-js), which opens the approval popup.confirmPaypalVaultSetup(setupTokenId, setAsDefault)— called fromonApproveonce the buyer approves; exchanges the setup token for a permanent PayPal payment token and creates thePaymentMethodrow (billingEmailfrom PayPal's response).
mutation CreatePaypalVaultSetupToken {
createPaypalVaultSetupToken { setupTokenId }
}
mutation ConfirmPaypalVaultSetup($setupTokenId: String!, $setAsDefault: Boolean) {
confirmPaypalVaultSetup(setupTokenId: $setupTokenId, setAsDefault: $setAsDefault) {
success message
paymentMethod { id type isDefault isActive billingEmail }
}
}
Once saved, it can be charged directly — see Coins & Tips → Alternative payment methods for purchaseCoinsWithSavedPaypal, which charges the vaulted token server-side with no popup.
No new environment variables: this reuses PAYPAL_CLIENT_ID / PAYPAL_CLIENT_SECRET / PAYPAL_API_BASE / NEXT_PUBLIC_PAYPAL_CLIENT_ID from the one-time PayPal checkout setup — see apps/backend/docs/PAYMENTS_WALLETS_PAYPAL.md.
Coin purchase flow
1. User selects a coin package on /settings/get-coins
2. Frontend calls createPaymentIntent
3. Backend creates a Stripe PaymentIntent → returns clientSecret
4. Frontend renders <Elements> with the clientSecret
5. User enters card details and confirms
6. Stripe confirms payment → webhook notifies backend
7. Backend receives payment_intent.succeeded
8. Backend credits coins (CoinPurchase + CoinTransaction records)
9. Apollo cache updates myCoinBalance
Security
- Card data never passes through Closegram servers — Stripe handles it directly.
STRIPE_SECRET_KEYlives only on the backend.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYis the only key exposed to the frontend.- Purchases are validated via a webhook signed with
STRIPE_WEBHOOK_SECRET.
Frontend libraries
| Package | Purpose |
|---|---|
@stripe/stripe-js | Loads Stripe.js asynchronously |
@stripe/react-stripe-js | <Elements>, <CardElement>, useStripe() |