Next.js
Publish to social platforms from Next.js App Router route handlers and server actions with Social SDK, with raw-body webhook verification.
Social SDK runs in Next.js server code: route handlers, server actions, and server components. It must never reach a client component, because backend credentials and connected-account references are server secrets.
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-sdkCreate the client in server-only code
Keep one module that constructs the client and mark it server-only so a client import fails at build time.
import "server-only";
import { createSocial } from "@opencoredev/social-sdk";
import { x } from "@opencoredev/social-sdk/x";
export const social = createSocial({
backend: x({
auth: { userId: process.env.X_USER_ID!, accessToken: process.env.X_ACCESS_TOKEN! },
}),
});
Publish from a route handler
import { connectedAccountRef } from "@opencoredev/social-sdk";
import { social } from "@/lib/social";
export async function POST(request: Request) {
const session = await requireSession(request); // your auth
const { text } = await request.json();
const account = connectedAccountRef({
backend: "default",
platform: "x",
accountId: await resolveAuthorizedAccount(session), // your tenant check
});
const result = await social.posts.publish({
targets: [{ account }],
content: { text },
});
return Response.json({
status: result.status,
outcomes: result.outcomes.map((o) => ({ state: o.state })),
});
}
The account ID must come from your own tenant-authorization lookup, never from the request body. See tenant authorization.
Publish from a server action
"use server";
import { connectedAccountRef } from "@opencoredev/social-sdk";
import { social } from "@/lib/social";
export async function publishPost(formData: FormData) {
const session = await requireSession();
const account = connectedAccountRef({
backend: "default",
platform: "x",
accountId: await resolveAuthorizedAccount(session),
});
return social.posts.publish({
targets: [{ account }],
content: { text: String(formData.get("text")) },
idempotencyKey: String(formData.get("draftId")),
});
}
Pass an idempotency key derived from your own draft identity so a double-submitted form cannot publish twice.
Mount the example handler
The repository ships a complete Request/Response handler with account selection, publishing, reconciliation, and webhook intake. Mount it under a catch-all route:
import { createNextRoute } from "@opencoredev/social-sdk-example/frameworks/next-route";
export const { GET, POST } = createNextRoute();
See framework recipes for the handler’s behavior and configuration.
Webhooks
Verify provider events against the raw request bytes before parsing. Next.js route handlers give you the untouched body through request.arrayBuffer() and the headers as a Headers object:
import { SocialError } from "@opencoredev/social-sdk";
import { verifyZernioWebhook } from "@opencoredev/social-sdk/server";
export async function POST(request: Request) {
const body = new Uint8Array(await request.arrayBuffer());
try {
await verifyZernioWebhook({
secret: process.env.ZERNIO_WEBHOOK_SECRET!,
headers: request.headers,
body,
});
} catch (error) {
if (error instanceof SocialError && error.code === "unauthorized") {
return new Response("unauthorized", { status: 401 });
}
throw error;
}
// decode, resolve tenants, and accept into your durable inbox
return new Response("accepted", { status: 202 });
}
Follow the events guide for decoding, tenant quarantine, and the durable inbox worker.
Notes
- Deploy targets must run Node 22.12+ or Node 24. The SDK is ESM-only.
- The edge runtime is not a tested target; keep these routes on the Node runtime.
- Scheduling needs a durable job runner such as Trigger.dev or Inngest; a serverless route alone cannot guarantee a future publish.