← All @molecule/* packages · App templates

@molecule/api-resource-payment-method

API resource · resource-payment-method · API (Node) · v1.0.1 · Apache-2.0

Saved Stripe PaymentMethods with metadata + default flag.

npm install @molecule/api-resource-payment-method

npm · Source on GitHub

How 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

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.ts JSDoc, not this file.

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.

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

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
}

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
}

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

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.

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.

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.

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.

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.

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.

toPaymentMethod(row)

Converts a raw database row into a typed {@link 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

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'

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
}

resourcePaymentMethodSecretDefinitions

Secret definitions required by the payment-method resource.

const resourcePaymentMethodSecretDefinitions: SecretDefinition[]

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']
  },
]

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 automatically on migrate; 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 (providerPaymentMethodId like pm_…, providerCustomerId like cus_…) plus safe display fields (brand, last4, expMonth, expYear). Confirm with GET /me/payment-methods that the response — and the payment_methods table 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 isDefault to false, so only one method has isDefault: 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 into PUT /me/payment-methods/:id/default or DELETE /me/payment-methods/:id returns 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.