← All @molecule/* packages · App templates
@molecule/api-resource-paymentAPI resource · payments · API (Node) · v1.0.1 · Apache-2.0
Payment resource with subscription plan management
npm install @molecule/api-resource-payment@molecule/api-resource-payment is an API resource: the routes, validation and storage for payments, built on the database and auth cores so it runs on whichever providers your app has bonded.
import { registerPlans, stripeMonthly, stripeYearly } from '@molecule/api-resource-payment'
// Register your plan catalogue at startup. The keys are YOUR plan ids; the ready-made
// Plan objects carry the env-configured Stripe price/product ids (also register the
// apple and google plan exports when you support those providers):
registerPlans({ monthly: stripeMonthly, yearly: stripeYearly })
// Then grant a plan ONLY after a SERVER-VERIFIED payment — never from a client field.
// The full verify → record (replay-guarded) → resolve → grant flow lives in the scaffolded
// user `verifyPayment` handler; verify receipts against @molecule/api-payments and store
// them with `paymentRecordService.store` (it THROWS on a replayed transactionId — reject).Works with: @molecule/api-bond, @molecule/api-database, @molecule/api-i18n, @molecule/api-locales-payment, @molecule/api-logger, @molecule/api-payments, @molecule/api-resource
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.
Payment resource with subscription plan management.
import { registerPlans, stripeMonthly, stripeYearly } from '@molecule/api-resource-payment'
// Register your plan catalogue at startup. The keys are YOUR plan ids; the ready-made
// Plan objects carry the env-configured Stripe price/product ids (also register the
// apple and google plan exports when you support those providers):
registerPlans({ monthly: stripeMonthly, yearly: stripeYearly })
// Then grant a plan ONLY after a SERVER-VERIFIED payment — never from a client field.
// The full verify → record (replay-guarded) → resolve → grant flow lives in the scaffolded
// user `verifyPayment` handler; verify receipts against @molecule/api-payments and store
// them with `paymentRecordService.store` (it THROWS on a replayed transactionId — reject).
resource
npm install @molecule/api-resource-payment @molecule/api-bond @molecule/api-database @molecule/api-i18n @molecule/api-locales-payment @molecule/api-logger @molecule/api-payments @molecule/api-resource zod
CreatePropsFields required when creating a new payment record.
type CreateProps = z.infer<typeof createPropsSchema>
PropsFull payment record properties (userId, platform, transactionId, productId, data, receipt).
type Props = z.infer<typeof propsSchema>
UpdatePropsUpdatable payment record fields (data and receipt).
type UpdateProps = z.infer<typeof updatePropsSchema>
PaymentRecordConflictErrorError thrown by {@link paymentRecordService.store} when inserting a payment
record violates the UNIQUE(platformKey, transactionId) constraint — i.e. the
transaction is already bound to an account.
This is surfaced (not swallowed) so callers such as the user resource's
verifyPayment handler can enforce first-claim-wins ownership and reject a
replayed receipt instead of silently granting the plan to a second account.
getPeriodTime(period)Get the duration in milliseconds for a billing period.
function getPeriodTime(period: PlanPeriod): number
period — The billing period ('month' or 'year').Returns: The period duration in milliseconds.
registerPlans(customPlans)Registers (or replaces) plans in the shared registry consumed by
planService — keyed by planKey, merged into the defaults above.
The built-in entries carry PLACEHOLDER platform product ids
(price_test_id / price_prod_id), so findPlanByProductId can never
match a real platform identifier until the app registers its own catalogue
— typically derived from its pricing tiers with the real Stripe price ids
from env (@molecule/api-bonds-default-express's createBillingRouter
does this automatically). Without registration, payment verification and
webhook plan grants fail with "unknown plan" even though the charge
succeeded.
Idempotent: re-registering the same planKey overwrites that entry.
function registerPlans(customPlans: Record<string, Plan>): void
customPlans — Plans to merge into the registry, keyed by planKey.appleMonthlyApple App Store monthly subscription plan ($5.99/month, auto-renews).
const appleMonthly: Plan
appleYearlyApple App Store yearly subscription plan ($64.99/year, auto-renews).
const appleYearly: Plan
createPropsSchemaZod schema for creating a payment record (picks fields required at creation time).
const createPropsSchema: z.ZodObject<
{
createdAt: z.ZodString
updatedAt: z.ZodString
userId: z.ZodString
platformKey: z.ZodDefault<
z.ZodEnum<{ '': ''; stripe: 'stripe'; apple: 'apple'; google: 'google' }>
>
transactionId: z.ZodOptional<z.ZodString>
productId: z.ZodString
data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>
receipt: z.ZodOptional<z.ZodString>
},
z.core.$strip
>
googleMonthlyGoogle Play monthly subscription plan ($5.99/month, auto-renews).
const googleMonthly: Plan
googleYearlyGoogle Play yearly subscription plan ($64.99/year, auto-renews).
const googleYearly: Plan
i18nRegisteredThe i18n registered.
const i18nRegistered: true
paymentRecordServicePaymentRecordService implementation for the bond system.
Provides payment record CRUD operations that other resources
can use through get<PaymentRecordService>('paymentRecords').
const paymentRecordService: PaymentRecordService
plansAll available plans indexed by planKey. The empty string key '' maps to the default free plan.
const plans: Record<string, Plan>
planServicePlanService implementation for the bond system.
Provides plan lookup operations that other resources
can use through get('plans') / require('plans').
const planService: PlanService
propsSchemaValidation schema for props.
const propsSchema: z.ZodObject<
{
id: z.ZodString
createdAt: z.ZodString
updatedAt: z.ZodString
userId: z.ZodString
platformKey: z.ZodDefault<
z.ZodEnum<{ '': ''; stripe: 'stripe'; apple: 'apple'; google: 'google' }>
>
transactionId: z.ZodOptional<z.ZodString>
productId: z.ZodString
data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>
receipt: z.ZodOptional<z.ZodString>
},
z.core.$strip
>
resourcePayment resource definition with JSON schema for validation.
const resource: types.Resource
stripeMonthlyStripe monthly subscription plan ($5/month, auto-renews).
const stripeMonthly: Plan
stripeYearlyStripe yearly subscription plan ($55/year, auto-renews).
const stripeYearly: Plan
updatePropsSchemaZod schema for updating a payment record (data and receipt only).
const updatePropsSchema: z.ZodObject<
{
data: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>
receipt: z.ZodOptional<z.ZodOptional<z.ZodString>>
},
z.core.$strip
>
typesMembers:
types.PlatformKey — type: Every available platform.types.PlanAlias — type: Every available plan alias.types.PlanPeriod — type: Every available plan period.types.Props — interface: The payment's properties.types.CreateProps — type: Properties when creating a payment.types.UpdateProps — type: Properties when updating a payment.types.Plan — interface: A plan's properties.types.Resource — interface: Resource type.This package exports services that should be registered with the bond system:
import { bond } from '@molecule/api-bond'
import { planService, paymentRecordService } from '@molecule/api-resource-payment'
bond('plans', planService)
bond('paymentRecords', paymentRecordService)
Peer dependencies:
@molecule/api-bond ^1.0.1@molecule/api-database ^1.0.1@molecule/api-i18n ^1.0.1@molecule/api-locales-payment ^1.0.1@molecule/api-logger ^1.0.1@molecule/api-payments ^1.0.1@molecule/api-resource ^1.0.1@molecule/api-bond@molecule/api-database@molecule/api-i18n@molecule/api-locales-payment@molecule/api-logger@molecule/api-payments@molecule/api-resourcezodA plan/entitlement is granted ONLY after a SERVER-VERIFIED payment — never from anything
the client reports. The flow (implemented by the user resource's verifyPayment handler):
@molecule/api-payments — read its docs). Trust only its normalized result..store, which enforces
UNIQUE(platformKey, transactionId) so one receipt binds to exactly ONE account.productId and priceId), then grant it.Weak-integration mistakes to avoid:
.store
THROWS when a transactionId is already claimed — that is the replay guard
(first-claim-wins). Catch it and REJECT; do not grant the plan to a second account.price_…), and resolve by BOTH product and price id — providers
report the product on a subscription while checkout uses the price.The payments table ships in setup/payments.sql — an mlcl-scaffolded API
copies and replays it automatically on migrate; anywhere else run it once.
Missing it surfaces as relation "payments" does not exist on the first
billing call — nothing at runtime creates the table.
Integration checklist — drive the real UI (live preview, no mocks), adapt
each item to this app's actual billing screens/flows, and check every box
off one by one. A box you can't check is an integration bug to fix — not a
skip. This resource RECORDS a server-verified provider result (a receipt or
subscription id verified via @molecule/api-payments) and resolves the plan
— it does NOT charge cards and has no amount/status column, so drive the
provider's real verify path; never fabricate a record or a status:
price) or the
provider's verified data — never a client-sent field. An amount renders
formatted with its currency, not as a raw smallest-unit integer.handlePaymentNotification) REVOKES it
so the user loses premium. No request field (planKey, isPro, a bare
subscriptionId) lets the client set or keep an active/paid status.