Skip to main content

Tier Configuration Strategy: Presets vs Custom

Analysis of how to manage organization-level configurations across different subscription tiers.

Last Updated: February 2025 Decision: Tiered Presets + Enterprise Overrides


The Question

How should we handle configuration across different customer tiers?

Options:

  1. Per-organization custom configs (maximum flexibility)
  2. Fixed tier presets (operational simplicity)
  3. Hybrid: Presets + selective overrides (recommended)

Decision: Tiered Presets + Enterprise Overrides ✅

Standard tiers (Free, Basic, Pro, Enterprise, Enterprise Plus): Fixed presets defined in code Custom tier (White-label/On-premise): Fully configurable in database Enterprise override: Allow specific overrides for compliance/migration cases


Why This Approach Wins

1. What Enterprise Customers Actually Want

Reality check:

80% of enterprise customers want:

  • ✅ "Better than standard" guarantees
  • ✅ Predictable, documented behavior
  • ✅ SLAs and priority support
  • ❌ NOT: "Configure your own retry backoff schedule"

20% of enterprise customers need:

  • ✅ Compliance-driven requirements (2-year retention)
  • ✅ Migration from competitors (match exact behavior)
  • ✅ Unusual use cases (white-label, on-premise)

Solution: Start with presets, add overrides for the 20%.


2. Operational Simplicity

With Presets:

// Simple to reason about
const config = TIER_PRESETS[org.tier];
// Done. Predictable behavior.

With Per-Org Configs:

// Complex to reason about
const config = await getCustomOrgConfig(org.id);
// Is it reasonable?
// Will it crash our system? (customer set 100 retries)
// Do we need to validate? (yes, complex rules)
// How do we handle migrations? (schema changes affect 1000s of configs)

3. Predictable System Capacity

Scenario: System is slow

With Presets:

  • "Check: How many Enterprise Plus orgs are active?"
  • "Ah, 50 orgs × 15 retries × 10-day duration = X jobs in queue"
  • Predictable capacity planning

With Custom Configs:

  • "Some org set retry to 30 days with 100 attempts"
  • "Another org set 1-minute intervals"
  • "Queue is exploding but we don't know why"
  • Unpredictable, hard to debug

4. Clear Upgrade Path (Sales Perspective)

Sales pitch writes itself: Clear value at each tier, obvious upgrade reasons.


Tier Presets (Defined in Code)

// config/tier-presets.ts
export const TIER_PRESETS = {
FREE: {
// Retry policy
retryDuration: 24 * 60 * 60 * 1000, // 24 hours
retryAttempts: 5,
retryIntervals: [30, 60, 120, 240, 480], // minutes

// Storage & retention
messageRetention: 7 * 24 * 60 * 60 * 1000, // 7 days
contentRetention: 7 * 24 * 60 * 60 * 1000, // 7 days

// Limits
emailsPerDay: 1000,
emailsPerMonth: 10000,
domainsLimit: 1,
contactsLimit: 1000,

// Features
webhookRetries: 3,
dedicatedIP: false,
customDomain: false,
prioritySupport: false,
slaGuarantee: null,
},

BASIC: {
retryDuration: 48 * 60 * 60 * 1000, // 48 hours
retryAttempts: 6,
retryIntervals: [15, 30, 60, 120, 240, 480],

messageRetention: 30 * 24 * 60 * 60 * 1000, // 30 days
contentRetention: 14 * 24 * 60 * 60 * 1000, // 14 days

emailsPerDay: 10000,
emailsPerMonth: 100000,
domainsLimit: 3,
contactsLimit: 10000,

webhookRetries: 5,
dedicatedIP: false,
customDomain: false,
prioritySupport: false,
slaGuarantee: null,
},

PRO: {
retryDuration: 72 * 60 * 60 * 1000, // 72 hours (industry standard)
retryAttempts: 8,
retryIntervals: [5, 15, 30, 60, 120, 240, 480, 960],

messageRetention: 90 * 24 * 60 * 60 * 1000, // 90 days
contentRetention: 30 * 24 * 60 * 60 * 1000, // 30 days

emailsPerDay: 100000,
emailsPerMonth: 1000000,
domainsLimit: 10,
contactsLimit: 100000,

webhookRetries: 10,
dedicatedIP: false,
customDomain: false,
prioritySupport: true,
slaGuarantee: null,
},

ENTERPRISE: {
retryDuration: 120 * 60 * 60 * 1000, // 5 days
retryAttempts: 12,
retryIntervals: [5, 15, 30, 60, 120, 240, 480, 960, 1440, 2880, 4320, 5760],

messageRetention: 365 * 24 * 60 * 60 * 1000, // 1 year
contentRetention: 90 * 24 * 60 * 60 * 1000, // 90 days

emailsPerDay: 1000000,
emailsPerMonth: 10000000,
domainsLimit: 50,
contactsLimit: 1000000,

webhookRetries: 15,
dedicatedIP: true,
customDomain: true,
prioritySupport: true,
slaGuarantee: 99.9,
},

ENTERPRISE_PLUS: {
retryDuration: 240 * 60 * 60 * 1000, // 10 days
retryAttempts: 15,
retryIntervals: [5, 10, 15, 30, 60, 120, 240, 480, 960, 1440, 2880, 4320, 5760, 8640, 11520],

messageRetention: 730 * 24 * 60 * 60 * 1000, // 2 years
contentRetention: 180 * 24 * 60 * 60 * 1000, // 180 days

emailsPerDay: 10000000,
emailsPerMonth: 100000000,
domainsLimit: 100,
contactsLimit: 10000000,

webhookRetries: 20,
dedicatedIP: true,
customDomain: true,
prioritySupport: true,
slaGuarantee: 99.99,
},
};

