Syndroo docs
Website GitHub

Quickstart

Run the Syndroo Worker on your machine, publish a post to your own accounts, and read its stored status. This path needs Node.js 22 or newer and npm; a Cloudflare account is only needed when you deploy.

These commands publish for real.

Once platform credentials are configured, a copied POST /v1/posts request creates a live post on every selected platform. The interactive demo on the marketing site is simulated; this page is not. Use a test account or an unpublished draft workflow of your own.

Prerequisites

  • Node.js 22 or newer. The repository declares "node": ">=22" for the workspace and the Worker package.
  • npm. The repository is an npm-workspaces monorepo.
  • git. Used to clone the source repository.
  • Platform credentials. Only for the platforms you want to publish to. Local development loads optional platform secrets from the local Wrangler environment.

Run it locally

  1. Install the source

    Clone the repository and install the workspace dependencies.

    git clone https://github.com/Syndroo/syndroo.git
    cd syndroo
    npm install
  2. Create your local secrets file

    Copy the example file, then put only the credentials you use into .dev.vars. Do not commit it.

    cp .dev.vars.example .dev.vars

    The publish example later on this page targets Bluesky and Threads together, so the local file below configures both. SYNDROO_API_KEY is a long random secret you choose; clients send it as the Bearer token. BLUESKY_PASSWORD must be a Bluesky app password, not your account password. The Threads value must be a long-lived user access token that carries threads_basic and threads_content_publish.

    SYNDROO_API_KEY=choose-a-long-random-secret
    BLUESKY_IDENTIFIER=alice.bsky.social
    BLUESKY_PASSWORD=your-bluesky-app-password
    BLUESKY_HOST=bsky.social
    THREADS_ACCESS_TOKEN=your-long-lived-threads-user-token

    If you only want to test Bluesky first, omit the Threads line and change the publish request below to "platforms": ["bluesky"]. Selecting a platform without its credentials fails with HTTP 422 before anything is stored, so the two-platform example needs both credential sets.

    Optional secrets are validated per platform.

    Platforms are enabled independently. A missing or incomplete credential set returns HTTP 422 with PLATFORM_NOT_CONFIGURED before persistence or Queue delivery. A deployment with no platform credentials can still serve health checks, but it cannot accept posts.

  3. Create the local database

    Apply the D1 migrations to the local Miniflare database.

    npm run db:migrate:local

    The candidate ships three migrations. Migration 0003_retry_timing.sql adds the nullable publications.retry_at column used to enforce strict retry deadlines.

  4. Start the local Worker

    npm run dev

    npm run dev selects the local Wrangler environment so optional platform secrets are loaded. Warnings about unused platform secrets are expected. The local Worker defaults to http://localhost:8787.

  5. Check the service

    The health endpoint does not require authentication. Set the two variables in the terminal that will call Syndroo.

    export SYNDROO_URL="http://localhost:8787"
    export SYNDROO_API_KEY="the-same-secret-from-dev-vars"
    curl "$SYNDROO_URL/health"

    Expected response:

    {"status":"ok"}
  6. Publish a post

    Send one shared body with per-platform overrides. This example targets Threads and Bluesky and uses a stable idempotency key.

    curl -X POST "$SYNDROO_URL/v1/posts" \
      -H "Authorization: Bearer $SYNDROO_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: example-post-001" \
      --data '{
        "content": "Hello from Syndroo on Threads",
        "platforms": ["threads", "bluesky"],
        "overrides": {
          "bluesky": { "content": "Hello from Syndroo on Bluesky" }
        }
      }'

    Syndroo accepts the request before the Queue finishes publishing, so the response uses HTTP 202:

    {
      "id": "post_...",
      "status": "queued"
    }

    Copy the returned id. queued means accepted for processing, not yet confirmed by either platform.

  7. Check the result

    Request the same post until it reaches a terminal status.

    curl \
      -H "Authorization: Bearer $SYNDROO_API_KEY" \
      "$SYNDROO_URL/v1/posts/post_..."

    A successful two-platform publication eventually looks like this, with one publication entry per selected platform. The Threads adapter returns an externalId; Bluesky returns both an externalId and a usable externalUrl.

    {
      "id": "post_...",
      "content": "Hello from Syndroo on Threads",
      "platforms": ["threads", "bluesky"],
      "status": "published",
      "createdAt": "2030-01-02T03:04:05.000Z",
      "overrides": {
        "bluesky": { "content": "Hello from Syndroo on Bluesky" }
      },
      "publications": [
        {
          "id": "pub_...",
          "postId": "post_...",
          "platform": "threads",
          "provider": "threads-native",
          "content": "Hello from Syndroo on Threads",
          "status": "published",
          "attempts": 1,
          "externalId": "1784...",
          "errorAmbiguous": false,
          "createdAt": "2030-01-02T03:04:05.000Z",
          "publishedAt": "2030-01-02T03:04:07.000Z"
        },
        {
          "id": "pub_...",
          "postId": "post_...",
          "platform": "bluesky",
          "provider": "bluesky-native",
          "content": "Hello from Syndroo on Bluesky",
          "status": "published",
          "attempts": 1,
          "externalId": "bafyre...",
          "externalUrl": "https://bsky.app/profile/.../post/...",
          "errorAmbiguous": false,
          "createdAt": "2030-01-02T03:04:05.000Z",
          "publishedAt": "2030-01-02T03:04:06.000Z"
        }
      ]
    }

    The aggregate status is published only when every platform succeeded. If it is still queued or publishing, wait briefly and request the same URL again. If it is partial, at least one platform already published and at least one did not: inspect each publication entry's status, errorCode, errorMessage, and errorAmbiguous before retrying anything by hand. If it is failed, no platform succeeded, and the same fields explain why.

    Polling is also how you list recent work. limit is optional, defaults to 50, and must be an integer from 1 to 100.

    curl \
      -H "Authorization: Bearer $SYNDROO_API_KEY" \
      "$SYNDROO_URL/v1/posts?limit=50"
  8. Schedule a post

    scheduledAt must be an ISO date-time. Use an explicit timezone, preferably UTC with Z.

    curl -X POST "$SYNDROO_URL/v1/posts" \
      -H "Authorization: Bearer $SYNDROO_API_KEY" \
      -H "Content-Type: application/json" \
      --data '{
        "content": "Shared fallback",
        "platforms": ["bluesky"],
        "overrides": {
          "bluesky": { "content": "Scheduled Bluesky post" }
        },
        "scheduledAt": "2030-01-02T03:04:05.000Z"
      }'

    Expected response:

    {
      "id": "post_...",
      "status": "scheduled",
      "scheduledAt": "2030-01-02T03:04:05.000Z"
    }

    Cron scans every 15 minutes, so scheduled publication can occur up to roughly 15 minutes after the requested time. A scheduledAt value in the past is handled as an immediate post.

