Skip to content

Overview

TendSocial's admin configuration system allows Super Admins to manage all platform settings, API keys, feature flags, and email templates without requiring code changes or deployments.

All configuration is stored in the database with 60-second LRU caching for performance, and critical keys can automatically sync to Google Secret Manager for disaster recovery.


Architecture

Database Tables

SystemSettings

Singleton table for app-wide configuration.

sql
CREATE TABLE "SystemSettings" (
  id                         TEXT PRIMARY KEY DEFAULT 'default',
  -- App Configuration
  appName                    TEXT DEFAULT 'TendSocial',
  appUrl                     TEXT DEFAULT 'https://app.tendsocial.com',
  supportEmail               TEXT DEFAULT 'support@tendsocial.com',
  alertEmail                 TEXT DEFAULT 'alerts@tendsocial.com',
  -- Signup & Onboarding
  allowSignups               BOOLEAN DEFAULT true,
  requireEmailVerification   BOOLEAN DEFAULT true,
  defaultTrialDays           INTEGER DEFAULT 14,
  -- Default Limits
  maxTeamMembersDefault      INTEGER DEFAULT 10,
  maxSocialAccountsDefault   INTEGER DEFAULT 5,
  -- AI Budget Controls
  dailyAIBudgetUSD           DECIMAL DEFAULT 50.00,
  monthlyAIBudgetUSD         DECIMAL DEFAULT 1000.00,
  updatedAt                  TIMESTAMP
);

EmailTemplate

Customizable email templates with variable substitution.

sql
CREATE TABLE "EmailTemplate" (
  id          TEXT PRIMARY KEY,
  name        TEXT UNIQUE,
  description TEXT,
  subject     TEXT,           -- Can include {{variables}}
  htmlBody    TEXT,           -- HTML template
  textBody    TEXT,           -- Plain text fallback
  variables   JSON,           -- ["varName1", "varName2"]
  category    TEXT,           -- 'team', 'billing', 'alerts', 'notifications'
  isActive    BOOLEAN DEFAULT true,
  createdAt   TIMESTAMP,
  updatedAt   TIMESTAMP
);

BillingConfig

Payment provider configuration.

sql
CREATE TABLE "BillingConfig" (
  id             TEXT PRIMARY KEY DEFAULT 'default',
  provider       TEXT DEFAULT 'lemonsqueezy',
  apiKey         TEXT,
  storeId        TEXT,
  webhookSecret  TEXT,
  testMode       BOOLEAN DEFAULT false,
  variantMapping JSON,      -- {"variant_123": "starter"}
  currency       TEXT DEFAULT 'USD',
  taxEnabled     BOOLEAN DEFAULT false,
  updatedAt      TIMESTAMP
);

FeatureFlag

Toggle features globally or per segment/user.

Note: The Feature Flag system has been significantly enhanced. See Platform Console Documentation for the complete schema including Segments, Entitlements, and Packages.

Key tables:

  • FeatureFlag - Flag definitions and enabled state
  • Segment - User/company groups for targeting
  • FeatureFlagSegment - Join table linking flags to segments
  • FeatureFlagUserOverride - Per-user overrides
  • EntitlementFeature - Commercial feature definitions
  • PlanEntitlement - Feature-to-plan mappings
  • PackageConfig - Subscription tier definitions

IntegrationConfig

Third-party service credentials.

sql
CREATE TABLE "IntegrationConfig" (
  id          TEXT PRIMARY KEY,
  service     TEXT UNIQUE,
  displayName TEXT,
  isEnabled   BOOLEAN DEFAULT false,
  apiKey      TEXT,
  apiSecret   TEXT,
  webhookUrl  TEXT,
  settings    JSON,
  rateLimit   INTEGER,
  updatedAt   TIMESTAMP
);

Configuration Service

ConfigService

Central service for all configuration management with caching.

File: src/services/config.service.ts

typescript
import { LRUCache } from 'lru-cache';
import prisma from '../infra/prisma.js';

