NestJS
Provide the Social SDK client through NestJS dependency injection, publish from services, and verify webhooks with a raw-body controller.
In NestJS, Social SDK becomes an injectable provider: one client instance constructed from configuration, shared across services, with a controller that keeps webhook bodies raw for verification.
Install
npm install @opencoredev/social-sdkpnpm add @opencoredev/social-sdkyarn add @opencoredev/social-sdkbun add @opencoredev/social-sdknub add @opencoredev/social-sdkaube add @opencoredev/social-sdkProvide the client
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { createSocial } from "@opencoredev/social-sdk";
import { x } from "@opencoredev/social-sdk/x";
export const SOCIAL = Symbol("SOCIAL");
@Module({
imports: [ConfigModule],
providers: [
{
provide: SOCIAL,
inject: [ConfigService],
useFactory: (config: ConfigService) =>
createSocial({
backend: x({
auth: {
userId: config.getOrThrow("X_USER_ID"),
accessToken: config.getOrThrow("X_ACCESS_TOKEN"),
},
}),
}),
},
],
exports: [SOCIAL],
})
export class SocialModule {}
Publish from a service
import { Inject, Injectable } from "@nestjs/common";
import { connectedAccountRef } from "@opencoredev/social-sdk";
import { SOCIAL } from "../social/social.module";
@Injectable()
export class PublishingService {
constructor(@Inject(SOCIAL) private readonly social: ReturnType<typeof createSocial>) {}
async publish(tenantId: string, draftId: string, text: string) {
const accountId = await this.resolveAuthorizedAccount(tenantId); // your lookup
const result = await this.social.posts.publish({
targets: [
{
account: connectedAccountRef({
backend: "default",
platform: "x",
accountId,
}),
},
],
content: { text },
idempotencyKey: draftId,
});
await this.saveOutcomes(tenantId, draftId, result.outcomes); // your storage
return result;
}
}
Guards and your tenant lookup own authorization. The SDK checks capabilities and account-to-backend binding, not your application’s user model.
Webhooks with a raw body
Enable Nest’s raw-body support and read req.rawBody in the events controller. req.rawBody is a Buffer, which the verifier accepts as a Uint8Array. Convert Node’s header record to a web Headers object:
const app = await NestFactory.create(AppModule, { rawBody: true });
import { Controller, HttpCode, Post, Req, UnauthorizedException } from "@nestjs/common";
import type { RawBodyRequest } from "@nestjs/common";
import type { Request } from "express";
import { SocialError } from "@opencoredev/social-sdk";
import { verifyZernioWebhook } from "@opencoredev/social-sdk/server";
@Controller("events")
export class EventsController {
@Post()
@HttpCode(202)
async receive(@Req() req: RawBodyRequest<Request>) {
try {
await verifyZernioWebhook({
secret: process.env.ZERNIO_WEBHOOK_SECRET!,
headers: new Headers(req.headers as Record<string, string>),
body: req.rawBody ?? new Uint8Array(),
});
} catch (error) {
if (error instanceof SocialError && error.code === "unauthorized") {
throw new UnauthorizedException();
}
throw error;
}
// decode, resolve tenants, accept into your durable inbox
return "accepted";
}
}
Nest fills req.rawBody only for bodies its built-in JSON and URL-encoded parsers handle, so providers must send Content-Type: application/json.
The events guide covers decoding, tenant quarantine, and the durable inbox worker.
Notes
- Run on Node 22.12+ or Node 24; the SDK is ESM-only, so use a NestJS setup that emits or runs ESM.
- One provider instance per process keeps per-backend concurrency limits meaningful.
- Put scheduled publishing in a durable job runner like Trigger.dev or Inngest rather than
@nestjs/scheduletimers that vanish on redeploys, unless a missed run is acceptable.