ribution.
// src/partners/partner-tenant.model.ts
export interface PartnerTenant {
id: string;
name: string;
tier: 'sandbox' | 'standard' | 'enterprise';
apiKey: string;
webhookSecret: string;
rateLimit: { requestsPerMinute: number; burstSize: number };
features: Record<string, boolean>;
createdAt: Date;
status: 'active' | 'suspended' | 'pending';
}
Step 2: Build the API Gateway with OAuth2 & Tenant Routing
The gateway authenticates partner requests, resolves the tenant, and injects context into downstream services. Client credentials flow is preferred for machine-to-machine partnerships.
// src/gateway/partner-auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { partnerRepository } from '../repositories/partner.repository';
import { createError } from '../utils/errors';
export async function resolvePartnerTenant(
req: Request,
_res: Response,
next: NextFunction
): Promise<void> {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
throw createError(401, 'Missing or invalid Bearer token');
}
const apiKey = authHeader.split(' ')[1];
const tenant = await partnerRepository.findByApiKey(apiKey);
if (!tenant || tenant.status !== 'active') {
throw createError(403, 'Partner tenant inactive or not found');
}
// Attach tenant context to request for downstream middleware
(req as any).partnerTenant = tenant;
next();
}
Step 3: Implement Webhook Event Routing with Idempotency
Partners consume product events via webhooks. Production systems require signature verification, retry policies, and idempotency keys to prevent duplicate processing.
// src/partners/webhook.dispatcher.ts
import crypto from 'crypto';
import { PartnerTenant } from './partner-tenant.model';
export function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expected}`)
);
}
export async function dispatchWebhook(
tenant: PartnerTenant,
event: string,
payload: Record<string, unknown>
): Promise<void> {
const idempotencyKey = crypto.randomUUID();
const body = JSON.stringify({ event, payload, idempotencyKey });
const response = await fetch(tenant.webhookEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Partner-Event': event,
'X-Idempotency-Key': idempotencyKey,
'X-Signature': crypto
.createHmac('sha256', tenant.webhookSecret)
.update(body)
.digest('hex'),
},
body,
});
if (!response.ok) {
// Queue for exponential backoff retry
await retryQueue.enqueue({ tenantId: tenant.id, event, payload, attempt: 1 });
}
}
Step 4: Add Attribution & Analytics Pipeline
Partnerships fail without measurable ROI. An attribution pipeline tags incoming traffic, tracks conversion funnels, and calculates partner-specific LTV/CAC.
// src/analytics/attribution.service.ts
import { db } from '../database';
export async function recordPartnerAttribution(
partnerId: string,
userId: string,
touchpoint: string,
metadata: Record<string, unknown>
): Promise<void> {
await db.query(
`INSERT INTO partner_attribution
(partner_id, user_id, touchpoint, metadata, created_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (partner_id, user_id, touchpoint)
DO UPDATE SET metadata = EXCLUDED.metadata`,
[partnerId, userId, touchpoint, JSON.stringify(metadata)]
);
}
export async function calculatePartnerROI(
partnerId: string,
windowDays: number = 30
): Promise<{ revenue: number; cpl: number; roi: number }> {
const result = await db.query(
`SELECT
SUM(revenue) as revenue,
COUNT(DISTINCT user_id) as conversions,
COALESCE(SUM(cost), 0) as total_cost
FROM partner_attribution pa
JOIN partner_costs pc ON pa.partner_id = pc.partner_id
WHERE pa.partner_id = $1
AND pa.created_at > NOW() - INTERVAL '${windowDays} days'`,
[partnerId]
);
const { revenue, conversions, total_cost } = result.rows[0];
const cpl = conversions > 0 ? total_cost / conversions : 0;
const roi = total_cost > 0 ? (revenue - total_cost) / total_cost : 0;
return { revenue, cpl, roi };
}
Architecture Decisions & Rationale
- Tenant-scoped API keys over shared credentials: Prevents cross-partner data leakage and enables granular revocation without disrupting other integrations.
- Webhook idempotency keys: Eliminates duplicate event processing caused by network retries or partner-side queue redrives.
- Exponential backoff retry queue: Ensures delivery during partner outages without overwhelming downstream services. Dead-letter queues capture unprocessable events for manual review.
- Feature flags per partner: Enables controlled rollouts, A/B testing of partner-specific endpoints, and instant rollback without code deployments.
- Attribution pipeline decoupled from core transactional DB: Prevents query contention. Uses append-only logging with materialized views for ROI calculations.
Pitfall Guide
-
Hardcoding partner endpoints or credentials: Embedding URLs or secrets in application code forces redeployments for every partner change. Store endpoints, secrets, and rate limits in a tenant configuration store with hot-reload capability.
-
Ignoring webhook idempotency: Partners will retry failed deliveries. Without idempotency keys, your system processes duplicates, corrupts analytics, and triggers duplicate billing. Implement idempotency at the ingestion layer, not the business logic layer.
-
Skipping partner sandbox environments: Onboarding partners directly into production breaks your SLA and exposes real user data. Provision isolated sandbox tenants with synthetic data, mirrored API contracts, and independent webhook endpoints.
-
Flat rate limits across all tiers: A single noisy partner can exhaust shared connection pools or database throughput. Implement tiered rate limiting with burst allowances and circuit breakers that degrade gracefully rather than failing hard.
-
Missing attribution pipeline: Without tagged touchpoints and conversion tracking, partnerships become unmeasurable cost centers. Attribute at the point of entry (UTM, referral header, or API key) and track through the full funnel.
-
Over-customizing payloads per partner: Building unique data shapes for each partner creates maintenance debt. Standardize on a core schema, then use a transformation layer to map partner-specific requirements. Version the schema and enforce contract testing.
-
No dead-letter queue for failed webhooks: Silent failures destroy partner trust. Route unretryable events to a DLQ with structured error context, alerting, and manual replay capabilities.
Production Best Practices:
- Use OpenAPI/Swagger contracts for all partner endpoints. Generate client SDKs automatically.
- Implement mutual TLS or signed JWTs for enterprise partners requiring higher assurance.
- Cache partner tenant configuration with short TTLs (30β60s) to balance consistency and latency.
- Run chaos tests on webhook delivery paths quarterly to validate retry and DLQ behavior.
- Instrument partner-specific dashboards: latency, error rates, throughput, and attribution accuracy.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| <10 partners, low volume | Shared API gateway with tenant scoping | Minimizes infra overhead while maintaining isolation | Low infra cost, moderate engineering time |
| 10β50 partners, mixed tiers | Tiered rate limits + sandbox provisioning | Prevents noisy-neighbor issues and accelerates onboarding | Moderate infra cost, high ROI via reduced support |
| Enterprise partners requiring compliance | mTLS + signed JWTs + audit logging | Meets security requirements and enables forensic tracking | Higher infra/compliance cost, reduces legal risk |
| High-frequency event streaming | Async message queue + DLQ + idempotency | Guarantees delivery without blocking sync request paths | Moderate queue infra cost, eliminates duplicate processing |
| Global partner distribution | Edge-cached tenant config + regional webhooks | Reduces latency and improves SLA across time zones | Higher CDN/edge cost, improves partner retention |
Configuration Template
# config/partners-engine.yaml
partner:
gateway:
auth: bearer
tenant_resolution_ttl_sec: 45
rate_limit:
default:
requests_per_minute: 60
burst_size: 10
tiers:
sandbox:
requests_per_minute: 20
burst_size: 5
enterprise:
requests_per_minute: 300
burst_size: 50
webhooks:
retry:
max_attempts: 5
backoff_base_sec: 2
backoff_multiplier: 2
dlq:
enabled: true
retention_days: 30
attribution:
touchpoint_sources: [utm, referral_header, api_key]
roi_window_days: 30
materialized_view_refresh_sec: 300
security:
enterprise:
mtls: enabled
jwt_signing_algo: RS256
audit_logging: enabled
Quick Start Guide
- Initialize the tenant configuration store: Run
npm run db:migrate to apply the partner schema, then seed a sandbox tenant using npm run partner:seed -- --tier=sandbox.
- Start the gateway: Execute
npm run dev to launch the Express server with middleware chain: resolvePartnerTenant β rateLimiter β webhookDispatcher.
- Test webhook delivery: Use
curl -X POST http://localhost:3000/api/v1/partners/webhooks/test -H "Authorization: Bearer <SANDBOX_API_KEY>" to trigger a signed event. Verify signature and idempotency in the logs.
- Validate attribution: Insert a test touchpoint via
npm run analytics:test-attribution -- --partner-id=<ID>, then run npm run analytics:roi -- --partner-id=<ID> --window=30 to confirm ROI calculation.
- Promote to production: Switch
NODE_ENV=production, enable DLQ alerts, and configure your CI/CD pipeline to run OpenAPI contract tests before deployment.