Building a Gateway Provider
This guide walks through creating a new payment gateway provider package. The same pattern applies to shipping carriers — substitute GatewayAdapter with ShippingAdapter and adjust the hub/category accordingly.
1. Scaffold the package
Create a new workspace package:
packages/gateway-myprovider/
├── package.json
├── tsconfig.json
└── src/
└── modules/
└── gateway_myprovider/
├── index.ts
├── integration.ts
├── acl.ts
├── setup.ts
├── di.ts
├── lib/
│ ├── adapter.ts
│ ├── webhook-handler.ts
│ ├── status-map.ts
│ └── health.ts
├── widgets/
│ └── payments/
│ └── client.tsx
├── workers/
│ └── webhook-processor.ts
└── i18n/
└── en.ts
Add the package to apps/saasframe/src/modules.ts to enable it.
2. Define the integration manifest
Declare metadata in integration.ts:
import type { IntegrationDefinition } from '@saasframe/shared/modules/integrations/types'
export const integration: IntegrationDefinition = {
id: 'gateway_myprovider',
title: 'My Provider',
description: 'Accept payments via My Provider.',
category: 'payment',
hub: 'payment_gateways',
providerKey: 'myprovider',
icon: 'myprovider',
package: '@saasframe/gateway-myprovider',
version: '1.0.0',
author: 'Your Name',
company: 'My Provider, Inc.',
credentials: {
fields: [
{ key: 'apiKey', label: 'API Key', type: 'secret', required: true },
{ key: 'webhookSecret', label: 'Webhook Secret', type: 'secret', required: true },
],
},
healthCheck: { service: 'myProviderHealthCheck' },
}
export const integrations: IntegrationDefinition[] = [integration]
export const bundles = []
This registers the provider in the Integration Marketplace with a credential form and health check.
3. Implement the adapter
Create lib/adapter.ts:
import type { GatewayAdapter, CreateSessionInput, CreateSessionResult } from '@saasframe/shared/modules/payment_gateways/types'
import { mapMyProviderStatus } from './status-map'
export const myProviderAdapter: GatewayAdapter = {
providerKey: 'myprovider',
async createSession(input: CreateSessionInput): Promise<CreateSessionResult> {
const client = createClient(input.credentials)
const session = await client.createPayment({
amount: input.amount,
currency: input.currencyCode,
})
return {
sessionId: session.id,
redirectUrl: session.checkoutUrl,
status: mapMyProviderStatus(session.status),
}
},
async capture(input) {
const client = createClient(input.credentials)
const result = await client.capture(input.sessionId, input.amount)
return { status: mapMyProviderStatus(result.status), capturedAmount: result.amount }
},
async refund(input) {
const client = createClient(input.credentials)
const result = await client.refund(input.sessionId, input.amount, input.reason)
return { refundId: result.id, status: mapMyProviderStatus(result.status), refundedAmount: result.amount }
},
async cancel(input) {
const client = createClient(input.credentials)
await client.cancel(input.sessionId)
return { status: 'cancelled' }
},
async getStatus(input) {
const client = createClient(input.credentials)
const payment = await client.getPayment(input.sessionId)
return {
status: mapMyProviderStatus(payment.status),
amount: payment.amount,
amountReceived: payment.captured,
currencyCode: payment.currency,
}
},
async verifyWebhook(input) {
const secret = input.credentials.webhookSecret as string
// Verify signature using provider SDK
const event = verifySignature(input.rawBody, input.headers, secret)
return {
eventType: event.type,
eventId: event.id,
data: event.data,
idempotencyKey: event.id,
timestamp: new Date(event.created),
}
},
mapStatus: mapMyProviderStatus,
}
4. Build the status map
Create lib/status-map.ts:
import type { UnifiedPaymentStatus } from '@saasframe/shared/modules/payment_gateways/types'
const STATUS_MAP: Record<string, UnifiedPaymentStatus> = {
'created': 'pending',
'awaiting_payment': 'pending',
'authorized': 'authorized',
'paid': 'captured',
'refunded': 'refunded',
'cancelled': 'cancelled',
'failed': 'failed',
'expired': 'expired',
}
export function mapMyProviderStatus(providerStatus: string): UnifiedPaymentStatus {
return STATUS_MAP[providerStatus] ?? 'unknown'
}
5. Register at runtime in di.ts
import type { AppContainer } from '@saasframe/shared/lib/di/container'
import { registerGatewayAdapter, registerWebhookHandler } from '@saasframe/shared/modules/payment_gateways/types'
import { myProviderAdapter } from './lib/adapter'
import { verifyMyProviderWebhook } from './lib/webhook-handler'
export function register(container: AppContainer) {
registerGatewayAdapter(myProviderAdapter)
registerWebhookHandler('myprovider', verifyMyProviderWebhook, {
queue: 'myprovider-webhook',
})
}
Keep setup.ts for tenant initialization concerns such as defaultRoleFeatures, seeded configuration, example data, and provider-owned env-backed preconfiguration that persists credentials or defaults. Do not rely on setup.ts for runtime adapter registration, because the gateway registry is in-memory and must be populated during app boot.
Recommended provider pattern:
- read env vars in a provider-local helper such as
lib/preset.ts - apply them from
setup.tsto persist credentials/defaults after fresh install - expose a provider CLI command such as
configure-from-envso operators can rerun the preset later - keep the preset logic inside the provider package instead of adding provider-specific code to core
6. Add the webhook worker
Create workers/webhook-processor.ts:
import type { Job } from '@saasframe/queue'
import type { WorkerContext, WorkerMeta } from '@saasframe/shared/modules/registry'
export const metadata: WorkerMeta = {
queue: 'myprovider-webhook',
id: 'myprovider-webhook-processor',
concurrency: 5,
}
export default async function handler(job: Job, ctx: WorkerContext) {
const { event, scope, transactionId } = job.data
// Update transaction status, emit domain events, log activity
}
7. Add health check
Create lib/health.ts and register in di.ts:
export function createMyProviderHealthCheck(deps) {
return {
async check(credentials: Record<string, unknown>) {
const client = createClient(credentials)
await client.ping()
return { healthy: true }
},
}
}
8. Configure webhooks
Tell the payment provider to send events to:
{YOUR_APP_URL}/api/payment_gateways/webhook/myprovider
The dynamic [provider] route automatically dispatches to your registered handler.
For local development, expose your app publicly with a tunnel such as ngrok:
ngrok http 3000
Then configure the provider to send webhooks to:
https://YOUR-NGROK-SUBDOMAIN.ngrok-free.app/api/payment_gateways/webhook/myprovider
9. Enable and test
- Add the package to
apps/saasframe/src/modules.ts. - Run
yarn generateandyarn build:packages. - Go to Settings > Integrations, find your provider, and enter credentials.
- Use the Payment Gateway Demo page or create a payment method with your provider key.
Provider-owned payment widgets
If your provider supports inline payment UI, register it from widgets/payments/client.tsx. That file is auto-discovered by the generator and imported through payments.client.generated.ts during client bootstrap.
Use this file to register renderer widgets declaratively by providerKey + rendererKey. Consumer modules such as checkout should only resolve them through the shared payment renderer registry; they must not import provider UI directly.
When a host page exposes payment-widget injection spots, keep provider-specific embellishments in those widgets too. Validation and submit hooks should flow through UMES behavior spots instead of custom provider branches.
Shipping carrier providers
For shipping carriers, follow the same pattern with these differences:
| Payment | Shipping |
|---|---|
GatewayAdapter | ShippingAdapter |
hub: 'payment_gateways' | hub: 'shipping_carriers' |
category: 'payment' | category: 'shipping' |
registerGatewayAdapter() | registerShippingAdapter() |
Methods: createSession, capture, refund, cancel | Methods: calculateRates, createShipment, getTracking, cancelShipment |
Webhook route: /api/payment_gateways/webhook/{provider} | Webhook route: /api/shipping_carriers/webhook/{provider} |
Stripe reference
The @saasframe/gateway-stripe package is the reference implementation. It demonstrates:
- Versioned adapters (supporting multiple Stripe API versions)
- Webhook signature verification using the Stripe SDK
- Status mapping for Payment Intents and Charges
- Health check via
stripe.accounts.retrieve() - Admin UI widget injection for capture mode configuration
See the Stripe configuration guide for setup instructions.