@prood/commerce
Server-only data layer wrapping the commerce adapter, payment providers, storage, and caching.
@prood/commerce is the server-only data layer used by apps/api. It wraps the configured CommerceAdapter, instantiates payment and storage providers, handles tenant scoping, and manages Next.js cache tags.
Installation
Workspace package. Available from the source repo for now.
This package is "server-only" — it cannot be imported in client components.
Usage
Only apps/api should import @prood/commerce directly. Other apps use @prood/api-client.
import {
getProducts,
createCart,
placeOrder,
getAdmin,
withTenant,
getTenantPaymentProvider,
verifyPaymentWebhook,
} from '@prood/commerce'Catalog functions
// Cached with per-tenant tags
const products = await getProducts(orgId, { query: 'shirt', page: 1, limit: 20 })
const product = await getProduct(orgId, 'prod_abc')
const categories = await getCategories(orgId)
const store = await getStoreInfo(orgId)Cache tags: products-{orgId}, categories-{orgId}, store-{orgId}.
Cart functions
const cart = await createCart(orgId)
const updated = await addCartItem(orgId, cartId, { productId, variantId, quantity: 1 })
await applyCartCoupon(orgId, cartId, 'SAVE10') // API and MCP only: stores have no coupon field
const order = await placeOrder(orgId, cartId)Admin functions
const admin = getAdmin(orgId)
const products = await admin.listProducts({ page: 1 })
const created = await admin.createProduct(input)
await admin.fulfillOrder(orderId, { trackingNumber: 'TRK123' })
await admin.recordRefund(orderId, 'Customer request') // records a refund already sent; moves no money
const stats = await admin.getDashboardStats()Payment providers
// Build provider with the tenant's stored config
const { provider } = await getTenantPaymentProvider(orgId, 'stripe')
// Verify webhook with tenant's stored secret
const result = await verifyPaymentWebhook(payload, signature, 'stripe', orgId)The provider factory reads from the encrypted integration_config table. There are no environment-level provider defaults: a store with no stored credentials has no provider.
Storage
import { uploadForTenant, tenantStorageDirectory } from '@prood/commerce'
const result = await uploadForTenant(orgId, {
file: buffer,
filename: 'product-image.jpg',
contentType: 'image/jpeg',
directory: tenantStorageDirectory(orgId, 'products', productId),
})
// result.url → CDN URL
// result.key → org/{orgId}/products/{productId}/...Encryption
Integration credentials are encrypted at rest:
import { encryptConfig, decryptConfig } from '@prood/commerce'
const encrypted = encryptConfig({ secretKey: 'sk_live_...' })
// → { secretKey: 'enc:v1:...' }
const decrypted = decryptConfig(encrypted)
// → { secretKey: 'sk_live_...' }Uses AES-256-GCM with INTEGRATION_ENCRYPTION_KEY. The key is required.
Tenant scoping
All functions accept orgId as the first parameter and call withTenant() internally:
export async function getProducts(orgId: string, params: SearchParams) {
return withTenant(orgId, () => adapter.getProducts(params))
}Configuration
Read via getCommerceConfig():
| Setting | Env var | Default |
|---|---|---|
| Database | DATABASE_URL | None. Required |
Module structure
Drag to pan · Scroll to zoom