TanStack Start
Publish to social platforms from TanStack Start server functions and API routes with Social SDK, keeping credentials in server-only code.
TanStack Start runs its server functions and server routes on Node, so Social SDK slots in directly: construct the client in server-only code, call it from server functions, and keep webhook verification on the raw request.
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 code
import { createSocial } from "@opencoredev/social-sdk";
import { bluesky } from "@opencoredev/social-sdk/bluesky";
export const social = createSocial({
backend: bluesky({
auth: {
service: process.env.BLUESKY_SERVICE!,
did: process.env.BLUESKY_DID!,
accessJwt: process.env.BLUESKY_ACCESS_JWT!,
},
}),
});
Publish from a server function
Server functions execute only on the server, so credentials and account references never reach the client bundle:
import { createServerFn } from "@tanstack/react-start";
import { connectedAccountRef } from "@opencoredev/social-sdk";
import { social } from "../server/social";
export const publishPost = createServerFn({ method: "POST" })
.validator((data: { text: string; draftId: string }) => data)
.handler(async ({ data }) => {
const session = await requireSession(); // your auth
const accountId = await resolveAuthorizedAccount(session); // your tenant check
const result = await social.posts.publish({
targets: [
{
account: connectedAccountRef({
backend: "default",
platform: "bluesky",
accountId,
}),
},
],
content: { text: data.text },
idempotencyKey: data.draftId,
});
return {
status: result.status,
outcomes: result.outcomes.map((o) => ({ state: o.state })),
};
});
Resolve the account from your own tenant data inside the handler. Client-supplied account IDs must never select the publishing account.
Webhooks through a server route
Server route handlers receive a web-standard Request, so verification reads the raw bytes and headers directly:
import { createFileRoute } from "@tanstack/react-router";
import { SocialError } from "@opencoredev/social-sdk";
import { verifyZernioWebhook } from "@opencoredev/social-sdk/server";
export const Route = createFileRoute("/api/events")({
server: {
handlers: {
POST: async ({ 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, 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 on a Node 22.12+ or Node 24 target; the SDK is ESM-only. Edge presets are not tested SDK runtimes.
- Keep the client module out of route components; only server functions and server routes may import it.
- Scheduled publishing belongs to a durable job runner such as Trigger.dev or Inngest.