How we built the aisociation.com website
2026-08-10
This is a short account of how the Cause Association Forum got built: a private, invite-only forum on Cloudflare's edge, designed and implemented by me working with an AI agent. I'm writing it down because the method turned out to be more interesting than the product, and because the thing that made the method work — being deliberate about what the agent has to hold in its head — isn't obvious until you've watched it fail the other way.
What we shipped
Nine phases, twenty-seven commits, in about two days of working sessions.
| Production code | 8,060 lines (lib/, app/, components/, migrations/, scripts/) |
| Test code | 5,372 lines — a 0.67:1 ratio to production |
| Tests | 337, across 18 files, full suite in 64 seconds |
| Bundle | 1.30 MB gzip, 51.9% of the 2.5 MB budget |
| Live | https://aisociation.org — apex, custom domain, real mail, cron janitor |
Everything is deployed. Not "works on my machine" — deployed, on the real domain, verified with curl against the live origin.
The core idea: context is the scarce resource
An AI agent has a fixed working memory. Every file it reads, every error it re-diagnoses, every architectural decision it re-derives, spends from the same budget. When that budget runs out mid-task the session compacts, and compaction is where quality goes to die — the agent keeps the summary and loses the texture.
So we treated context the way you'd treat RAM on an embedded target. Every decision below is really the same decision: make the next question cheap to answer.
1. Plan once, in writing, before any code
We wrote the full phased plan as a document before the repo existed. It cost a few thousand tokens once. It then got re-injected at the start of every subsequent session at near-zero cost, and it meant we never re-litigated the architecture. Compare that to the default mode, where every new session re-derives "why are we on Workers and not Pages" from scratch.
The plan also caught five wrong library choices before they were installed — bcrypt can't load in Cloudflare's runtime, speakeasy is unmaintained since 2016, DOMPurify needs a DOM that doesn't exist there. Finding that in a document costs a paragraph. Finding it at the bundler costs an afternoon.
2. Phases with exit criteria, each one ending deployed
No phase started until the previous one's exit check passed on the remote.
This bounds the working set. Building Phase 5's two-factor auth never required Phase 4's Markdown pipeline in context, because Phase 4 was finished, tested, and deployed — a closed box with a green light on it. Work that isn't sealed off leaks into every later session as "let me just re-check how that works."
3. Strict layering, enforced by the linter
L4 app/** routes, server actions
L3 lib/services/** the only place auth decisions get composed
L2 lib/*.ts password, session, markdown, github, validation
L1 lib/env.ts typed access to platform bindings
L0 lib/types.ts pure types, zero imports
Imports only go downward, and an ESLint rule fails the build if they don't.
The token payoff is direct: to change something at L3 you need L0–L2, and you provably do not need app/. The layering isn't a style preference, it's a statement about how much of the repo any given task has to load.
4. Let the compiler do the reading
We turned TypeScript up as far as it goes — strict, plus noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride.
This is the single highest-leverage thing in the project. Checking whether a change broke something by reading files costs tokens proportional to the size of the repo. Checking it with tsc --noEmit costs about two hundred tokens of output and is exhaustive. The stricter the settings, the more questions get answered by the compiler instead of by reading.
It caught real things. The GitHub test file I finished this afternoon had three places where indexing into an array was assumed safe; the compiler named all three by line number in under a second.
5. Tests are the receipt, not the ritual
337 tests, and the reason there are that many isn't diligence — it's economics. A green test run is fifteen tokens of evidence for a claim that would take thousands of tokens of careful reading to establish by inspection.
Two suites, deliberately split:
- unit — password encoding, TOTP against the RFC 6238 vectors, and a checked-in XSS corpus for the Markdown renderer. That corpus is the highest-value file in the repo.
- workers — runs in Cloudflare's actual runtime, against a real database. Because "passes in Node" and "works in production" are different claims, and the difference is exactly where this platform bites.
The standing rule we worked under: a green receipt has to come from a measurement, not from the code looking right.
6. Rationale lives in the code, at the point of the decision
Every non-obvious choice is commented where it was made — and the comment says why, including what we rejected.
The projects migration doesn't just declare two timestamp columns, it explains that one nullable fetched_at cannot simultaneously mean "this snapshot is from yesterday" and "this is going stale," which is why there are two. The GitHub URL parser explains that github.com.evil.test/a/b is a perfectly valid URL that a naive includes("github.com") check waves straight through.
A separate design document has to be loaded. A comment is already there when you're reading the line it explains. Same information, zero marginal cost.
7. Compress each phase into a handoff, then let the source go
Each phase ends with a handoff report: files changed, rationale for anything not already in the plan, edge cases assumed and not tested, and the actual curl output from the remote. Thirteen documents in docs/ now.
A new session reads a 200-line handoff instead of 8,000 lines of source. That's the compression ratio the whole method is chasing.
8. Write down every trap, once
Cloudflare and Next.js 16 have a lot of sharp edges, and the expensive ones are the failures that look like something else:
wrangler deployexits non-zero after a completely successful deploy.nvm usesilently does nothing when the shell's directory has drifted, and the only symptom is a misleading Node version error three minutes later.- Invite codes get mangled by the code alphabet's own canonicalization rules.
Each of those cost real time the first time. Each is now a written note. The second encounter costs nothing, which is the entire point — the agent should never have to be taught the same thing twice.
9. Stop after three failures
If the same work unit fails verification three times in a row, stop and write up the three wrong diagnoses. Don't try a fourth.
A wrong fourth guess isn't just wasted budget, it's poisoned budget — the failed attempts stay in context and drag every subsequent attempt toward the same wrong neighborhood. Cutting the loop early and writing down what we ruled out is strictly better than pushing on.
Where it paid off, concretely
The deploy that exited non-zero. The error pointed at an API token permission. The obvious move was to widen the permission. Instead we read Cloudflare's own CLI source and found the failing call is only reachable from two specific commands — swapping to a different pair of commands fixed it with the token untouched, and left the security posture narrower than the "obvious" fix would have. Read the tool's source before accepting its error at face value. That one habit has now paid for itself several times.
Email deliverability. We didn't assert it worked. We sent a real message and read Gmail's own headers back: SPF pass, DKIM pass, DMARC pass, inbox not spam. Two things surfaced in the process that would have made a naive test lie to us — Cloudflare signs with two DKIM keys, and a Gmail-to-Gmail message carries no authentication results at all, which makes self-addressed test mail completely vacuous as evidence.
The database race. Sign-up redeems a single-use invite code. We didn't reason about whether it was safe under concurrency — we fired twenty simultaneous redemptions of one code at it and asserted exactly one winner.
What I'd tell someone starting this
The instinct is to point the agent at the goal and let it work. That produces motion, and motion looks like progress right up until the session compacts and you discover it's been rebuilding its own understanding every hour.
The better move is boring and it's all front-loaded: write the plan down, cut the work into phases that each end in something deployed, turn the type checker up past comfortable, test the claims you'd otherwise have to trust, and put the reasoning next to the code.
None of that is novel software engineering. It's what good engineers already do. What's new is why it matters this much — every one of those practices converts an expensive question ("is this right?") into a cheap one ("is the build green?"). With a human team that's a nice-to-have. With an agent whose working memory is the binding constraint, it's the whole ballgame.
We built a complete, secure, deployed application in two days. Not because the model is fast — because it almost never had to learn the same thing twice.