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.
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
localWrangler environment.
Run it locally
-
Install the source
Clone the repository and install the workspace dependencies.
git clone https://github.com/Syndroo/syndroo.git cd syndroo npm install -
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.varsThe publish example later on this page targets Bluesky and Threads together, so the local file below configures both.
SYNDROO_API_KEYis a long random secret you choose; clients send it as the Bearer token.BLUESKY_PASSWORDmust be a Bluesky app password, not your account password. The Threads value must be a long-lived user access token that carriesthreads_basicandthreads_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-tokenIf 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 HTTP422before 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
422withPLATFORM_NOT_CONFIGUREDbefore persistence or Queue delivery. A deployment with no platform credentials can still serve health checks, but it cannot accept posts. -
Create the local database
Apply the D1 migrations to the local Miniflare database.
npm run db:migrate:localThe candidate ships three migrations. Migration
0003_retry_timing.sqladds the nullablepublications.retry_atcolumn used to enforce strict retry deadlines. -
Start the local Worker
npm run devnpm run devselects thelocalWrangler environment so optional platform secrets are loaded. Warnings about unused platform secrets are expected. The local Worker defaults tohttp://localhost:8787. -
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"} -
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.queuedmeans accepted for processing, not yet confirmed by either platform. -
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 anexternalIdand a usableexternalUrl.{ "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
publishedonly when every platform succeeded. If it is stillqueuedorpublishing, wait briefly and request the same URL again. If it ispartial, at least one platform already published and at least one did not: inspect each publication entry'sstatus,errorCode,errorMessage, anderrorAmbiguousbefore retrying anything by hand. If it isfailed, no platform succeeded, and the same fields explain why.Polling is also how you list recent work.
limitis optional, defaults to50, and must be an integer from1to100.curl \ -H "Authorization: Bearer $SYNDROO_API_KEY" \ "$SYNDROO_URL/v1/posts?limit=50" -
Schedule a post
scheduledAtmust be an ISO date-time. Use an explicit timezone, preferably UTC withZ.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
scheduledAtvalue 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
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-templatepassed 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
localWrangler environment.
Read the Cloudflare deployment section in the repository README for the current procedure.
Common failures
| Symptom | Cause | What to do |
|---|---|---|
HTTP 401 | Missing or incorrect Bearer token. | Send Authorization: Bearer $SYNDROO_API_KEY with the same value the Worker has. |
HTTP 415 | Body is not sent as JSON. | Add Content-Type: application/json. |
HTTP 413 | Request body exceeds 64 KiB. | Shorten the request. Per-platform text limits are much smaller than this ceiling. |
HTTP 422 | Selected platform is not installed or its credentials are missing. | Configure that platform's secrets in .dev.vars and restart npm run dev. |
HTTP 409 | An 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_CONTENT | Text 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: true | A timeout or post-stage 5xx left the remote outcome unknown. | Check the platform manually. Syndroo does not resend ambiguous publications automatically. |