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