Skip to content
Social SDK megaphone markSocial SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

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-sdk
pnpm add @opencoredev/social-sdk
yarn add @opencoredev/social-sdk
bun add @opencoredev/social-sdk
nub add @opencoredev/social-sdk
aube add @opencoredev/social-sdk

Create 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.

Last updated on September 23, 2026