const configCache = new LRUCache<string, any>({
    max: 100,
    ttl: 60 * 1000, // 60 seconds
});

export class ConfigService {
    // System Settings
    static async getSystemSettings();
    static async updateSystemSettings(updates);

    // Billing Config
    static async getBillingConfig();
    static async updateBillingConfig(updates);

    // Email Templates
    static async getEmailTemplate(name);
    static async getEmailTemplates();
    static async updateEmailTemplate(id, updates);

    // Feature Flags
    static async isFeatureEnabled(featureName, companyId?, tier?);
    static async getFeatureFlags();
    static async updateFeatureFlag(id, updates);

    // Integration Configs
    static async getIntegrationConfig(service);
    static async getIntegrationConfigs();
    static async updateIntegrationConfig(id, updates);

    // Cache Management
    static clearCache();
}

Usage Examples

Get System Settings

typescript
import { configService } from './services/config.service.js';

const settings = await configService.getSystemSettings();
console.log(settings.appName); // 'TendSocial'
console.log(settings.defaultTrialDays); // 14

Check Feature Flag

typescript
const hasMultiAgent = await configService.isFeatureEnabled(
    'multi_agent_system',
    companyId,
    'professional'
);

if (hasMultiAgent) {
    // Enable multi-agent features
}

Get Integration API Key

typescript
const resendConfig = await configService.getIntegrationConfig('resend');
if (resendConfig?.isEnabled && resendConfig.apiKey) {
    const resend = new Resend(resendConfig.apiKey);
}

Google Secret Manager Sync

Overview

When API keys are updated via Admin UI, they automatically sync to Google Secret Manager (GSM). This provides:

  • Disaster Recovery: If database is compromised, keys still available
  • Version Control: GSM keeps all versions, easy rollback
  • Audit Trail: Track all key changes
  • Single Source of Truth: Update in UI, GSM automatically updated

Configuration

Enable GSM sync via environment variables:

bash
GSM_SYNC_ENABLED=true
GCP_PROJECT_ID=your-project-id

Secret Mappings

Integration Configs:

ServiceDatabase FieldGSM Secret Name
resendapiKeyRESEND_API_KEY
tavilyapiKeyTAVILY_API_KEY
anthropicapiKeyANTHROPIC_API_KEY
openaiapiKeyOPENAI_API_KEY
googleapiKeyGEMINI_API_KEY
newsapiapiKeyNEWS_API_KEY
slackwebhookUrlSLACK_WEBHOOK_URL

Billing Config:

FieldGSM Secret Name
apiKeyLEMONSQUEEZY_API_KEY
storeIdLEMONSQUEEZY_STORE_ID
webhookSecretLEMONSQUEEZY_WEBHOOK_SECRET

Sync Behavior

  1. Update Triggered: Admin updates key via UI
  2. Database Updated: Primary source of truth
  3. GSM Sync: New version created in GSM
  4. Failure Handling: If GSM sync fails, database update still succeeds (logged)

Rollback Process

If a bad key is pushed, rollback in GSM:

bash
# List all versions
gcloud secrets versions list RESEND_API_KEY

# Enable previous version
gcloud secrets versions enable 2 --secret=RESEND_API_KEY

# Disable current version
gcloud secrets versions disable 3 --secret=RESEND_API_KEY

GSMSyncService

File: src/services/gsmSync.service.ts

typescript
export class GSMSyncService {
    // Check if sync is enabled
    isEnabled(): boolean;

    // Sync integration API key
    async syncIntegrationConfig(service, apiKey);

    // Sync billing config
    async syncBillingConfig(updates);

    // Test connection
    async testConnection();
}

Feature Flags

Pre-configured Flags

Flag NameDescriptionDefault Tiers
ai_content_generationAI-powered content generationstarter, professional, agency, enterprise
multi_agent_systemMulti-agent research systemprofessional, agency, enterprise
trend_monitoringAutomatic trend detectionprofessional, agency, enterprise
auto_pilotAutomatic content schedulingagency, enterprise
performance_predictionsPost performance predictionsprofessional, agency, enterprise
bio_managementUnified bio managementstarter, professional, agency, enterprise
team_collaborationTeam features & workflowsprofessional, agency, enterprise
advanced_analyticsDetailed analytics & reportsprofessional, agency, enterprise
white_labelWhite label customizationenterprise
api_accessExternal API accessagency, enterprise

