I run a small content brand as a side project. Posting to Twitter and Bluesky took me about 15 minutes a post, every day. So I automated it. Now I schedule a post in a CMS and it shows up on both platforms on time. It’s been running for months, and I forget it’s there most days (which is the whole point).

I’m not really a developer. I can read code and I know how systems connect, but I wasn’t going to write 400 lines of Express.js from scratch. Claude Code wrote the code. I made the design decisions.

The architecture (the part I designed)

Three pieces, each with one job:

Directus (headless CMS)
  → stores posts with media, captions, and scheduled publish times

N8N (workflow automation)
  → polls every 15 minutes for posts that are due
  → calls the posting microservice

Express.js microservice
  → receives a media URL + caption
  → posts to Twitter and Bluesky concurrently
  → returns per-platform success/failure

I chose Directus because I’d used it before and it gives me an admin screen without writing frontend code. N8N was already running my other automations. And I kept the posting logic in its own service, out of N8N, because mixing orchestration and business logic makes debugging miserable.

AI can’t make those calls for you. Claude doesn’t know what’s already on your server, which tools you’re comfortable with, or how you want to debug at 11pm. All three pieces run as Docker containers on one VPS managed by Coolify, for about $15/month.

How the work actually went

It’s less “write me a social media app” and more like being a technical project manager who can read code. I’d say something like: “The posting service needs to handle the case where Twitter fails but Bluesky succeeds. Use HTTP 207 for partial success.” Claude wrote the code, I reviewed it, and sometimes I asked for changes.

Being that specific matters. “Handle partial failures” is too vague. “Use HTTP 207 when one platform succeeds and the other fails, with per-platform status in the response body” gives Claude what it needs.

Claude also handled the platform-specific stuff I didn’t want to learn, like Twitter’s OAuth 1.0a request signing and chunked video uploads. And it debugged. Bluesky video uploads worked for months, then started failing with auth errors. Claude worked out in about a minute that I’d hardcoded a Bluesky server address (a PDS URL) that had changed. That would have taken me hours.

The 207 trick

The posting service uses Promise.allSettled, so if Twitter chokes, Bluesky still goes through. N8N can route on the status code alone: 200 means done, 207 (Multi-Status) means check which platform failed, and 500 means everything broke.

{
  "twitter": { "success": true, "id": "1234567890" },
  "bluesky": { "success": false, "error": "rate limited" }
}

N8N knows nothing about Twitter or Bluesky. It just calls a URL and handles the response. So a problem is always in one of two places: N8N didn’t call the service, or the service failed to post. Never both.

HTTP 207 has been in the spec since WebDAV and nobody seems to use it, which is a shame. I’m kind of in love with it.

How it’s held up

After a few months, it succeeds about 98% of the time, and the failures are almost always Twitter rate limits. Scheduling a post takes about 30 seconds instead of 10-15 minutes per platform. The VPS runs other stuff too, so the extra cost is close to zero.

Two things I’d change. The 15-minute polling means a post can land up to 14 minutes late, which hasn’t mattered enough to fix. And I wish I’d asked Claude for a dry-run mode from the start, because there’s no way to test a post without publishing it. (Ask me how I know.)

I could not have built this without Claude Code. But Claude couldn’t have built it without me deciding how the pieces fit together.

Twitter’s OAuth was the hardest part of all of this, by far. Budget an afternoon and try not to throw anything.