Skip to content

drateberry/social-sync

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Social Sync

Self-hosted cross-platform social sync for X, Mastodon, and Bluesky. Post to any one of them and the post fans out to the other two within ~15 minutes.

Single-tenant (one user per deployment), runs on Cloudflare Workers + D1 + R2 + Queues. MIT-licensed.

For the design rationale, especially loop prevention, see docs/architecture.md. For X API cost tuning, see docs/costs.md.


Repo layout

/shared      TypeScript types shared between worker and web
/worker      Cloudflare Worker (pollers, publishers, orchestrator, queue consumer, admin API)
  /migrations  D1 schema migrations + seed template
  /tests       Vitest unit tests (loop-prevention canary lives here)
/web         Next.js (App Router) dashboard, deployed as a Cloudflare Worker via OpenNext
/docs        Architecture + cost notes

Quick start (fresh clone)

Requirements: Node 20+, a Cloudflare account, wrangler (installed as a worker devDependency — call via npx wrangler or the workspace scripts).

git clone <repo-url> social-sync
cd social-sync
npm install
npm run test          # 38 tests, all local — no Cloudflare setup required yet

If tests pass, the code is healthy. Now set up Cloudflare.

1. Create Cloudflare resources

cd worker
npx wrangler login

npx wrangler d1 create social-sync
#   → copy the returned database_id into worker/wrangler.toml
#     (the top-level [[d1_databases]] block)

npx wrangler r2 bucket create social-sync-media
npx wrangler queues create social-sync-publish
npx wrangler queues create social-sync-publish-dlq

Open worker/wrangler.toml and paste the D1 database_id into the empty database_id = "" field. If you also want a preview environment, repeat for social-sync-preview, social-sync-media-preview, and social-sync-publish-preview and fill the [env.preview.*] block.

2. Run migrations

# from worker/
npm run db:migrate:local    # for `wrangler dev`
npm run db:migrate:remote   # for the deployed worker

3. Configure secrets

Copy the template and fill in the values you need:

# from worker/
cp .dev.vars.example .dev.vars   # for local `wrangler dev`

For production, set each as a Cloudflare secret instead (values below, only the platforms you want to sync are required):

# Mastodon (required for Mastodon sync)
npx wrangler secret put MASTODON_INSTANCE_URL
npx wrangler secret put MASTODON_ACCESS_TOKEN

# Bluesky (required for Bluesky sync)
npx wrangler secret put BLUESKY_IDENTIFIER
npx wrangler secret put BLUESKY_APP_PASSWORD

# X (optional — only if you set ENABLE_X_SYNC="true" in wrangler.toml)
npx wrangler secret put X_BEARER_TOKEN
npx wrangler secret put X_CLIENT_ID
npx wrangler secret put X_CLIENT_SECRET

# Admin auth fallback (skip if using Cloudflare Access — see Admin section)
npx wrangler secret put ADMIN_PASSWORD_HASH

Platform-specific instructions for obtaining these values are in the Platform setup section below.

4. Seed your platform accounts

# from worker/
cp migrations/seed.sql.example migrations/seed.sql
# edit seed.sql, fill in your handles/IDs, then:
npx wrangler d1 execute social-sync --file=./migrations/seed.sql                # local
npx wrangler d1 execute social-sync --file=./migrations/seed.sql --remote       # production

The seed.sql file is gitignored, so your IDs stay out of version control.

5. Optional: configure blog exclusion

If you auto-post from a personal blog to all three networks via some other automation, set BLOG_FEED_URL in worker/wrangler.toml to the RSS/Atom feed URL of that blog. Social Sync will skip any social post that links to a blog entry published in the last 2 hours — preventing re-amplification of your own cross-network announcements.

Leave BLOG_FEED_URL = "" to disable this feature entirely.

6. Deploy

Deploy the worker:

cd worker && npx wrangler deploy

Deploy the dashboard. It's a Next.js App Router app with server components, compiled to a Cloudflare Worker (with static assets) via the OpenNext Cloudflare adapter. The dashboard Worker reaches the API Worker via a service binding (configured in web/wrangler.toml), not a public URL — so the API Worker has workers_dev = false and is not exposed to the open internet.

