Skip to content

OAuth Credential Management

This document describes the OAuth credential system architecture, how credentials are resolved, and patterns for adding new platforms.


Overview

TendSocial uses a database-first credential resolution strategy. The system checks the database (Admin > Integrations) before falling back to environment variables.

┌─────────────────────────────────────────────────────────┐
│                   Credential Resolution                  │
├─────────────────────────────────────────────────────────┤
│  1. Check Database (IntegrationConfig table)            │
│     ↓ Found & Enabled?                                  │
│  2. YES → Use DB credentials                            │
│     NO  → Fall back to environment variables            │
└─────────────────────────────────────────────────────────┘

Key Files

FilePurpose
src/utils/credentials.tsCentralized credential provider with caching
src/adapters/adapter-factory.tsFactory for creating adapters with DB credentials
src/config/constants/social.constants.tsOAuth URLs and scopes per platform
src/services/social/social-config.service.tsgetOAuthConfig() function
src/adapters/platforms/*.tsPlatform-specific adapters

Architecture

1. Credential Provider (credentials.ts)

typescript
// Async - checks database FIRST, then env vars
const creds = await getCredentials('linkedin');

// Sync - env vars only (for constructors)
const creds = getCredentialsSync('linkedin');

// Clear cache when admin updates credentials
clearCredentialCache('linkedin');

Features:

  • 5-minute credential cache to reduce DB calls
  • Automatic fallback to environment variables
  • Platform-to-env-var mapping built-in

2. Adapter Factory (adapter-factory.ts)

Use this instead of getPlatformAdapter() when you need database credentials:

typescript
import { getAdapterWithCredentials } from '../adapters/adapter-factory.js';

// Gets adapter with database credentials injected
const adapter = await getAdapterWithCredentials(Platform.LINKEDIN_PAGE);

3. OAuth Config Service (social-config.service.ts)

The getOAuthConfig() function resolves credentials and builds the OAuth configuration:

typescript
const config = await getOAuthConfig('LINKEDIN');
// Returns: { clientId, clientSecret, redirectUri, authUrl, tokenUrl, scopes }

4. Platform Adapters

All adapters accept optional config and have setCredentials():

typescript
// Constructor with config
const adapter = new LinkedInPagesAdapter({ 
  clientId: 'xxx', 
  clientSecret: 'yyy' 
});

// Or inject later
adapter.setCredentials('xxx', 'yyy');

Adding a New Platform

Step 1: Add OAuth Constants

Edit src/config/constants/social.constants.ts:

typescript
export const OAUTH_URLS = {
  // ... existing platforms
  NEWPLATFORM: {
    AUTH: 'https://newplatform.com/oauth/authorize',
    TOKEN: 'https://newplatform.com/oauth/token',
    REVOKE: 'https://newplatform.com/oauth/revoke',
  },
};

export const PLATFORM_SCOPES = {
  // ... existing platforms
  NEWPLATFORM: ['read', 'write', 'publish'],
};

Step 2: Add Environment Variable Mapping

Edit src/utils/credentials.ts:

typescript
const PLATFORM_ENV_VARS: Record<string, {...}> = {
  // ... existing platforms
  newplatform: {
    clientId: 'NEWPLATFORM_CLIENT_ID',
    clientSecret: 'NEWPLATFORM_CLIENT_SECRET',
  },
};

Step 3: Create Platform Adapter

Create src/adapters/platforms/newplatform.ts:

typescript
export class NewPlatformAdapter implements PlatformAdapter {
  private clientId: string;
  private clientSecret: string;

  constructor(config?: { clientId?: string; clientSecret?: string }) {
    this.clientId = config?.clientId || process.env.NEWPLATFORM_CLIENT_ID || '';
    this.clientSecret = config?.clientSecret || process.env.NEWPLATFORM_CLIENT_SECRET || '';
  }

  setCredentials(clientId: string, clientSecret: string): void {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
  }

  // ... implement PlatformAdapter interface
}

Step 4: Update Adapter Factory Mapping

Edit src/adapters/adapter-factory.ts:

typescript
function getPlatformCredentialKey(platform: Platform): string {
  const mapping: Record<Platform, string> = {
    // ... existing platforms
    NEWPLATFORM: 'newplatform',
  };
  return mapping[platform] || platform.toLowerCase();
}

Step 5: Register in Platform Registry

Edit src/adapters/platforms.ts or where adapters are registered:

typescript
import { NewPlatformAdapter } from './adapters/newplatform.js';
registerAdapter(Platform.NEWPLATFORM, new NewPlatformAdapter());

Step 6: Add Database Schema (if needed)

The IntegrationConfig table stores per-platform credentials. Ensure the platform enum includes your new platform.


Environment Variables vs Database

SourceWhen UsedManaged By
Environment VariablesFallback onlyDevOps (.env files, secrets)
DatabasePrimaryAdmin UI (Admin > Integrations)

Best Practice: Store credentials in database via Admin UI. Environment variables are for:

  • Local development without database
  • CI/CD testing
  • Fallback when database unavailable

Caching

Credentials are cached for 5 minutes to avoid repeated database queries:

typescript
// Automatic in getCredentials()
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

// Clear cache when admin saves new credentials
clearCredentialCache('linkedin');  // Clear one platform
clearCredentialCache();            // Clear all

Note: The admin integrations endpoint should call clearCredentialCache() when credentials are updated.


Testing

With Mock Credentials

typescript
const adapter = new LinkedInPagesAdapter({
  clientId: 'test-id',
  clientSecret: 'test-secret',
});

Mocking getCredentials()

typescript
vi.mock('../utils/credentials.js', () => ({
  getCredentials: vi.fn().mockResolvedValue({
    clientId: 'mock-id',
    clientSecret: 'mock-secret',
  }),
}));

Troubleshooting

"Missing environment variable" Error

Cause: Database credentials not configured or disabled.

Fix:

  1. Go to Admin > Integrations
  2. Configure the platform credentials
  3. Enable the integration

Credentials Not Updating

Cause: Cache not cleared after admin update.

Fix: Ensure the admin endpoint calls clearCredentialCache(platform).

Wrong Credentials Used

Cause: Adapter created before DB credentials fetched.

Fix: Use getAdapterWithCredentials() instead of getPlatformAdapter().

TendSocial Documentation