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

Production Callback Contract

The current frontend OAuth flow calls:

text
POST /api/company/social-accounts/connect/:platform
GET  /api/company/social-accounts/callback/:platform

Platform values are Prisma enum names such as FACEBOOK_PAGE, INSTAGRAM_BUSINESS, THREADS, and TWITTER. Provider dashboards must register callback URLs with those exact enum values:

text
https://api.tendsocial.com/api/company/social-accounts/callback/FACEBOOK_PAGE
https://api.tendsocial.com/api/company/social-accounts/callback/INSTAGRAM_BUSINESS
https://api.tendsocial.com/api/company/social-accounts/callback/THREADS
https://api.tendsocial.com/api/company/social-accounts/callback/TWITTER
https://api.tendsocial.com/api/company/social-accounts/callback/LINKEDIN_PROFILE
https://api.tendsocial.com/api/company/social-accounts/callback/LINKEDIN_PAGE
https://api.tendsocial.com/api/company/social-accounts/callback/YOUTUBE
https://api.tendsocial.com/api/company/social-accounts/callback/GOOGLE_BUSINESS
https://api.tendsocial.com/api/company/social-accounts/callback/TIKTOK_BUSINESS
https://api.tendsocial.com/api/company/social-accounts/callback/PINTEREST
https://api.tendsocial.com/api/company/social-accounts/callback/REDDIT
https://api.tendsocial.com/api/company/social-accounts/callback/MASTODON

Set OAUTH_REDIRECT_BASE_URL=https://api.tendsocial.com in production so callback token exchange reconstructs the same redirect URI that was used in the authorize request. OAuth providers require exact redirect URI matching.


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