@venturekit-pro/social
v0.0.0-dev.20260609102541
Published
Social-platform publishing adapters + catalog for VentureKit applications
Downloads
1,033
Maintainers
Readme
@venturekit-pro/social
Generic social-platform publishing adapters + tenant catalog for VentureKit applications.
Domain-neutral. This package knows about platforms, posts, media, OAuth credentials, validation, and publishing. It does not know about cadence, fan-out, AI generation, angles, blog↔social linkage, or any other calling-app concept. Apps compose those on top.
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). All factory-constructed, stateless, served from 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 + author-ref pairing.- Typed errors —
SocialAuthError/SocialRateLimitError/SocialValidationError/SocialPublishErrorso callers can build retry policies that match the failure mode. - Pre-publish validation —
validatePost()enforces per-platform body / hashtag / media constraints and surfaces both errors and warnings.
Wire-up
// In your project's vk.config.ts:
import { getSocialMigrationsDir } from '@venturekit-pro/social';
export default defineVenture({
extraMigrationsDirs: [getSocialMigrationsDir()],
});vk migrate runs vk_social_0001_init.sql alongside your project's own
migrations.
Building the registry
import {
createAdapterRegistry,
createLinkedInAdapter,
createXAdapter,
createFacebookAdapter,
createInstagramAdapter,
} from '@venturekit-pro/social';
export const socialRegistry = createAdapterRegistry([
createLinkedInAdapter(),
createXAdapter(),
createFacebookAdapter(),
createInstagramAdapter(),
]);Validate + publish
import { socialRegistry } from './social';
const adapter = socialRegistry.resolve('linkedin');
const validation = adapter.validate(post);
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
The package never reads or writes credentials — the caller fetches the
{accessToken, refreshToken?, expiresAt?, authorRef} pair from its own
secret store (the CMS uses KMS-encrypted-in-DB jsonb on tenants.secrets)
and passes it per call.
authorRef carries the platform-side target id:
| Platform | authorRef example |
|------------|--------------------------------------|
| LinkedIn | urn:li:organization:12345 |
| X | the user/page handle |
| Facebook | page_12345 |
| Instagram | ig_user_17841412345 |
Media staging
Image upload is caller-side: the package treats SocialMedia.url as
an opaque platform reference. For LinkedIn / X the caller stages the
upload via the platform's pre-upload endpoint and passes the resulting
URN / media_id_string as url. For Facebook / Instagram the package
calls Meta's /photos or /media endpoint with a public image URL
directly.
The CMS handles staging in its own pre-publish workflow.
Extending with a custom platform
Add a row to social_platforms (any tenant-admin-owned migration of yours)
and register a custom adapter:
const mastodonAdapter: SocialAdapter = {
key: 'mastodon',
displayName: 'Mastodon',
constraints: { /* … */ },
validate(post) { /* … */ },
async publish(post, credentials) { /* … */ },
};
const registry = createAdapterRegistry([
createLinkedInAdapter(),
// …
mastodonAdapter,
]);