@happyvertical/smrt-social
Social media account management with OAuth and post scheduling across YouTube, Threads, X, Bluesky, and Facebook.
v0.29.34OAuthPost SchedulingMulti-PlatformOptional Tenancy
Overview
smrt-social manages social media connections and publishing across multiple platforms. It handles OAuth credential storage (CSRF + PKCE), post creation and scheduling, and per-post analytics tracking. The platform enum is hardcoded — extending it requires code changes.
Installation
bash
npm install @happyvertical/smrt-socialQuick Start
typescript
import { SocialAccount, SocialPost, OAuthState } from '@happyvertical/smrt-social';
// Connect a social account. Prefer storing credentials in smrt-secrets
// via setCredentials() rather than the deprecated plaintext token columns.
const account = new SocialAccount({
name: 'Bentley News YouTube',
platform: 'youtube',
platformUsername: 'Bentley News',
tokenExpiresAt: new Date('2026-06-01'),
defaultHashtags: ['news', 'local'],
linkBehavior: 'description',
publishMode: 'dry_run', // safety default; gate 'public' behind publicPublishingAllowed
});
await account.save();
// Store the OAuth payload in smrt-secrets (sets credentialSecretId on the account)
await account.setCredentials({
accessToken: '...',
refreshToken: '...',
});
// 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' | 'facebook'
platformUserId: string | null
platformUsername: string | null
platformUrl: string | null
accessToken: string | null // deprecated: prefer credentialSecretId/setCredentials()
refreshToken: string | null // deprecated
credentialSecretId: string | null // smrt-secrets reference for the credential payload
accessTokenSecretName: string | null
refreshTokenSecretName: string | null
tokenExpiresAt: Date | null
isActive: boolean
status: 'connected' | 'disconnected' | 'expired' | 'missing_permissions' | 'error'
defaultHashtags: string[]
scopes: string[]
linkBehavior: 'description' | 'inline' | 'attachment' | 'reply' | 'none'
publishMode: 'dry_run' | 'stage_remote' | 'private_or_scheduled' | 'public'
publicPublishingAllowed: boolean // latch required before publishMode 'public' takes effect
errorMessage: string | null
get isReady(): boolean // active + connected + credentials + no missing perms + not expired + publish latch
get isTokenExpired(): boolean // 5-minute buffer before expiry
get hasCredentials(): boolean // any usable credential reference present
get needsAttention(): boolean
get effectivePublishMode(): PublishMode // downgrades 'public' to 'dry_run' until the latch is set
async setCredentials(credentials: Record<string, unknown>, options?): Promise<void>
}SocialPost
typescript
class SocialPost extends SmrtObject {
socialAccountId: string | null
postType: 'text' | 'link' | 'image' | 'video'
title: string | null
description: string
hashtags: string[]
linkUrl: string | null
platformPostId: string | null // id assigned by the platform after publish
scheduledAt: Date | null
publishedAt: Date | null
status: 'draft' | 'pending_approval' | 'approved' | 'scheduled'
| 'publishing' | 'dry_run' | 'staged' | 'published'
| 'failed' | 'cancelled'
errorMessage: string | null
analytics: PostAnalytics // object: views, impressions, likes, comments, shares, clicks, raw, lastUpdated
analyticsLastSyncedAt: Date | null
get isEditable(): boolean // true when draft, pending_approval, or failed
get isScheduled(): boolean
get isPublished(): boolean
get fullText(): string // description + formatted hashtags
}OAuthState (STI)
typescript
class OAuthState extends SmrtObject {
platform: 'youtube' | 'threads' | 'x' | 'bluesky' | 'facebook'
state: string // CSRF token
codeVerifier: string | null // PKCE code verifier
redirectUri: string
scopes: string[]
expiresAt: Date // defaults to now + 10 minutes (10-minute TTL)
get isExpired(): boolean
get isValid(): boolean // not expired and state is set
verifyState(callbackState: string): boolean
static generateState(): string
static generateCodeVerifier(): string
static async generateCodeChallenge(verifier: string): Promise<string> // S256
}Best Practices
DOs
- Check
account.isReadybefore attempting to publish - Use
OAuthState.generateState()for CSRF protection - Use PKCE (
generateCodeVerifier/generateCodeChallenge) for OAuth flows - Implement a job runner to trigger publishing at
scheduledAttime - 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 write OAuth tokens into the deprecated plaintext
accessToken/refreshTokencolumns -- usesetCredentials()(smrt-secrets) - Don't enable
publishMode: 'public'without also settingpublicPublishingAllowed(the latch gates real public publishing) - Don't extend the platform enum without code changes (hardcoded list)