Portal Community

Multi-Tenant Architecture

Each tenant (TenantID) can configure their own email provider independently:

Tenant 1 (TenantID=1): Uses Gmail OAuth2 (email-primary)
Tenant 2 (TenantID=2): Uses AWS SES (email-primary)
Tenant 3 (TenantID=3): Uses custom SMTP server (email-primary)

Database Structure

SELECT * FROM AIExt_Credentials
WHERE Name='email-primary' AND TenantID IN (1,2,3)
ORDER BY TenantID;

Results:
TenantID=1: provider=gmail,   accessToken=...
TenantID=2: provider=ses,     region=us-east-1
TenantID=3: provider=smtp,    host=mail.company.local

Setup: Tenant 1 with Gmail

INSERT INTO AIExt_Credentials (
  Name, DisplayMetadata, EncryptedData, TenantID, CredentialTypeID, Enabled
) VALUES (
  'email-primary',  -- All tenants use same name
  '{
    "provider": "gmail",
    "fromEmail": "user1@gmail.com",
    "fromName": "Tenant 1 Company"
  }',
  0x...,  -- Encrypted OAuth tokens
  1,      -- TenantID=1
  3,      -- OAuth2Record
  1
);

Setup: Tenant 2 with SES

INSERT INTO AIExt_Credentials (
  Name, DisplayMetadata, EncryptedData, TenantID, CredentialTypeID, Enabled
) VALUES (
  'email-primary',  -- Same name!
  '{
    "provider": "ses",
    "region": "us-east-1",
    "fromEmail": "notifications@tenant2.com",
    "fromName": "Tenant 2 Notifications"
  }',
  0x...,  -- Encrypted AWS keys
  2,      -- TenantID=2
  2,      -- ApiKeyRecord
  1
);

Code: Same API, Different Providers

// The same code works for all tenants!
private readonly IEmailService _emailService;
private readonly ITenantSettingsService _tenantSettingsService;

public async Task SendWelcomeEmail(int tenantID, string email, CancellationToken ct)
{
    // Resolve credential name from tenant settings (defaults to "email-primary")
    var credentialName = await _tenantSettingsService.GetAsync<string>(
        tenantID,
        "EmailCredentialName",
        defaultValue: "email-primary",
        ct);

    var result = await _emailService.SendAsync(
        tenantID: tenantID,
        to: email,
        subject: "Welcome",
        html: "<p>Welcome to our platform!</p>",
        credentialName: credentialName,  // Resolved per tenant from settings
        ct: ct);

    return result.Success;
}

// Tenant 1 call: Uses Gmail
SendWelcomeEmail(tenantID: 1, email: "user@example.com", CancellationToken.None);
// ↓ Reads TenantSettings: Key='EmailCredentialName', TenantID=1 → "email-primary"
// ↓ Looks up: AIExt_Credentials WHERE Name='email-primary' AND TenantID=1
// ↓ Finds Gmail credential
// ↓ Sends via Gmail

// Tenant 2 call: Uses SES
SendWelcomeEmail(tenantID: 2, email: "user@example.com", CancellationToken.None);
// ↓ Reads TenantSettings: Key='EmailCredentialName', TenantID=2 → "email-primary"
// ↓ Looks up: AIExt_Credentials WHERE Name='email-primary' AND TenantID=2
// ↓ Finds SES credential
// ↓ Sends via SES

Multi-Credential Setup

Each tenant can have multiple named credentials and configure which one to use:

-- Tenant 1 credentials
INSERT INTO AIExt_Credentials (Name, TenantID, ...)
VALUES ('email-primary', 1, ...);      -- Main email (Gmail)

INSERT INTO AIExt_Credentials (Name, TenantID, ...)
VALUES ('email-alerts', 1, ...);       -- Alerts only (SMTP)

-- Configure which credential to use for welcome emails
INSERT INTO TenantSettings (TenantID, Key, Value)
VALUES (1, 'EmailCredentialName', 'email-primary');

-- In code, TenantSettings controls which credential is used:
public async Task SendWelcomeEmail(int tenantID, string email, CancellationToken ct)
{
    var credentialName = await _tenantSettingsService.GetAsync<string>(
        tenantID, "EmailCredentialName", "email-primary", ct);
    await _emailService.SendAsync(tenantID, email, subject, html, credentialName, ct);
}

public async Task SendAlert(int tenantID, string email, CancellationToken ct)
{
    // Can use a different credential if configured
    var credentialName = await _tenantSettingsService.GetAsync<string>(
        tenantID, "EmailAlertCredentialName", "email-alerts", ct);
    await _emailService.SendAsync(tenantID, email, subject, html, credentialName, ct);
}

Isolation Guarantees

Next Steps