foryour.fans: An AT Protocol-Native Creator Platform, Built Spec-First with AI
Why I built this
Two things I keep coming back to outside of work: open protocols and the creator economy. Bluesky and the AT Protocol made the first one concrete again — portable identity, user-owned data, and a public firehose anyone can build on — and the creator-platform space is where that openness has the most obvious unmet need. Existing creator platforms own the identity, own the audience graph, and own the payment relationship. If the platform decides you’re done, so is everything you built on it.
So I set out to build the counter-example: a paid creator platform where the identity is yours, the public content is yours, and the platform is just one app reading and writing your repo. It’s called foryour.fans, and it’s an AT Protocol-native take on Patreon / OnlyFans.
What it is
A creator signs in with their existing AT Protocol identity — no new username, no password, no “create an account” step. Their account is their DID. From there they publish a public profile and posts, define subscription tiers, and gate private content behind entitlements. Subscribers sign in the same way, discover creators, subscribe, and unlock tier content.
The split that makes it work:
- Identity — the creator’s own AT Protocol account. Resolved and OAuth’d at login; the app never sees anything password-shaped.
- Public profile, public posts, tier metadata — Lexicon records (
fans.foryour.profile/post/tier) written into the creator’s own PDS via OAuth-scoped writes. The platform never runs a PDS of its own. Public posts can be dual-published asapp.bsky.feed.postso they also show up in Bluesky. - Private / subscriber / tier content — the application’s PostgreSQL database. App-authoritative, never written to the open network.
- Media bytes — private S3-compatible object storage, served only through short-lived signed URLs after an entitlement check.
- Discovery — a separate long-lived process consumes the AT Protocol firehose (Jetstream) and indexes any DID publishing
fans.foryour.*records, whether or not that person has ever signed into this app.
A driving constraint throughout: the platform is built for creator niches that mainstream payment processors and platforms often won’t serve. That single assumption is behind a lot of the architecture — payments and payouts sit behind a PaymentProvider / PayoutProvider abstraction instead of a hardcoded integration, and creator identity and age verification are first-class gating requirements rather than “future work.”
How it was built: a spec, then AI, phase by phase
The entire codebase was scaffolded by AI from a written specification — not vibe-coded prompt by prompt, but driven from a committed prompts/ directory that lays out the build as 17 numbered API phases and a shadowing set of web-client phases, each with explicit exit criteria: tests pass, lint passes, type-check passes, the app starts, docs updated, known limitations written down, next phase identified.
That structure is the whole point. Each phase is a contained unit of work with a definition of done, so the AI is implementing against a spec and a checklist instead of a vague request. The result, over about ten days of iteration:
- a pnpm-workspace monorepo — a deliberate modular monolith, two deployable apps (
apps/apion Fastify 5,apps/webon Next.js 15 / React 19) plus ten library packages consumed viaworkspace:* - 862 unit / integration tests and 36 browser tests green, plus lint, type-check, and a clean dependency audit
- AT Protocol OAuth (PKCE + DPoP), custom Lexicons, creator accounts, subscription tiers, a payment/entitlement layer, private content storage, secure media, feeds, firehose-backed discovery, comments/likes, a creator analytics dashboard, a trust-and-safety foundation, rate limiting and security headers, and Kubernetes manifests
- two mid-project rearchitecture specs written and executed after the fact (moving toward creator-owned encrypted storage, and Bluesky-compatible public posts)
This is the same spec-driven, AI-assisted delivery approach I apply to reliability and platform work — the rigor comes from the spec and the exit checklist, and the AI moves the implementation faster without loosening either.
Architecture decisions worth calling out
The AT record is written before the database row. Becoming a creator is fundamentally a “publish to the open network” action. So POST /creators publishes the fans.foryour.profile record to the user’s PDS first; only on success does anything land in Postgres. If the PDS write fails, the request returns 502 and the local state never diverges from what’s actually published. The local displayName / bio / website columns are a write-through cache of that record, never the source of truth.
The Lexicon schemas are resolvable from the domain. fans.foryour.* isn’t just an internal namespace — two DNS TXT records point foryour.fans at a did:web, and the web app serves a minimal signed AT Protocol repo (a CAR with a real commit signature and MST proof, not just JSON) so any other app on the network can fetch and validate our record definitions. That’s the half of the portability promise that’s easy to skip: a second app can read our records and verify them.
Payments are an interface with fakes behind it. No real processor is integrated, on purpose. PaymentProvider / PayoutProvider define a hosted-checkout shape (createSubscription returns {status, redirectUrl?} rather than assuming synchronous confirmation), webhook processing is idempotent via a PaymentEvent ledger keyed on [provider, providerEventId], and entitlement checks use a price snapshot taken at subscription time, not the live tier price. Because the target creators rule out the obvious mainstream processors, this abstraction isn’t speculative flexibility — it’s the point. Swapping in a specialized processor later touches no domain code.
Discovery is its own process. The Jetstream consumer runs as a separate OS process from the HTTP API, sharing one codebase, so that horizontally scaling the API doesn’t mean N replicas all re-consuming the same firehose and racing to write the same index.
Where AI moved fast, and where live verification earned its keep
The interesting failures were all at the boundary between what the docs said and what the network actually does.
The Jetstream wire format. Doc-based research described a “v2” envelope shape as recommended for new projects. Connecting directly to the real production endpoint showed it actually serves the older flat v1 shape, and that the query parameter is wantedCollections, not collections — the real server just silently ignores the wrong one rather than erroring. The ingestion code is built against the verified-real format, confirmed by a live consumer processing hundreds of real events end to end. This is exactly the class of gap that a live check catches and a doc fetch doesn’t.
Cookies and same-origin. The API and web app run on different ports. The obvious approach — browser JS calling the API cross-origin with credentials: 'include' — doesn’t work for a SameSite=Lax session cookie, and switching to SameSite=None; Secure trades that for flakiness on plain-HTTP localhost. The fix is a Next.js rewrite so every browser request, including the OAuth callback that sets the cookie, is same-origin — which also happens to match how it’d be deployed behind a single ingress in production.
Real test races, found and fixed. Vitest runs test files in parallel, and that surfaced two genuine bugs during the build: two files independently using the same hardcoded AT handle ("liam.test"), and one file’s blanket afterEach cleanup wiping rows a concurrently-running file hadn’t finished asserting on. Both fixed the same way — unique per-test identifiers, cleanup scoped to the specific DIDs a test created, never a blanket wipe — and written up so the pattern doesn’t come back.
Status, honestly
It’s feature-complete through the planned build phases and runs end to end locally — sign in with a real AT identity, become a creator, publish, subscribe, unlock content, get discovered through the firehose. What it is not is deployable for real money: no real payment or payout processor is integrated, and NODE_ENV=production deliberately refuses to boot with the fake providers in place. Age and identity verification is designed as a gating requirement but not built against a real KYC vendor. The did:web signing key in the repo is a clearly-stamped dev key. It’s a working reference implementation, not a launched product.
What I took from it
The reusable lesson isn’t about creator platforms — it’s about the shape of the work. A written spec broken into phases with hard exit criteria turns AI from a fast way to produce code into a fast way to produce reviewed, tested, documented code. And every time the spec said “research the current mechanism before implementing,” the payoff came from checking against the live network rather than the docs. That’s the same instinct that keeps production systems honest: trust the behavior you’ve observed, not the behavior you were promised.