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

Elysia

Publish to social platforms from Elysia routes on Bun or Node using web-standard Request and Response with Social SDK.

Elysia handles web-standard Request and Response objects, so Social SDK needs no adapter. Routes validate input with Elysia’s schema builder, webhook routes can skip body parsing to keep the raw bytes, and the same app runs on Bun or on Node 22.12+ through the Node adapter.

Install

npm install @opencoredev/social-sdk elysia
pnpm add @opencoredev/social-sdk elysia
yarn add @opencoredev/social-sdk elysia
bun add @opencoredev/social-sdk elysia
nub add @opencoredev/social-sdk elysia
aube add @opencoredev/social-sdk elysia

Publish from a route

Create the client once at module scope and validate the request body with t before it reaches the SDK. The idempotencyKey only stops a retried request from posting twice when the client has an idempotencyStore, so give it a durable store that every process shares. The Neon guide includes a Postgres implementation.

import { Elysia, t } from "elysia";
import { createSocial, connectedAccountRef } from "@opencoredev/social-sdk";
import { bluesky } from "@opencoredev/social-sdk/bluesky";
import { idempotencyStore } from "./stores"; // your durable IdempotencyStore

const social = createSocial({
  backend: bluesky({
    auth: {
      service: process.env.BLUESKY_SERVICE!,
      did: process.env.BLUESKY_DID!,
      accessJwt: process.env.BLUESKY_ACCESS_JWT!,
    },
  }),
  idempotencyStore,
});

export const app = new Elysia().post(
  "/publish",
  async ({ request, body }) => {
    const tenant = await requireTenant(request); // your auth
    const result = await social.posts.publish(
      {
        targets: [
          {
            account: connectedAccountRef({
              backend: "default",
              platform: "bluesky",
              accountId: await resolveAuthorizedAccount(tenant),
            }),
          },
        ],
        content: { text: body.text },
        idempotencyKey: body.draftId,
      },
      // Scopes the idempotency key to this tenant, so tenants can't collide on a draft ID.
      { authorization: { tenantId: tenant.id } },
    );

    return {
      status: result.status,
      outcomes: result.outcomes.map((o) => ({ state: o.state })),
    };
  },
  {
    body: t.Object({
      text: t.String({ minLength: 1 }),
      draftId: t.String({ minLength: 1 }),
    }),
  },
);

Elysia rejects a body that fails the schema with a 422 before your handler runs. See idempotency for how keys, fingerprints, and unknown outcomes fit together.

Webhooks from the raw request

Elysia parses JSON bodies by default, and a Request body can only be read once. Set parse: "none" on the webhook route so verification reads the exact bytes the provider signed. Pass request.headers as is, because verifiers read signatures with headers.get(). Built-in verifiers throw a SocialError with code unauthorized for a missing or invalid signature, so map that error to a 401.

The route reads the body before anything is authenticated, so it stops at 1 MiB, the built-in verifiers’ default limit. A Content-Length check alone is not enough, because chunked requests omit it:

import { SocialError } from "@opencoredev/social-sdk";

const MAX_WEBHOOK_BYTES = 1024 * 1024;

async function readLimited(request: Request, limit: number): Promise<Uint8Array | null> {
  if (Number(request.headers.get("content-length") ?? 0) > limit) return null;
  const chunks: Uint8Array[] = [];
  let size = 0;
  for await (const chunk of request.body ?? []) {
    size += chunk.byteLength;
    if (size > limit) return null;
    chunks.push(chunk);
  }
  const body = new Uint8Array(size);
  let offset = 0;
  for (const chunk of chunks) {
    body.set(chunk, offset);
    offset += chunk.byteLength;
  }
  return body;
}

app.post(
  "/events",
  async ({ request, status }) => {
    const body = await readLimited(request, MAX_WEBHOOK_BYTES);
    if (!body) return status(413, "payload too large");
    let verification;
    try {
      verification = await adapter.webhooks?.verify({ headers: request.headers, body }, context);
    } catch (error) {
      if (error instanceof SocialError && error.code === "unauthorized") {
        return status(401, "unauthorized");
      }
      throw error;
    }
    if (!verification?.valid) return status(401, "unauthorized");
    // decode, resolve tenants, accept into your durable inbox
    return status(202, "accepted");
  },
  { parse: "none" },
);

See the events guide for the decode, quarantine, and worker steps.

Mount the example handler

Elysia’s mount forwards the untouched request to any fetch handler and strips the mount prefix from the path. The repository’s example Hono adapter already accepts prefix-free paths, so it works here unchanged. Import it from the example app in the Social SDK repository:

import { Elysia } from "elysia";
import { createExampleHandler } from "../../apps/example/src/app.js";
import { createHonoFetch } from "../../apps/example/src/frameworks/hono.js";

const app = new Elysia().mount("/social", createHonoFetch(createExampleHandler()));

Requests to /social/accounts and /social/publish reach the example API. Framework recipes documents what the example handler provides.

Run on Bun or Node

// Bun
import { app } from "./app.js";
app.listen(3030);

On Node, install @elysia/node and pass its adapter when you create the app:

import { Elysia } from "elysia";
import { node } from "@elysia/node";

export const app = new Elysia({ adapter: node() });

Then call app.listen(3030) as on Bun.

Notes

  • Both runtimes are tested SDK targets; the SDK is ESM-only.
  • Cloudflare Workers is not a tested SDK runtime. For Workers-fronted apps, run the SDK in a Node or Bun service behind the Worker.
  • Keep one client instance per process so per-backend concurrency limits apply.

Last updated on September 23, 2026