Database Schema

// prisma/schema.prisma

model Organization {
id String @id @default(cuid())
name String
tier Tier @default(FREE)

// Optional overrides (nullable = use tier defaults)
// Only populated for special cases
customRetryDuration Int? // milliseconds
customRetryAttempts Int?
customRetryIntervals Json? // [5, 10, 15, ...]
customMessageRetention Int? // milliseconds
customContentRetention Int? // milliseconds
customEmailsPerDay Int?
customEmailsPerMonth Int?
customDomainsLimit Int?
customContactsLimit Int?
customWebhookRetries Int?

// Enterprise-specific fields
dedicatedIP String?
customDomain String?
slaGuarantee Float? // e.g., 99.99
customContract Boolean @default(false)

// Governance & audit
configOverrideReason String? // "HIPAA compliance requires 2-year retention"
configApprovedBy String? // Account manager email
configApprovedAt DateTime?
configLastModified DateTime?
configModifiedBy String?

// ... other fields (users, domains, messages, etc.)

@@index([tier])
}

enum Tier {
FREE
BASIC
PRO
ENTERPRISE
ENTERPRISE_PLUS
CUSTOM // For white-label/on-premise
}

Configuration Resolution Logic

// lib/config/org-config.ts

interface OrgConfig {
// Retry policy
retryDuration: number;
retryAttempts: number;
retryIntervals: number[];

// Retention
messageRetention: number;
contentRetention: number;

// Limits
emailsPerDay: number;
emailsPerMonth: number;
domainsLimit: number;
contactsLimit: number;

// Features
webhookRetries: number;
dedicatedIP: boolean;
customDomain: boolean;
prioritySupport: boolean;
slaGuarantee: number | null;
}

export function getOrgConfig(org: Organization): OrgConfig {
// Start with tier defaults
const tierDefaults = TIER_PRESETS[org.tier];

// For CUSTOM tier or if overrides exist, merge
return {
// Retry policy
retryDuration: org.customRetryDuration ?? tierDefaults.retryDuration,
retryAttempts: org.customRetryAttempts ?? tierDefaults.retryAttempts,
retryIntervals: org.customRetryIntervals
? (org.customRetryIntervals as number[])
: tierDefaults.retryIntervals,

// Retention
messageRetention: org.customMessageRetention ?? tierDefaults.messageRetention,
contentRetention: org.customContentRetention ?? tierDefaults.contentRetention,

// Limits
emailsPerDay: org.customEmailsPerDay ?? tierDefaults.emailsPerDay,
emailsPerMonth: org.customEmailsPerMonth ?? tierDefaults.emailsPerMonth,
domainsLimit: org.customDomainsLimit ?? tierDefaults.domainsLimit,
contactsLimit: org.customContactsLimit ?? tierDefaults.contactsLimit,

// Features
webhookRetries: org.customWebhookRetries ?? tierDefaults.webhookRetries,
dedicatedIP: org.dedicatedIP ? true : tierDefaults.dedicatedIP,
customDomain: org.customDomain ? true : tierDefaults.customDomain,
prioritySupport: tierDefaults.prioritySupport,
slaGuarantee: org.slaGuarantee ?? tierDefaults.slaGuarantee,
};
}

