Deploy a server
Use the optional server commands to run Syndroo in your own Cloudflare account. This path connects one Threads account and can publish from GitHub Actions. The local connect, publish and status workflow remains separate.
0.7.2 self-hosted path. server init uses 0.7.2 @syndroo/cli and @syndroo/cloudflare packages and installs project-local Wrangler. Cloudflare, GitHub, Meta and Threads acceptance remains unverified. See the CLI reference for acceptance limits.
Project scope in this release. A Server command that works on a project is meant to use the project in the current directory. The published 0.7.2 release already does that for deploy and doctor, but connect threads and publish threads refuse to guess and ask for the project explicitly. Every example that operates on the project therefore passes --project . before the subcommand, so the same lines work on both the published release and the reviewed fix. That fix is not in a published release yet; explicit scope stays correct either way.
What you need
- Node.js 24.19.0 or newer on macOS or Linux, with npm.
- Git with your name and email configured.
- A Cloudflare account and a Threads app in Meta.
- GitHub CLI
ghif you want a repository and manual CI. - A project directory that will own the deployment, and the runtime manager that directory declares.
Choose the runtime before installing. From the project directory, record the runtime that is actually in effect and where a global install lands:
command -v node npm
node --version
npm --version
npm root --global
npm prefix --globalUse that same runtime for the install, the version check and every later command. With mise, prefix each command with mise exec -- and resolve the paths one at a time, because mise which takes a single argument: mise which node, mise which npm and mise which syndroo. A shim is expected, so check the executable the shell resolves instead of comparing pathnames or editing PATH. Do not assume Homebrew: several Node installations can coexist, and command -v node says which one wins.
Let an agent run the setup
Paste this prompt into an agent:
You are a local-shell coding Agent. Set up Syndroo 0.7.2 for me.
1. Before installing anything, establish where Syndroo will run. If I have not said, ask me which project directory will use Syndroo and which runtime manager that directory declares (mise, asdf, Volta, nvm, Homebrew or a system Node), and stay in that directory. A chat scratch directory or my home directory is not the project.
2. In that directory, record the runtime that is actually in effect: `command -v node npm`, `node --version` (24.19.0 or newer), `npm --version`, `npm root --global` and `npm prefix --global`. Confirm macOS or Linux for supported local writes.
3. Use that one runtime for the install, the version check, the Skill lookup and every later command. With mise, prefix commands with `mise exec --`; `mise which node`, `mise which npm` and `mise which syndroo` each take one argument, so run them separately. Do not assume Homebrew, do not compare pathnames across managers, and never prepend another bin directory to `PATH` to get around a shim.
4. Install Syndroo and its official platform plugins with:
npm install --global @syndroo/cli@0.7.2 @syndroo/provider-sdk@0.7.2 @syndroo/provider-bluesky@0.7.2 @syndroo/provider-threads@0.7.2 @syndroo/provider-linkedin@0.7.2 @syndroo/provider-mastodon@0.7.2 @syndroo/provider-devto@0.7.2
5. Check what the shell resolves, then read and follow the packaged Skill: `command -v syndroo`, `syndroo --version`, then `npm root --global` and read `<global-root>/@syndroo/cli/skills/syndroo/SKILL.md`. Do not run or invent a separate Skill-installer command.
6. Ask whether I want the local workflow or the self-hosted Server workflow, then continue with my choice. For Server setup, ask whether I want GitHub; create or push a repository only when I explicitly authorize it, and reuse authorization I have already given. In 0.7.2, `connect threads` and `publish threads` need the project passed explicitly, for example `syndroo server --project . connect threads --app-id <app-id>`. Select my intended account from the accessible accounts listed by the project-local `wrangler whoami` output and pass that account ID with `--account`; if my intended account is not listed, stop and ask me instead of deploying to another account. Authenticate only when the session is missing or expired.
7. Keep secrets out of prompts, command arguments, logs, and tracked files. Publish content only with authorization for that content and destination.
For this task, use the self-hosted Server workflow, deploy to my Cloudflare account, and connect one Threads app.
Follow https://YOUR-DOCS-ORIGIN/getting-started/server/ after replacing YOUR-DOCS-ORIGIN with the configured docs origin.
Ask whether I want GitHub; if yes, get OWNER/REPO and private/public, defaulting to private.
Use stable request IDs and stop for Wrangler login, Meta authorization or Git push when those actions still need me.
Pass the project explicitly with --project . on every Server command that works on the project. Select my intended account from the accessible accounts listed by the project-local Wrangler whoami output, and pass that account ID with --account. If my intended account is not listed, stop and ask me; never deploy to another account. Authenticate only when the session is missing or expired.1. Create the project
Run this from the parent directory that the runtime you selected controls. Choose a destination that is missing or empty, or an existing Syndroo project whose initialization can be resumed; server init refuses a nonempty directory and reports SERVER_PROJECT_CONFLICT. Check the local tools first:
node --version
npm --version
npm root --global
git config --get user.name
git config --get user.emailThen install the CLI used by this guide and check what the shell resolves:
npm install --global @syndroo/cli@0.7.2
command -v syndroo
syndroo --versionConfirm that syndroo --version reports 0.7.2; the Node.js that ran it is checked separately with node --version. For a local-only project, create it, enter it, and confirm that the same runtime and executable are still in effect:
syndroo server init ./syndroo-site
cd syndroo-site
command -v syndroo
node --version
npm --version
syndroo --versionIf command -v syndroo or node --version changes after the cd, activate the project runtime with your manager before running any Server command, for example mise exec -- syndroo --version.
For GitHub, run this alternative instead of the local-only block above, from the same runtime-controlled parent; running both would create a nested project. Reuse an existing gh session and run gh auth login only when gh auth status reports that none exists. Private is the default; use --public only when that is your choice. The repository flags go on the same command, and the new project is entered afterwards, so either branch ends inside the child directory before doctor:
gh auth status
syndroo server init ./syndroo-site --github OWNER/REPO --private
cd syndroo-site
command -v syndroo
node --version
npm --version
syndroo --versionserver init creates the application and Wrangler configs, pinned dependency files, a sample post and the first Git commit. It takes the directory as its argument, so it does not use --project.
2. Run doctor
syndroo server --project . doctor --jsonDoctor is read-only and does not repair the project. Fix any local check reported as missing or attention_required before deploying. Deployment and credentials are expected to be not_configured, while OAuth and publication readiness remain unverified.
3. Log in and deploy
server deploy reuses an existing Wrangler login and does not launch one itself. Run the project-local Wrangler first and read which accounts the session can reach:
./node_modules/.bin/wrangler whoamiRun ./node_modules/.bin/wrangler login once only when whoami reports that the session is missing or expired, and recheck with whoami afterwards; otherwise keep the session you already have.
whoami lists the accessible accounts. Choose the ACCOUNT ID of the account that should own the Worker; an account ID is not a member or user ID, and the CLI accepts it verbatim. Then review the target account, Worker and resources and authorize this one deployment:
syndroo server --project . deploy --yesPass the chosen account with --account <ACCOUNT_ID> when the session can reach more than one. On a later deploy, omit --account to reuse the verified stored account; an explicit account, CLOUDFLARE_ACCOUNT_ID or a Wrangler account_id must resolve to that same account.
If the command reports that the account cannot be used, that is a selection problem rather than an authentication failure: pick an accessible account from the whoami output and retry with --account. Sign in again only when whoami shows that the session is missing or expired, not merely because it lists an account other than the one you meant.
The result reports a deployed Worker and its confirmed HTTPS origin.
4. Register the callback
In your Meta app, register the Worker origin followed by the callback path. For example, replace the example host with your complete confirmed origin:
https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev/oauth/callback/threads5. Connect Threads
syndroo server --project . connect threads --app-id APP_IDReplace APP_ID with the Threads app ID and complete the Meta browser prompt. A terminal reads the App Secret without echo; automation can use THREADS_APP_SECRET or --app-secret-stdin. The command prints the connection ID and saves the verified App ID and connection reference in syndroo.config.jsonc.
Steps 6 and 7 are optional. If you do not want GitHub CI, setup is complete after step 5.
6. Optionally prepare GitHub Actions
Run gh auth login if you have not already. Then configure the repository:
syndroo server --project . ci setup github --repo OWNER/REPO --yesThis creates .github/workflows/threads-publish.yml and configures the threads-ci GitHub environment. It does not commit, push or publish.
Your post is strict JSON with one text property:
{
"text": "Hello from Syndroo CI"
}Review the updated config, workflow and post, then commit and push them using the authorization you already have, or ask if this push is not covered:
git add syndroo.config.jsonc .github/workflows/threads-publish.yml posts/hello.json
git commit -m "Configure Threads publishing"
git push7. Optionally run the workflow
In GitHub, open Actions, choose Publish to Threads and select Run workflow. Use a checked-in post path and a stable request ID:
- post_path:
posts/hello.json - request_id:
threads-ci:hello-001
The generated workflow runs server publish threads with a scoped token, the checked-in post path and the request ID. Publication succeeds only with a settled succeeded result and at least one successful delivery.
Review the generated publish step before you commit or run it. The 0.7.2 release writes the command without the project scope and then refuses to run it, so add --project . after server in that step, exactly as in the manual commands above. The reviewed fix makes the current directory the default, so the scope is optional there.
If the response is lost or the result is unknown, inspect the operation. Rerun the same post with the same request ID; do not create a new ID for the same logical publication.
Files and secrets
syndroo.config.jsonc is the tracked application config. wrangler.jsonc is the tracked Cloudflare config. Do not commit wrangler.effective.jsonc, .syndroo/, node_modules/, environment files or private server state.
The App Secret, CI bearer, Cloudflare admin bearer, SECRET_KEY and RUNTIME_KEY must not appear in arguments, logs or repository files. Use hidden terminal input or the documented stdin or environment paths. Recovery evidence is identifiers only: it never carries a token, a key or a secret.
If a deploy is uncertain
A deploy that stops midway has an unknown result until verified evidence says otherwise. Keep the reported operation ID, the confirmed resources and the machine receipt, then rerun the same project:
syndroo server --project . deploy --yesAn ordinary rerun may still fail closed. A complete machine receipt can let it reconcile without a new deployment, while an unusable receipt leaves the remote result unknown and the rerun asks for explicit evidence instead of guessing. The published 0.7.2 release already reports nextAction and failedStep; the evidence fields that separate the failing stage, the deploy subprocess ending and each receipt state are part of the reviewed fix and are not in a published release.
Explicit recovery verifies identifiers; it cannot repair a private record that the installed release fails to read. In 0.7.2 a stored receipt or state file that is unreadable, oversized or not valid UTF-8 can block a rerun even when you already hold verified evidence. Keep the private state and its permissions exactly as they are, and never delete files, loosen permissions or bypass a guard to get past a check. Follow the reported nextAction; if the same format failure repeats, stop and wait for the verified fix rather than editing private state by hand.
Explicit evidence has to be verified independently, never inferred. A Worker name and a /health response are not proof, and a Worker tag is not a version annotation: deployments list and versions view do not return it. Read the tag from the account-scoped Workers service metadata instead, field result.default_environment.script.tag of GET /accounts/{account}/workers/services/{name}. Then read the active version from the most recent deployment and inspect it, both pinned to the same verified account:
CLOUDFLARE_ACCOUNT_ID=<VERIFIED_ACCOUNT_ID> ./node_modules/.bin/wrangler deployments list --name <WORKER_NAME> --json
CLOUDFLARE_ACCOUNT_ID=<VERIFIED_ACCOUNT_ID> ./node_modules/.bin/wrangler versions view <ACTIVE_VERSION_ID> --name <WORKER_NAME> --jsonTake the version ID whose percentage is greater than zero in that deployment and confirm it belongs to this Worker. Use the project-local Wrangler and the runtime you selected for the project. Check the public target separately on the Worker's Domains & Routes page in the Cloudflare dashboard for the same account; no API for that check is documented here. Compare the bindings Wrangler reports with the project's D1 database, queues and recorded origin. None of these reads establishes ownership on its own: the CLI still verifies account, project, Worker, active version and target.
The minimal evidence is exactly two verified fields, supplied as JSON on standard input rather than in an argument:
printf '%s\n' '{"workerTag":"<VERIFIED_WORKER_TAG>","versionId":"<VERIFIED_VERSION_ID>"}' | syndroo server --project . deploy --yes --recover -The reviewed read-only procedure for both reads, including how the Wrangler token is kept out of arguments, environment variables and logs, is in the packaged Skill reference references/server.md at commit c2d22c1. Follow it instead of copying token handling from anywhere else.
Recovery only establishes a baseline. The command then deploys a new version, which is a new deployment and needs the same explicit authorization as any other. A malformed or oversized receipt is not adopted as evidence, and only identifiers you verified independently continue. Until a release that reports these fields is installed, keep the operation ID and the receipt, rerun the same project, and stop rather than inventing identifiers.
Other failures
- Unknown publication: treat it as possibly published. Inspect the operation and reuse the same request ID for the same content.
- Doctor is not ready: read the named checks in the CLI reference and the server configuration notes.
- Meta callback fails: compare the registered callback with your confirmed origin and see the Threads server notes and credential reference.