@happyvertical/smrt-social

Social media account management with OAuth, post scheduling, and analytics tracking across YouTube, Threads, X, and Bluesky.

v0.20.44OAuthPost SchedulingMulti-Platform

Overview

smrt-social manages social media connections and publishing across multiple platforms. It handles OAuth credential storage, post creation and scheduling, and per-post analytics tracking with PKCE support for secure OAuth flows.

Installation

bash
npm install @happyvertical/smrt-social

Quick Start

typescript
import { SocialAccount, SocialPost, OAuthState } from '@happyvertical/smrt-social';

// Connect a social account
const account = new SocialAccount({
  name: 'Bentley News YouTube',
  platform: 'youtube',
  platformUsername: 'Bentley News',
  accessToken: 'encrypted-token',
  refreshToken: 'encrypted-refresh',
  tokenExpiresAt: new Date('2026-06-01'),
  defaultHashtags: ['news', 'local'],
  linkBehavior: 'description',
});
await account.save();

// Check readiness before publishing
if (account.isReady) {
  const post = new SocialPost({
    socialAccountId: account.id,
    title: 'Breaking News from Bentley',
    description: 'Latest updates from the town council meeting.',
    hashtags: ['news', 'local', 'bentley'],
    linkUrl: 'https://example.com/article',
    scheduledAt: new Date('2026-03-05T18:00:00Z'),
    status: 'scheduled',
  });
  await post.save();
}

// OAuth flow: create state, redirect user, verify callback
const state = new OAuthState({
  platform: 'youtube',
  state: OAuthState.generateState(),
  codeVerifier: OAuthState.generateCodeVerifier(),
  redirectUri: 'https://app.example.com/oauth/callback',
  scopes: ['youtube.upload', 'youtube.readonly'],
});
await state.save();
// On callback: state.verifyState(callbackState)

Core Models

SocialAccount (STI)

typescript
class SocialAccount extends SmrtObject {
  name: string
  platform: 'youtube' | 'threads' | 'x' | 'bluesky'
  platformUsername?: string
  accessToken?: string
  refreshToken?: string
  tokenExpiresAt?: Date
  status: 'connected' | 'expired' | 'error'
  defaultHashtags?: string[]
  linkBehavior: 'description' | 'reply' | 'none'

  get isReady(): boolean       // active + connected + token + not expired
  get isTokenExpired(): boolean // 5-minute buffer before expiry
}

SocialPost

typescript
class SocialPost extends SmrtObject {
  socialAccountId: string
  title?: string
  description: string
  hashtags?: string[]
  linkUrl?: string
  scheduledAt?: Date
  publishedAt?: Date
  status: 'draft' | 'scheduled' | 'publishing' | 'published' | 'failed'
  analytics: string            // JSON: views, likes, comments, shares, clicks

  get isEditable(): boolean    // true when draft or failed
  get fullText(): string       // description + formatted hashtags
}

OAuthState (STI)

typescript
class OAuthState extends SmrtObject {
  platform: string
  state: string                // CSRF token
  codeVerifier?: string        // PKCE code verifier
  redirectUri: string
  scopes?: string[]
  // 10-minute TTL

  get isValid(): boolean
  verifyState(callback: string): boolean
  static generateState(): string
  static generateCodeVerifier(): string
  static generateCodeChallenge(verifier: string): string  // S256
}

Best Practices

DOs

  • Check account.isReady before attempting to publish
  • Use OAuthState.generateState() for CSRF protection
  • Use PKCE (generateCodeVerifier/generateCodeChallenge) for OAuth flows
  • Implement a job runner to trigger publishing at scheduledAt time
  • Clean up expired OAuthState records (10-minute TTL)

DON'Ts

  • Don't assume auto-publishing (scheduledAt is metadata only -- app must trigger)
  • Don't expect analytics to auto-populate (must sync from platform APIs)
  • Don't store tokens without encryption (currently plaintext -- integrate smrt-secrets)
  • Don't skip the 5-minute token expiry buffer when checking readiness
  • Don't extend the platform enum without code changes (hardcoded list)

Related Modules