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
| File | Purpose |
|---|---|
src/utils/credentials.ts | Centralized credential provider with caching |
src/adapters/adapter-factory.ts | Factory for creating adapters with DB credentials |
src/config/constants/social.constants.ts | OAuth URLs and scopes per platform |
src/services/social/social-config.service.ts | getOAuthConfig() function |
src/adapters/platforms/*.ts | Platform-specific adapters |
Production Callback Contract
The current frontend OAuth flow calls:
POST /api/company/social-accounts/connect/:platform
GET /api/company/social-accounts/callback/:platformPlatform 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:
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/MASTODONSet 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)
// 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:
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:
const config = await getOAuthConfig('LINKEDIN');
// Returns: { clientId, clientSecret, redirectUri, authUrl, tokenUrl, scopes }4. Platform Adapters
All adapters accept optional config and have setCredentials():
// 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:
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:
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:
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:
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:
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
| Source | When Used | Managed By |
|---|---|---|
| Environment Variables | Fallback only | DevOps (.env files, secrets) |
| Database | Primary | Admin 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:
// 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 allNote: The admin integrations endpoint should call clearCredentialCache() when credentials are updated.
Testing
With Mock Credentials
const adapter = new LinkedInPagesAdapter({
clientId: 'test-id',
clientSecret: 'test-secret',
});Mocking getCredentials()
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:
- Go to Admin > Integrations
- Configure the platform credentials
- 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().