// Validation for custom configs
export function validateCustomConfig(config: Partial<OrgConfig>): string[] {
const errors: string[] = [];

// Retry duration: Max 30 days
if (config.retryDuration && config.retryDuration > 30 * 24 * 60 * 60 * 1000) {
errors.push('Retry duration cannot exceed 30 days');
}

// Retry attempts: Max 50
if (config.retryAttempts && config.retryAttempts > 50) {
errors.push('Retry attempts cannot exceed 50');
}

// Retry intervals: Min 5 minutes
if (config.retryIntervals && config.retryIntervals.some(i => i < 5)) {
errors.push('Retry intervals must be at least 5 minutes');
}

// Message retention: Max 5 years
if (config.messageRetention && config.messageRetention > 5 * 365 * 24 * 60 * 60 * 1000) {
errors.push('Message retention cannot exceed 5 years');
}

return errors;
}

When to Use Overrides

Scenario 1: Compliance Requirements

// Large healthcare provider needs 2-year retention for HIPAA
await prisma.organization.update({
where: { id: 'hospital-org-123' },
data: {
tier: 'ENTERPRISE_PLUS',
customMessageRetention: 2 * 365 * 24 * 60 * 60 * 1000, // 2 years
customContract: true,
configOverrideReason: 'HIPAA compliance requires 2-year message retention',
configApprovedBy: 'sales@postchi.io',
configApprovedAt: new Date(),
}
});

Scenario 2: Migration from Competitor

// Customer migrating from SendGrid wants exact same retry behavior
await prisma.organization.update({
where: { id: 'sendgrid-migration-customer' },
data: {
tier: 'ENTERPRISE',
customRetryIntervals: [1, 10, 60, 240], // Match SendGrid pattern
customContract: true,
configOverrideReason: 'Migration from SendGrid - matching existing retry behavior',
configApprovedBy: 'migration-team@postchi.io',
configApprovedAt: new Date(),
}
});

Scenario 3: Beta Testing New Features

// Internal test org for new retry algorithm
await prisma.organization.update({
where: { id: 'internal-test-org' },
data: {
tier: 'PRO',
customRetryIntervals: [5, 10, 20, 40, 80], // New ML-optimized algorithm
configOverrideReason: 'Testing new ML-based retry algorithm',
configApprovedBy: 'engineering@postchi.io',
configApprovedAt: new Date(),
}
});

UI/UX Implications

Standard Tiers (Free → Enterprise Plus)

Settings page shows:

┌─────────────────────────────────────────┐
│ Retry Policy │
├─────────────────────────────────────────┤
│ Plan: Enterprise Standard │
│ │
│ ✓ Retry Duration: 5 days │
│ ✓ Max Attempts: 12 retries │
│ ✓ Message Retention: 365 days │
│ ✓ Dedicated IP: Included │
│ │
│ [Upgrade to Enterprise Plus] → │
│ Get 10-day retry & 2-year retention │
└─────────────────────────────────────────┘

Read-only with upgrade CTA.


Custom Tier

Settings page shows:

┌─────────────────────────────────────────┐
│ Retry Policy: Custom Configuration │
├─────────────────────────────────────────┤
│ Retry Duration: [10] days ✏️ │
│ Max Attempts: [15] retries ✏️ │
│ Retry Schedule: [Edit Intervals] ✏️ │
│ Message Retention: [730] days ✏️ │
│ │
│ ⚠️ Changes require approval from │
│ your account manager │
│ │
│ [Request Configuration Change] │
└─────────────────────────────────────────┘

Editable with approval workflow.


Enterprise with Override (Special Case)

Settings page shows:

┌─────────────────────────────────────────┐
│ Retry Policy: Enterprise (Custom) │
├─────────────────────────────────────────┤
│ Retry Duration: 5 days │
│ (tier default) │
│ │
│ Max Attempts: 12 retries │
│ (tier default) │
│ │
│ Message Retention: 730 days ⚡ │
│ (custom: HIPAA compliance) │
│ │
│ ℹ️ Contact your account manager to │
│ modify custom configurations │
└─────────────────────────────────────────┘

