How netweft is built, and a prompt to build your own
netweft is four Cloudflare Workers over one D1 database: static marketing and dashboard sites, a Hono API with Workflows and a cron, and an analytics relay. Six decisions shaped it: a provider port, reserve-then-settle credits, stored snapshots, one-statement D1 guards, a read-only MCP surface and no free tier.
netweft is an SEO research and rank-tracking product that one person can run. This guide lays out how it is built, explains the six decisions that shaped it, and ends with a prompt you can hand to an AI coding agent to build a product of the same shape.
The pieces
Everything runs on Cloudflare, as four Workers in one Bun and Turborepo workspace:
| Worker | What it is |
|---|---|
| Marketing | an Astro site rendered to static files at build time |
| Dashboard | a TanStack Start single-page app, served as static assets |
| API | Hono with an OpenAPI 3.1 document, two Cloudflare Workflows and a five-minute cron |
| Relay | a small Worker that forwards consented analytics |
The API owns all state: a D1 database through Drizzle ORM, a KV namespace for caching vendor answers, and Workers Rate Limiting for request limits. Sign-in is better-auth with Google, GitHub and email codes, payments are Polar, SEO data comes from DataForSEO, AI calls go through OpenRouter and email through Resend.
The dashboard and the API are separate origins on the same site, so a
SameSite=Lax session cookie works across them without a token in the
browser.
Decision 1: the data vendor sits behind a port
One module knows DataForSEO exists. Everything else calls an interface with two implementations: the real adapter, and a mock that serves fixtures and needs no account. The whole product runs locally and in tests with no vendor key, and a vendor change touches one folder. See the provider sits behind a port.
Decision 2: credits are reserved, then settled
Every paid call holds its price before it runs and settles at what the vendor actually billed, never more than the hold. A workspace that cannot cover the hold is refused before money is spent. Lists are held at their 200-row ceiling; rank checks are priced per page of ten results, because that is how the vendor bills them. See reserve, then settle and the rate card.
Decision 3: rankings are stored observations
A rank check writes a snapshot per keyword and device: position, URL and the time it was seen. History is the list of snapshots, never a recomputation, so last month’s chart cannot change. See snapshots, not recomputation.
Decision 4: D1 guards are single statements
D1 has no interactive transactions, so every check-then-write is one
conditional statement (INSERT … SELECT … WHERE the balance still covers
it), one atomic batch, or a short leased lock row. Long work runs in
Workflows, which retry steps and survive a Worker being recycled, and the cron
starts scheduled checks and ends runs whose Workflow is gone.
Decision 5: agents read, people spend
An MCP server lets an agent read a workspace with a scoped API key. All 25 tools are read-only over stored rows, so a leaked key can read but never write or spend. The in-app assistant uses the same tools and must quote only figures a tool returned.
Decision 6: no free tier, a trial and a demo instead
Every lookup costs real money, so a free tier pays for accounts that never convert. netweft offers a short trial with a tighter rate limit and a read-only demo that answers from sample data. See why netweft has no free tier.
A prompt to build your own
Paste this into an AI coding agent such as Claude Code in an empty repository. Replace the bracketed parts. It builds in phases, and each phase ends with tests passing before the next starts.
Build a metered SEO SaaS for [your niche] as a Bun + Turborepo monorepo on
Cloudflare Workers. Work in phases; finish each with passing tests and a
commit before starting the next. Ask me before choosing anything not listed.
Stack:
- packages/api: Hono with @hono/zod-openapi on a Worker, D1 via Drizzle ORM,
KV for caching, Workers Rate Limiting, Cloudflare Workflows for long jobs,
a cron trigger every 5 minutes. Vitest with @cloudflare/vitest-plugin
against a real local D1.
- packages/web: TanStack Start as a static SPA, TanStack Query, Tailwind,
shadcn/ui, a typed client generated from the API's route types.
- packages/db: one Drizzle schema and its migrations, shared by api and web
test fixtures.
- Auth: better-auth with organizations (workspaces), Google sign-in and
email + password confirmed by a 6-digit code.
- Payments: Polar subscriptions and one-off top-ups, mirrored by webhook.
Rules that must hold everywhere:
1. The SEO data vendor ([DataForSEO or another]) sits behind a provider
interface with a deterministic mock; nothing outside the adapter imports
the vendor client. The app must run end to end with the mock.
2. Credits: 1 credit = a fixed fraction of a dollar of vendor cost. Before any
paid call, insert a hold with ONE conditional SQL statement that succeeds
only if balance minus open holds covers it; refuse with 402 otherwise.
After the call, settle at the vendor's reported cost, capped at the hold.
Price every operation per the vendor's real billing unit, and hold list
lookups at their maximum page size.
3. D1 has no interactive transactions: use single conditional statements,
db.batch, or a leased lock row for every check-then-write.
4. Rank checks store immutable snapshots (keyword, device, position or null,
URL, timestamp). Never recompute history.
5. Expose a read-only MCP server at POST /mcp authenticated by hashed,
scoped API keys. No tool writes or spends credits.
6. Every outbound fetch of a user-supplied URL passes an SSRF guard that
resolves DNS and refuses private addresses, follows redirects manually
and caps the body size.
7. No free tier: plans with a trial and trial credits, plus a read-only demo
workspace that answers paid lookups from sample data.
Phases:
1. Monorepo, D1 schema, auth, workspaces and projects (domain + market).
2. Credit ledger, holds, settlement, Polar plans, top-ups and webhook.
3. Keyword research and saved keywords through the provider port.
4. Rank tracking: trackers, Workflow-run checks, snapshots, cron schedule.
5. Site audit: own crawler in a Workflow, robots.txt, issues by severity.
6. Dashboard screens for each of the above, then the MCP server.
7. Marketing site, docs and the read-only demo.The prompt encodes the decisions above as rules rather than suggestions, because an agent follows a numbered rule more reliably than a paragraph of reasoning. Expect to review every phase: the rules prevent the expensive mistakes, and the review catches the rest.