Run the local test gates

The repository ships unit tests, type checks, and a Mock SNS end-to-end gate that drives the bundled Worker, D1, Queue, Cron handler, and real adapters against loopback servers.

npm test
npm run check
npm run test:e2e
The end-to-end gate needs no credentials.

It injects fake bindings, replaces the network boundary with a loopback Mock SNS server, and fails closed on any outbound attempt outside its allowlist. It proves wiring, persistence, idempotency, ambiguity handling, and scheduling; it cannot prove provider permissions, API compatibility, or rate limits.

Bluesky and Threads live-account acceptance remains a separate release requirement. X, Tumblr, and LinkedIn are experimental in this candidate.

Deploying later

Deployment is not part of this prototype. When you are ready, the repository README documents the Cloudflare path: the Deploy to Cloudflare button creates an independent repository in your account, prompts for SYNDROO_API_KEY, provisions D1 and Queue resources, applies migrations, configures the Cron Trigger, and deploys the Worker.

  • Production requires only SYNDROO_API_KEY. Configure platform secrets after deployment, only for the platforms you use.
  • The thin syndroo-deploy-template passed isolated local installation, migration, build, and startup checks. Registry installation and live deployment remain pending, and the button is not yet wired to it.
  • Never deploy using the local Wrangler environment.

Read the Cloudflare deployment section in the repository README for the current procedure.

Common failures

SymptomCauseWhat to do
HTTP 401Missing or incorrect Bearer token.Send Authorization: Bearer $SYNDROO_API_KEY with the same value the Worker has.
HTTP 415Body is not sent as JSON.Add Content-Type: application/json.
HTTP 413Request body exceeds 64 KiB.Shorten the request. Per-platform text limits are much smaller than this ceiling.
HTTP 422Selected platform is not installed or its credentials are missing.Configure that platform's secrets in .dev.vars and restart npm run dev.
HTTP 409An Idempotency-Key was reused with a different request.Use a new key for a genuinely different post, or resend the original body.
Publication failed with INVALID_CONTENTText passed the API but exceeded the platform's own limit.Shorten the per-platform override. Threads allows 500 characters, Bluesky 300 characters and 3,000 UTF-8 bytes, X 280 weighted characters.
errorAmbiguous: trueA timeout or post-stage 5xx left the remote outcome unknown.Check the platform manually. Syndroo does not resend ambiguous publications automatically.