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);
}

On this page