Usage in Code

typescript
// Middleware to check feature access
async function requireFeature(featureName: string) {
    return async (request, reply) => {
        const { companyId } = request.user;
        const company = await prisma.company.findUnique({
            where: { id: companyId },
            select: { subscriptionTier: true },
        });

        const hasAccess = await configService.isFeatureEnabled(
            featureName,
            companyId,
            company.subscriptionTier
        );

        if (!hasAccess) {
            return reply.status(403).send({
                error: 'Feature not available on your plan'
            });
        }
    };
}

// Use in routes
app.post('/api/ai/multi-agent',
    { preValidation: [verifyJwt, requireFeature('multi_agent_system')] },
    async (request, reply) => {
        // Multi-agent logic
    }
);

Email Templates

Pre-configured Templates

  1. team_invite - Team invitation emails
  2. cost_alert - AI cost limit alerts
  3. new_message - Inbox message notifications

Variables

Templates support syntax:

team_invite:

  • - Person sending invitation
  • - Company name
  • - Invitation acceptance URL
  • - Application name

cost_alert:

  • - Type of alert
  • - Company name
  • - Current cost in cents
  • - Cost limit in cents

new_message:

  • - Message sender
  • - Social platform
  • - Message preview text
  • - Link to inbox

Usage

typescript
import { emailService } from './services/email.service.js';

// Send using database template
await emailService.sendFromTemplate('team_invite', userEmail, {
    inviterName: 'John Doe',
    companyName: 'Acme Corp',
    inviteUrl: 'https://app.tendsocial.com/invite/abc123',
    appName: settings.appName,
});

// Or use predefined method
await emailService.sendTeamInvite({
    toEmail: userEmail,
    inviterName: 'John Doe',
    companyName: 'Acme Corp',
    inviteToken: 'abc123',
});

Configuration Priority

Services check configuration in this order:

  1. Database (Primary)
  2. Environment Variables (Fallback)
  3. Defaults (Hardcoded)

Example:

typescript
// Email service checks:
const apiKey =
    config?.apiKey ||              // 1. Database
    process.env.RESEND_API_KEY ||  // 2. Environment
    null;                          // 3. Error if missing

This allows:

  • Hot configuration updates (no restart)
  • Graceful fallback if database unavailable
  • Easy local development (use env vars)

Best Practices

Security

  1. API Keys: Never expose in frontend, always fetch server-side
  2. Encryption: Consider encrypting sensitive fields in database
  3. Access Control: Only Super Admins can modify config
  4. Audit Logging: Log all configuration changes

Performance

  1. Caching: 60s TTL prevents repeated database queries
  2. Selective Invalidation: Only clear specific cache keys on update
  3. Lazy Loading: Load config only when needed

Deployment

  1. Bootstrap: Seed default config on first deployment
  2. Migration: Run migration to create tables
  3. Validation: Test GSM sync before enabling in production
  4. Monitoring: Alert on config changes

Troubleshooting

Config Not Updating

  1. Clear Cache:

    bash
    curl -X POST /api/admin/config/cache/clear
  2. Check Database:

    sql
    SELECT * FROM "SystemSettings" WHERE id = 'default';
  3. Verify Permissions: Ensure user has isSuperAdmin: true

GSM Sync Failing

  1. Check Status:

    bash
    curl /api/admin/config/gsm/status
  2. Verify Environment:

    bash
    echo $GSM_SYNC_ENABLED  # Should be 'true'
    echo $GCP_PROJECT_ID    # Should be set
  3. Check Permissions: Service account needs roles/secretmanager.admin

  4. Manual Sync: Update key again via Admin UI to retry


See Also

TendSocial Documentation