Overview
Postiz supports 28+ social media platforms through a provider-based integration system. Each provider extends a baseSocialAbstract class and implements the SocialProvider interface.
Provider Architecture
All social media providers are located in:libraries/nestjs-libraries/src/integrations/social/
├── social.abstract.ts # Base abstract class
├── social.integrations.interface.ts # Provider interface
└── x.provider.ts # Example: X/Twitter provider
facebook.provider.ts # Example: Facebook provider
linkedin.provider.ts # Example: LinkedIn provider
... (28+ providers)
Creating a New Provider
Step 1: Create Provider File
Create a new file following the naming convention:platform-name.provider.ts
example.provider.ts
import {
AuthTokenDetails,
PostDetails,
PostResponse,
SocialProvider,
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
import { Integration } from '@prisma/client';
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
@Rules('Platform-specific posting rules and limitations')
export class ExampleProvider extends SocialAbstract implements SocialProvider {
identifier = 'example'; // Unique identifier
name = 'Example Platform'; // Display name
// OAuth scopes required
scopes = ['read', 'write', 'publish'];
// Concurrent job limit (platform rate limiting)
override maxConcurrentJob = 1;
// Editor type: 'normal' or 'poll'
editor = 'normal' as const;
// Maximum post length
maxLength() {
return 280; // Characters
}
// Whether this is a one-time token (no refresh)
oneTimeToken = false;
// Required methods (implement below)
async generateAuthUrl() { /* ... */ }
async authenticate(params: any) { /* ... */ }
async refreshToken(refreshToken: string) { /* ... */ }
async post(details: PostDetails, integration: Integration) { /* ... */ }
}
Step 2: Implement OAuth Authentication
Generate Auth URL
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
async generateAuthUrl() {
const state = makeId(6); // Generate random state
const codeVerifier = makeId(30); // For PKCE if needed
const url = `https://example.com/oauth/authorize?` +
`client_id=${process.env.EXAMPLE_CLIENT_ID}&` +
`redirect_uri=${encodeURIComponent(`${process.env.FRONTEND_URL}/integrations/social/example`)}&` +
`response_type=code&` +
`scope=${encodeURIComponent(this.scopes.join(' '))}&` +
`state=${state}`;
return {
url,
codeVerifier, // Optional, for PKCE
state,
};
}
Authenticate with Code
async authenticate(params: { code: string; codeVerifier: string }) {
// Exchange authorization code for access token
const response = await fetch('https://example.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: params.code,
redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/example`,
client_id: process.env.EXAMPLE_CLIENT_ID!,
client_secret: process.env.EXAMPLE_CLIENT_SECRET!,
}),
});
const {
access_token: accessToken,
refresh_token: refreshToken,
expires_in: expiresIn,
} = await response.json();
// Fetch user profile
const profileResponse = await fetch('https://api.example.com/v1/me', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const { id, name, username, picture } = await profileResponse.json();
// Verify scopes
this.checkScopes(this.scopes, this.scopes.join(' '));
return {
id,
name,
accessToken,
refreshToken,
expiresIn,
picture,
username,
};
}
Refresh Token
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
const response = await fetch('https://example.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token,
client_id: process.env.EXAMPLE_CLIENT_ID!,
client_secret: process.env.EXAMPLE_CLIENT_SECRET!,
}),
});
const {
access_token: accessToken,
refresh_token: refreshToken,
expires_in: expiresIn,
} = await response.json();
// Re-fetch user info
const profileResponse = await fetch('https://api.example.com/v1/me', {
headers: { Authorization: `Bearer ${accessToken}` },
});
const { id, name, username, picture } = await profileResponse.json();
return {
id,
accessToken,
refreshToken,
expiresIn,
name,
picture,
username,
};
}
Step 3: Implement Post Publishing
import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch';
import { lookup } from 'mime-types';
async post(
details: PostDetails<any>,
integration: Integration
): Promise<PostResponse[]> {
const results: PostResponse[] = [];
for (const item of details.posts) {
try {
// Handle media uploads first
const mediaIds: string[] = [];
if (item.media?.length) {
for (const media of item.media) {
const mediaId = await this.uploadMedia(
integration.token,
media.url
);
mediaIds.push(mediaId);
}
}
// Create the post
const response = await this.fetch(
'https://api.example.com/v1/posts',
{
method: 'POST',
headers: {
Authorization: `Bearer ${integration.token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: item.content,
media_ids: mediaIds,
}),
},
integration.internalId
);
const data = await response.json();
results.push({
postId: data.id,
releaseURL: `https://example.com/posts/${data.id}`,
status: 'success',
});
} catch (error) {
results.push({
postId: '',
releaseURL: '',
status: 'failed',
error: error.message,
});
}
}
return results;
}
private async uploadMedia(token: string, url: string): Promise<string> {
// Fetch the media file
const { buffer } = await readOrFetch(url);
const mimeType = lookup(url) || 'image/jpeg';
// Upload to platform
const uploadResponse = await fetch('https://api.example.com/v1/media', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': mimeType,
},
body: buffer,
});
const { media_id } = await uploadResponse.json();
return media_id;
}
Step 4: Error Handling
override handleErrors(
body: string
): { type: 'refresh-token' | 'bad-body' | 'retry'; value: string } | undefined {
// Check for token expiration
if (body.includes('invalid_token') || body.includes('token_expired')) {
return {
type: 'refresh-token',
value: 'Authentication token has expired',
};
}
// Check for rate limiting
if (body.includes('rate_limit_exceeded')) {
return {
type: 'retry',
value: 'Rate limit exceeded, will retry',
};
}
// Check for bad request
if (body.includes('invalid_request')) {
return {
type: 'bad-body',
value: 'Invalid request parameters',
};
}
return undefined;
}
Step 5: Analytics (Optional)
import { AnalyticsData } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
async analytics(
integration: Integration,
dateRange: { startDate: Date; endDate: Date }
): Promise<AnalyticsData[]> {
const response = await this.fetch(
`https://api.example.com/v1/analytics?` +
`start_date=${dateRange.startDate.toISOString()}&` +
`end_date=${dateRange.endDate.toISOString()}`,
{
headers: {
Authorization: `Bearer ${integration.token}`,
},
},
integration.internalId
);
const data = await response.json();
return data.posts.map((post: any) => ({
postId: post.id,
likes: post.likes_count,
comments: post.comments_count,
shares: post.shares_count,
views: post.impressions,
}));
}
Environment Variables
Add required environment variables:.env
EXAMPLE_CLIENT_ID="your_client_id"
EXAMPLE_CLIENT_SECRET="your_client_secret"
Complete Example
Here’s a simplified complete provider:simple.provider.ts
import {
AuthTokenDetails,
PostDetails,
PostResponse,
SocialProvider,
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { Integration } from '@prisma/client';
export class SimpleProvider extends SocialAbstract implements SocialProvider {
identifier = 'simple';
name = 'Simple Platform';
scopes = ['read', 'write'];
editor = 'normal' as const;
maxLength() {
return 500;
}
async generateAuthUrl() {
const state = makeId(6);
return {
url: `https://simple.com/oauth?client_id=${process.env.SIMPLE_CLIENT_ID}&state=${state}`,
codeVerifier: '',
state,
};
}
async authenticate(params: { code: string }) {
const tokenResponse = await fetch('https://simple.com/oauth/token', {
method: 'POST',
body: JSON.stringify({
code: params.code,
client_id: process.env.SIMPLE_CLIENT_ID,
client_secret: process.env.SIMPLE_CLIENT_SECRET,
}),
});
const { access_token } = await tokenResponse.json();
const userResponse = await fetch('https://api.simple.com/me', {
headers: { Authorization: `Bearer ${access_token}` },
});
const user = await userResponse.json();
return {
id: user.id,
name: user.name,
accessToken: access_token,
refreshToken: '',
expiresIn: 3600,
picture: user.avatar,
username: user.username,
};
}
async post(details: PostDetails, integration: Integration): Promise<PostResponse[]> {
const results = [];
for (const post of details.posts) {
const response = await this.fetch(
'https://api.simple.com/posts',
{
method: 'POST',
headers: { Authorization: `Bearer ${integration.token}` },
body: JSON.stringify({ content: post.content }),
},
integration.internalId
);
const data = await response.json();
results.push({
postId: data.id,
releaseURL: `https://simple.com/posts/${data.id}`,
status: 'success',
});
}
return results;
}
}
Testing Your Provider
Next Steps
OAuth Flow
Deep dive into OAuth implementation
Testing Integrations
Learn testing strategies