@molecule/api-resource-payment-method
API resource · resource-payment-method · API (Node) · v1.0.4 · Apache-2.0
Saved Stripe PaymentMethods with metadata + default flag.
npm install @molecule/api-resource-payment-methodHow it works
@molecule/api-resource-payment-method is an API resource: the routes, validation and storage for resource-payment-method, built on the database and auth cores so it runs on whichever providers your app has bonded.
import { routes, requestHandlerMap } from '@molecule/api-resource-payment-method'
// Mount via mlcl-generated router; service-level usage:
import {
createSetupIntent,
attachPaymentMethod,
listPaymentMethods,
setDefaultPaymentMethod,
deletePaymentMethod,
} from '@molecule/api-resource-payment-method'Works with: @molecule/api-bond, @molecule/api-i18n, @molecule/api-payments, @molecule/api-resource, @molecule/api-secrets
Secrets: STRIPE_SECRET_KEY
Reference
Saved payment-method resource for molecule.dev.
Wraps the Stripe SetupIntent flow (and any future card-style provider) into a database-backed list of saved payment methods with a per-user default.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { routes, requestHandlerMap } from '@molecule/api-resource-payment-method'
// Mount via mlcl-generated router; service-level usage:
import {
createSetupIntent,
attachPaymentMethod,
listPaymentMethods,
setDefaultPaymentMethod,
deletePaymentMethod,
} from '@molecule/api-resource-payment-method'
Type
resource
Installation
npm install @molecule/api-resource-payment-method @molecule/api-bond @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-payments @molecule/api-resource @molecule/api-secrets
API
Interfaces
PaymentMethod
A saved payment method as returned to API consumers.
interface PaymentMethod {
/** Unique identifier for the saved payment method (UUID). */
id: string
/** Owner of this payment method. */
userId: string
/** Provider that issued the payment method (`stripe`, etc.). */
provider: PaymentMethodProvider
/** Provider customer ID (e.g. Stripe `cus_...`). */
providerCustomerId: string
/** Provider payment-method ID (e.g. Stripe `pm_...`). */
providerPaymentMethodId: string
/** Last four digits of the card. */
last4: string
/** Card brand (e.g. `visa`, `mastercard`). */
brand: string
/** Two-digit expiry month (1–12). */
expMonth: number
/** Four-digit expiry year. */
expYear: number
/** Whether this is the user's default saved payment method. */
isDefault: boolean
/** ISO 8601 creation timestamp. */
createdAt: string
}
PaymentMethodRow
Internal database row for a saved payment method.
interface PaymentMethodRow {
id: string
userId: string
provider: string
providerCustomerId: string
providerPaymentMethodId: string
last4: string
brand: string
expMonth: number
expYear: number
isDefault: boolean
createdAt: string
}
SetupIntentResponse
Result returned to the client after creating a SetupIntent.
interface SetupIntentResponse {
/** Provider SetupIntent ID. */
id: string
/** Client secret consumed by the frontend SDK to confirm the SetupIntent. */
clientSecret: string
/** Provider customer ID this SetupIntent is attached to. */
customerId: string
/** Provider that issued the SetupIntent. */
provider: PaymentMethodProvider
}
AttachPaymentMethodInput
Input body for POST /me/payment-methods after the SetupIntent confirms.
interface AttachPaymentMethodInput {
/** Provider payment-method ID returned by the frontend SDK after confirmation. */
providerPaymentMethodId: string
/** Optional flag — when `true`, mark this method as default after attaching. */
setDefault?: boolean
}
Types
PaymentMethodProvider
Supported saved-payment-method providers.
Open string type so additional rails can be added without editing this
package. Well-known values include stripe.
type PaymentMethodProvider = string
Functions
createSetupIntent(userId)
Creates a SetupIntent for the saved-card flow.
Reuses the user's provider customer ID if one already exists; otherwise the
provider creates a new customer and the resulting ID is returned to the
client (and persisted on the next attachPaymentMethod call).
function createSetupIntent(userId: string): Promise<SetupIntentResponse>
userId— The user the SetupIntent is being created for.
Returns: The SetupIntent payload to forward to the frontend SDK.
attachPaymentMethod(userId, providerPaymentMethodId)
Records a saved payment method after the SetupIntent confirms client-side.
Looks the payment method up via the bonded payments provider to capture brand/last4/exp, then persists it. If the user has no other saved methods, the new method is marked as default.
function attachPaymentMethod(
userId: string,
providerPaymentMethodId: string,
): Promise<PaymentMethod>
userId— The owning user.providerPaymentMethodId— The payment-method ID returned by the frontend SDK.
Returns: The persisted payment method.
listPaymentMethods(userId)
Lists every saved payment method for a user, newest first.
function listPaymentMethods(userId: string): Promise<PaymentMethod[]>
userId— The owning user.
Returns: The user's saved payment methods.
getPaymentMethod(id, userId)
Looks up a single saved payment method, enforcing ownership.
function getPaymentMethod(id: string, userId: string): Promise<PaymentMethod | null>
id— The payment-method row ID.userId— The user expected to own the row.
Returns: The payment method, or null if not found / not owned by userId.
setDefaultPaymentMethod(userId, id)
Marks a payment method as the user's default, clearing the flag from any other saved methods belonging to the same user.
function setDefaultPaymentMethod(userId: string, id: string): Promise<PaymentMethod | null>
userId— The owning user.id— The payment-method row to promote.
Returns: The updated default payment method, or null if not found / not owned.
deletePaymentMethod(id, userId)
Deletes a saved payment method, detaching it from the provider first.
The local row is removed even if the provider detach call fails — the provider error is logged but does not abort the delete (orphaned records at the provider are preferable to a method we can't remove from our own UI).
function deletePaymentMethod(id: string, userId: string): Promise<boolean>
id— The payment-method row to delete.userId— The user expected to own the row.
Returns: true on success, false if the row was not found or not owned by userId.
toPaymentMethod(row)
Converts a raw database row into a typed PaymentMethod.
function toPaymentMethod(row: PaymentMethodRow): PaymentMethod
row— The database row.
Returns: The deserialized payment method.
Constants
i18nRegistered
Whether i18n registration has been attempted. Always true; this module is
a placeholder for symmetry with locale-bonded resources.
const i18nRegistered: true
requestHandlerMap
Handler map for the payment-method resource routes.
const requestHandlerMap: {
readonly createSetupIntent: typeof createSetupIntent
readonly listPaymentMethods: typeof listPaymentMethods
readonly setDefaultPaymentMethod: typeof setDefaultPaymentMethod
readonly deletePaymentMethod: typeof deletePaymentMethod
}
routes
Saved payment-method routes.
const routes: readonly [
{
readonly method: 'post'
readonly path: '/me/payment-methods/setup-intent'
readonly handler: 'createSetupIntent'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'get'
readonly path: '/me/payment-methods'
readonly handler: 'listPaymentMethods'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'put'
readonly path: '/me/payment-methods/:id/default'
readonly handler: 'setDefaultPaymentMethod'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'delete'
readonly path: '/me/payment-methods/:id'
readonly handler: 'deletePaymentMethod'
readonly middlewares: readonly ['authenticate']
},
]
resourcePaymentMethodSecretDefinitions
Secret definitions required by the payment-method resource.
const resourcePaymentMethodSecretDefinitions: SecretDefinition[]
PROVIDER_NAME
The provider name used by this resource. Currently fixed to stripe —
a future cross-rail rollout would dispatch on the user's selection.
const PROVIDER_NAME: 'stripe'
TABLE_NAME
The database table name for saved payment methods.
const TABLE_NAME: 'payment_methods'
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bond^1.0.1@molecule/api-database^1.0.1@molecule/api-i18n^1.0.1@molecule/api-logger^1.0.1@molecule/api-payments^1.0.1@molecule/api-resource^1.0.1@molecule/api-secrets^1.0.1
Environment Variables
STRIPE_SECRET_KEY(required) — Stripe secret key- Setup: Stripe Dashboard → Developers → API keys; use the sk_test_ key in test mode, sk_live_ in production.
- Get it here: https://dashboard.stripe.com/apikeys
- Example:
sk_test_...
Runtime Dependencies
@molecule/api-bond@molecule/api-database@molecule/api-i18n@molecule/api-logger@molecule/api-payments@molecule/api-resource@molecule/api-secrets
Bond ordering: the service resolves the provider with
get('payments', 'stripe') — wire @molecule/api-payments-stripe under the
name stripe (bond('payments', 'stripe', provider)) BEFORE any route
runs, and set STRIPE_SECRET_KEY. The provider name is currently fixed
(PROVIDER_NAME = 'stripe'); other card-style providers plug in by
implementing the same SetupIntent-shaped PaymentProvider surface.
Table: src/__setup__/payment_methods.sql creates payment_methods. An
mlcl-scaffolded API replays __setup__/*.sql on migrate (a CREATE TABLE IF NOT EXISTS, so a table the app's own migrations already create keeps only its own columns: ALTER in the ones it lacks);
anywhere else run it once.
All routes are SELF-scoped under /me/payment-methods and read the
authenticated user from res.locals.session (401 without a session) — a
user can only list/attach/default/delete their OWN methods; never accept a
target userId from the client. Raw card data never touches your API: the
client confirms the SetupIntent with the provider and only the provider's
payment-method id is attached and stored.
E2E Tests
SECURITY / PCI-critical verification — drive the real UI (live preview, no
mocks) and adapt each item to this app's actual card screens. Check every
box off one by one; a box you can't check is a security bug to fix, not a
skip. NEVER type a real card number anywhere — use the provider's TEST card
(Stripe test mode, 4242 4242 4242 4242), and it must go to the provider
SDK, never to your API:
- Adding a card via the SetupIntent flow stores only a provider TOKEN
(
providerPaymentMethodIdlikepm_…,providerCustomerIdlikecus_…) plus safe display fields (brand,last4,expMonth,expYear). Confirm withGET /me/payment-methodsthat the response — and thepayment_methodstable schema — carry NO full card number (PAN) and NO CVV: the raw PAN/CVV must appear NOWHERE in the DB row, the API response, or the server logs. - The UI shows only the masked card (
brand+•••• 4242) — never the full number and never the CVV. - Setting a default makes exactly ONE default: the first card added is
auto-default; promoting a second card flips the old default's
isDefaultto false, so only one method hasisDefault: true(a partial unique index enforces this at the DB level). - Removing a method deletes it (
DELETE /me/payment-methods/:id→ 204, gone from the list) AND detaches it at the provider, so it can no longer be charged. - AUTHORIZATION — every route is
/me/…-scoped to the session user: a user lists/adds/defaults/deletes only their OWN methods. Guessing another user's payment-method id intoPUT /me/payment-methods/:id/defaultorDELETE /me/payment-methods/:idreturns 404 (never touches their card), and no endpoint accepts a target userId from the client. - The provider secret (
STRIPE_SECRET_KEY) stays server-side only — never shipped to the browser bundle (this package is server-only). The card is tokenized client-side by the provider SDK using the SetupIntent client secret, so the raw card never touches your server.