Skip to main content

Overview

Postiz supports 28+ social media platforms through a provider-based integration system. Each provider extends a base SocialAbstract 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

1

Register OAuth App

Create an OAuth application on the platform and get client ID/secret.
2

Add environment variables

Add CLIENT_ID and CLIENT_SECRET to your .env file.
3

Test OAuth flow

Try connecting the integration from the Postiz frontend.
4

Test posting

Create a test post and verify it publishes correctly.
5

Test error handling

Simulate errors (expired token, rate limit) and verify recovery.

Next Steps

OAuth Flow

Deep dive into OAuth implementation

Testing Integrations

Learn testing strategies

Build docs developers (and LLMs) love