Multi-Tenant Architectures
Patterns for platforms and ISVs (Independent Software Vendors) integrating Sent on behalf of multiple customers.
Architecture Patterns
Pattern 1: Shared Sent Account
Use a single Sent account for all tenants:
Pros:
- Simple setup
- Unified billing
- Centralized management
Cons:
- No tenant isolation
- Shared rate limits
- Single point of failure
// Tag messages with tenant ID
await client.messages.send({
to: [phoneNumber],
template: { id: templateId },
metadata: {
tenantId: tenant.id,
userId: user.id
}
});Pattern 2: Account Per Tenant
Each tenant has their own Sent account:
Pros:
- Complete isolation
- Independent rate limits
- Tenant-specific branding
Cons:
- Complex onboarding
- Multiple API keys to manage
// Use tenant-specific client
const client = new SentDm({
apiKey: tenant.sentApiKey
});
await client.messages.send({...});Pattern 3: Hybrid Approach
Shared account for small tenants, dedicated accounts for enterprise:
async function sendMessage(tenant: Tenant, message: Message) {
if (tenant.plan === 'enterprise') {
// Use tenant's dedicated account
const client = new SentDm({ apiKey: tenant.sentApiKey });
return client.messages.send(message);
} else {
// Use shared platform account
return platformClient.messages.send({
...message,
metadata: { tenantId: tenant.id }
});
}
}Tenant Onboarding
Automated Setup
async function onboardTenant(tenantData: TenantData) {
// 1. Create Sent sub-account (if applicable)
const sentAccount = await createSentSubAccount(tenantData);
// 2. Store credentials securely (v3 API uses only apiKey)
await secretsManager.store(`sent-api-key-${tenantData.id}`, {
apiKey: sentAccount.apiKey
// Note: x-sender-id is legacy v2 API
});
// 3. Configure webhooks for tenant
await configureTenantWebhook(tenantData.id, sentAccount.webhookUrl);
// 4. Set usage limits
await setTenantLimits(tenantData.id, {
messagesPerMonth: tenantData.planLimits.messages
});
}Usage Tracking
Track usage per tenant:
app.post('/webhooks/sent', async (req, res) => {
// Acknowledge immediately
res.sendStatus(200);
const { field, payload } = req.body;
if (field === 'messages' && payload.message_status === 'SENT') {
// Retrieve tenant context from your database using message ID
const message = await db.messages.findBySentId(payload.message_id);
if (message?.tenantId) {
await analytics.track('message_sent', {
tenantId: message.tenantId,
messageId: payload.message_id,
channel: payload.channel,
to: payload.inbound_number,
timestamp: new Date()
});
}
}
});Tenant Isolation
Data Separation
// Middleware to inject tenant context
app.use('/api/:tenantId/messages', async (req, res, next) => {
const tenant = await getTenant(req.params.tenantId);
// Verify tenant has access
if (!await canAccessTenant(req.user, tenant)) {
return res.status(403).send('Access denied');
}
// Attach tenant-specific client
req.sentClient = new SentDm({ apiKey: tenant.sentApiKey });
next();
});Rate Limiting Per Tenant
const tenantRateLimiters = new Map();
function getRateLimiter(tenantId: string) {
if (!tenantRateLimiters.has(tenantId)) {
tenantRateLimiters.set(tenantId, new RateLimiter({
tokensPerInterval: 100,
interval: 'minute'
}));
}
return tenantRateLimiters.get(tenantId);
}