cd ../web
cp .env.example .env.local                  # used only for `npm run dev` locally
npm run cf:deploy                           # opennextjs-cloudflare build + deploy

The dashboard deploys as a separate Worker named social-sync-web — distinct from the API Worker. You can preview locally with npm run cf:preview. The service binding requires the API Worker (social-sync) to exist first; step 6 already took care of that.

Then give the dashboard the admin password so it can authenticate service-binding calls to the API Worker:

# from web/
npx wrangler secret put ADMIN_TOKEN         # plaintext admin password (the one you hashed into ADMIN_PASSWORD_HASH on the API worker)

Optional — IP allowlist: to restrict dashboard access to specific IPs (e.g. your VPN egress), set a comma-separated list of IPv4/IPv6 addresses and/or IPv4 CIDR ranges:

npx wrangler secret put ALLOWED_IPS         # e.g. "203.0.113.42" or "203.0.113.0/24,198.51.100.5"

Leave ALLOWED_IPS unset to accept any IP. The check is implemented as Next.js middleware against Cloudflare's cf-connecting-ip header.

If you're using Cloudflare Access on a custom domain instead, skip ADMIN_TOKEN and ALLOWED_IPS — but note that Access doesn't protect *.workers.dev URLs, so you'd need to put the dashboard behind a zone you own.


Platform setup

Mastodon

  1. Log in to your Mastodon instance.
  2. Settings → Development → New Application.
  3. Scopes: read:accounts, read:statuses, write:statuses, write:media.
  4. Save, then copy "Your access token".

Values:

  • MASTODON_INSTANCE_URL — e.g. https://mastodon.social
  • MASTODON_ACCESS_TOKEN — from the app page

Docs: https://docs.joinmastodon.org/client/token/

Bluesky

Use an app password, not your main password.

  1. Settings → Privacy and Security → App passwords → Add.
  2. Copy the generated password.

Values:

  • BLUESKY_IDENTIFIER — e.g. you.bsky.social
  • BLUESKY_APP_PASSWORD — the app password
  • BLUESKY_SERVICE_URL — optional, defaults to https://bsky.social

Docs: https://docs.bsky.app/docs/advanced-guides/app-passwords

X (Twitter)

Requires a developer account and an app with Read and Write user permissions. X's API has per-call pricing — read docs/costs.md before enabling. Keep ENABLE_X_SYNC = "false" in wrangler.toml until you've confirmed the spend ceiling is set the way you want.

Values:

  • X_BEARER_TOKEN, X_CLIENT_ID, X_CLIENT_SECRET

Docs: https://developer.x.com/en/docs

Admin auth

Two options:

Cloudflare Access (recommended). Wrap the deployed worker URL in a Zero Trust Access application. The worker trusts the CF-Access-Authenticated-User-Email header that Access injects. Nothing to configure in this repo.

Password fallback. If you'd rather not set up Access:

printf 'your-long-random-password' | shasum -a 256
npx wrangler secret put ADMIN_PASSWORD_HASH   # paste the hex digest

The dashboard then sends this password in the x-admin-token request header.

Local development

npm install
npm run test          # all workspaces, ~1s
npm run typecheck     # all workspaces
npm run worker:dev    # wrangler dev on :8787
npm run web:dev       # next dev on :3000

The canary test (worker/tests/loop-prevention.test.ts) is the one test you must never let regress — it covers the X→Mastodon→poll-Mastodon scenario and each loop-prevention rule individually. If this test fails, stop and investigate.

Monitoring

  • npx wrangler tail social-sync — live worker logs.
  • /logs on the dashboard — last 500 sync events from D1.
  • / on the dashboard — current month's X spend vs. ceiling.

Contributing

Pull requests welcome. Two ground rules:

  1. The loop-prevention canary test must stay green. If you touch any of the rules in worker/src/lib/loop-prevention.ts or the surrounding infrastructure, add a regression test for the scenario you're changing.
  2. If you add platform-specific behavior (new field, new limit, new quirk), add a short note to docs/architecture.md under "Text transformation and platform specifics" so the reasoning survives future refactors.

License

MIT. See LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages