@venturekit-pro/social
@venturekit-pro/social provides generic social-platform publishing adapters and a tenant catalog. It is domain-neutral: it knows about platforms, posts, media, OAuth credentials, validation and publishing. It does not know about cadence, fan-out, AI generation, or any other calling-app concept — apps compose those on top.
What it gives you
Section titled “What it gives you”- One canonical adapter contract —
SocialAdapterwithvalidate()+publish()and optionalverify()+parseWebhook(). - Four v1 adapters — LinkedIn (UGC Posts), X (v2 Tweets), Facebook (Pages Graph), Instagram (Content Publishing). Factory-constructed, stateless, one instance per process.
social_platformscatalog — a database row per supported platform (seeded by the package’s migration) plus a thin per-tenanttenant_social_platformstable for enable/disable and author-ref pairing.- Typed errors —
SocialAuthError,SocialRateLimitError,SocialValidationError,SocialPublishError, so retry policies can match the failure mode. - Pre-publish validation —
validatePost()enforces per-platform body / hashtag / media constraints and surfaces both errors and warnings. - First-comment threads — set
SocialPost.firstCommentand any adapter whoseconstraints.supportsFirstCommentistrue(LinkedIn, X, Instagram) posts it as a companion comment / reply after the main post. Best-effort: a failed comment never unwinds the already-live post.
Installation
Section titled “Installation”npm install @venturekit-pro/social# optional — WebP → JPEG conversion for LinkedIn uploadsnpm install sharpThe package ships vk_social_0001_init.sql and declares it via "vk": { "migrations": "src/migrations" }, so vk migrate auto-discovers and applies it for any project that depends on the package. getSocialMigrationsDir() returns the directory for a custom runner.
1. Build the registry
Section titled “1. Build the registry”import { createAdapterRegistry, createLinkedInAdapter, createXAdapter, createFacebookAdapter, createInstagramAdapter,} from '@venturekit-pro/social';
export const socialRegistry = createAdapterRegistry([ createLinkedInAdapter(), // each factory accepts { baseUrl?, constraints? } overrides createXAdapter(), createFacebookAdapter(), createInstagramAdapter(),]);
socialRegistry.keys(); // ['linkedin', 'x', 'facebook', 'instagram']socialRegistry.find('mastodon'); // undefinedsocialRegistry.resolve('linkedin'); // throws when unknownAdapters can also be imported individually from @venturekit-pro/social/adapters/linkedin, /adapters/x, /adapters/facebook and /adapters/instagram (each also exports its *_CONSTRAINTS).
2. Validate + publish
Section titled “2. Validate + publish”import { SocialAuthError, SocialRateLimitError, SocialValidationError, SocialPublishError, type SocialPost, type OAuthCredentials,} from '@venturekit-pro/social';import { socialRegistry } from '../lib/social';
const adapter = socialRegistry.resolve('linkedin');
const post: SocialPost = { body: 'Launch day! …', media: [{ url: presignedImageUrl, mimeType: 'image/png', altText: 'Dashboard screenshot' }], link: 'https://app.example.com/blog/launch', firstComment: 'Read the full story → …', idempotencyKey: `blog-${blogPost.id}-linkedin`,};
const validation = adapter.validate(post); // { ok, issues: [{ code, message, severity, … }] }if (!validation.ok) throw new Error(validation.issues.map((i) => i.message).join('; '));
try { const result = await adapter.publish(post, credentials); // result.externalRef, result.publishedUrl?, result.publishedAt} catch (err) { if (err instanceof SocialRateLimitError) { // back off using err.retryAfterSeconds } else if (err instanceof SocialAuthError) { // refresh credentials and retry } else if (err instanceof SocialValidationError) { // platform rejected the body — show err.message to the editor } else if (err instanceof SocialPublishError) { // transient or unexpected — retry with backoff }}OAuth credentials
Section titled “OAuth credentials”The package never reads or writes credentials. The caller fetches the OAuthCredentials — { accessToken, refreshToken?, expiresAt?, authorRef } — from its own secret store and passes them per call. authorRef carries the platform-side target:
| Platform | authorRef example |
|---|---|
urn:li:organization:12345 | |
| X | the user / page handle |
page_12345 | |
ig_user_17841412345 |
Media staging
Section titled “Media staging”Callers pass a plain image URL (presigned S3, CDN, …) in SocialMedia.url; the adapters do the rest:
- Facebook — downloads the image in-process and uploads the bytes as multipart form data to
/<page-id>/photos. Works even for private/presigned URLs Meta’s servers cannot reach (local MinIO, private S3). - LinkedIn — a
urn:li:digitalmediaAsset:…URL is used directly; otherwise the two-step staging (registerUpload→ PUT bytes → asset URN) runs automatically. Unsupported formats (e.g. WebP) are converted to JPEG first — requires the optionalsharppeer. - Instagram — passes the URL to Meta’s
/mediaendpoint (Meta fetches it server-side), so it must be publicly reachable. - X — the caller stages the upload via X’s chunk-upload endpoint and passes the resulting
media_id_stringasurl.
The upload format is decided from the payload’s magic number (sniffImageMime), not from SocialMedia.mimeType, which is only a fallback. Downloads are capped at the platform’s maxMediaSizeBytes and time out after 30 s; oversized payloads raise SocialValidationError (permanent), transport failures SocialPublishError. Only http(s) URLs are accepted — and because the worker fetches them, apps that let end users supply media URLs must block internal targets (link-local, RFC 1918, cloud metadata endpoints) themselves.
3. Platform catalog
Section titled “3. Platform catalog”import { listSocialPlatforms, getSocialPlatformByKey, patchSocialPlatform } from '@venturekit-pro/social';import { query } from '@venturekit/data';
await listSocialPlatforms(query); // SocialPlatformRow[] — key, displayName, status, meta, …await getSocialPlatformByKey(query, 'linkedin'); // row or nullawait patchSocialPlatform(query, 'x', { status: 'disabled' }); // 'available' | 'deprecated' | 'disabled'Extending with a custom platform
Section titled “Extending with a custom platform”Add a row to social_platforms (createSocialPlatform(query, { key, displayName, … }) or a migration of yours) and register a custom adapter:
import type { SocialAdapter } from '@venturekit-pro/social';
const mastodonAdapter: SocialAdapter = { key: 'mastodon', displayName: 'Mastodon', constraints: { maxBodyChars: 500, minBodyChars: 1, minHashtags: 0, maxHashtags: 10, allowedMimeTypes: ['image/png', 'image/jpeg'], maxMediaCount: 4, allowedAspectRatios: [], maxMediaSizeBytes: 8_000_000, supportsFirstComment: true }, validate(post) { /* … */ }, async publish(post, credentials) { /* … */ },};
const registry = createAdapterRegistry([createLinkedInAdapter(), mastodonAdapter]);Related
Section titled “Related”- API Reference — every export
@venturekit-pro/audit— record publish attempts withcorrelationId= your post id@venturekit/storage— presigned URLs for the media the adapters download