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 |
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().