Shows which settings are customized with reason.


Governance & Approval Workflow

Configuration Change Request Process

Admin Panel for Overrides

// Internal admin panel: /admin/organizations/:id/config

<ConfigOverrideForm>
<Alert type="warning">
This organization is on {org.tier} tier.
Any overrides must have a documented business reason.
</Alert>

<FormField>
<Label>Message Retention Override</Label>
<Input
type="number"
defaultValue={org.customMessageRetention}
placeholder={`Default: ${TIER_PRESETS[org.tier].messageRetention} ms`}
/>
<Help>
Leave blank to use tier default.
Max: 5 years (1825 days)
</Help>
</FormField>

<FormField required>
<Label>Business Justification</Label>
<Textarea
placeholder="e.g., HIPAA compliance requires 2-year retention"
/>
</FormField>

<FormField required>
<Label>Approved By</Label>
<Input
type="email"
placeholder="account-manager@postchi.io"
/>
</FormField>

<Button type="submit">
Apply Configuration Override
</Button>
</ConfigOverrideForm>

Monitoring & Alerts

System Health Checks

// Cron job: Monitor unusual configurations
async function auditCustomConfigs() {
// Find orgs with high retry counts
const highRetryOrgs = await prisma.organization.findMany({
where: {
customRetryAttempts: { gte: 20 }
}
});

if (highRetryOrgs.length > 0) {
await sendAlert({
type: 'unusual_config',
message: `${highRetryOrgs.length} orgs have 20+ retry attempts configured`,
orgs: highRetryOrgs.map(o => o.id)
});
}

// Find orgs with very long retry durations
const longDurationOrgs = await prisma.organization.findMany({
where: {
customRetryDuration: { gte: 14 * 24 * 60 * 60 * 1000 } // 14+ days
}
});

if (longDurationOrgs.length > 0) {
await sendAlert({
type: 'unusual_config',
message: `${longDurationOrgs.length} orgs have 14+ day retry duration`,
orgs: longDurationOrgs.map(o => o.id)
});
}
}

Audit Trail

// Log all config changes
model ConfigAuditLog {
id String @id @default(cuid())
organizationId String
changedBy String // User email or "system"
changedAt DateTime @default(now())

field String // e.g., "customRetryDuration"
oldValue Json?
newValue Json?
reason String?

approvedBy String?
approvedAt DateTime?

organization Organization @relation(fields: [organizationId], references: [id])

@@index([organizationId, changedAt])
}

Implementation Priority

Phase 1: MVP (Launch)

  • ✅ Implement 3 tiers: FREE, PRO, ENTERPRISE
  • ✅ Hard-coded presets in code
  • ✅ No overrides, no customization
  • ✅ Simple tier selection in settings

Phase 2: Enterprise Features (Month 2-3)

  • ✅ Add ENTERPRISE_PLUS tier
  • ✅ Add override fields to database
  • ✅ Internal admin panel for overrides
  • ✅ Approval workflow
  • ❌ No customer-facing customization UI

Phase 3: Self-Service Configuration (Month 4-6)

  • ✅ Add CUSTOM tier
  • ✅ Customer-facing configuration UI
  • ✅ Validation and safety checks
  • ✅ Usage analytics per config
  • ✅ Configuration templates

Phase 4: Advanced Features (Month 7+)

  • ✅ ML-based retry optimization
  • ✅ ISP-specific retry recommendations
  • ✅ A/B testing different retry strategies
  • ✅ Configuration marketplace (templates)

Conclusion

Decision: Tiered Presets + Enterprise Overrides

Why it works:

  • ✅ Operational simplicity (predictable behavior)
  • ✅ Clear upgrade path (sales enablement)
  • ✅ Flexibility for edge cases (enterprise appeal)
  • ✅ System health (capacity planning)
  • ✅ Governance (audit trail, approvals)

Alternative approaches considered:

  • ❌ Per-org custom configs: Too complex, unpredictable
  • ❌ Fixed presets only: Not flexible enough for enterprise

Implementation approach:

  1. Start with simple tier presets (MVP)
  2. Add override capability for enterprise (selective)
  3. Full customization only for highest tier (controlled)