
A stack for solo developers and early startups: build small, keep operations light
How to choose technology to build and run with a small team, using the setup of a real web app (React Router, Cloudflare Workers, D1, Clerk) as the example.
A small team should choose its stack for light operations, not raw speed. Own no servers, delegate authentication, automate deployment, and keep a human check only for production. These four keep a small team going for a long time.
How a small team should choose technology
Big companies start from “will it survive thousands of users?” Solo developers and newly founded startups have different problems. There are few people, little time, and when something breaks at night, you fix it yourself.
This article uses the setup of a small web app that is actually running, PAIBOT.app, to show how to choose a stack you can build and keep operating with few people. Five criteria guide the choices.
- Own no servers: do not carry OS updates, reboots or disk worries yourself
- Low fixed cost: you can start in a form that costs almost nothing when nobody uses it
- Delegate what you can: hand things that are dangerous to get wrong, such as authentication, payments and email, to specialist services
- Automated tests and deployment: the fewer people you are, the more time manual checks waste
- Stay movable: do not tie yourself to one company more than you must
This setup is one example, not the answer. Service features, prices and free tiers change, so check each company's official documentation before you use it.
The overall setup
PAIBOT.app is a web app of small browser tools with login and favorites added. Its setup is:
- Framework: React Router (framework mode). Pages are built on the server (SSR), and run as interactive parts in the browser
- Runtime: Cloudflare Workers. There is no server of your own; the code runs at sites around the world for each request
- Database: Cloudflare D1 (SQLite-compatible), with Drizzle as the ORM
- Authentication: Clerk. You build neither the login screen nor password storage
- AI: call Workers AI through a binding, so you need not hold an API key for an outside AI service
- Rate limiting: limit the number of requests per IP with Workers' rate limiting feature
Choices by layer, and why
| Layer | Choice | Why |
|---|---|---|
| Pages | React Router (SSR) | Pages that search engines can read are built on the server. Routing and data loading come together in one mechanism |
| Runtime | Cloudflare Workers | No server operations. You can start in a form that costs almost nothing while unused |
| Data | D1 + Drizzle | No separate database server to run. You write typed queries. Schema changes are managed with migrations |
| Authentication | Clerk | Authentication is easy to get wrong and costly when wrong. Delegate it to a specialist service |
| i18n | use-intl | Japanese, English and Chinese are managed in message files |
| Tests | Node's built-in test runner + Playwright | Light unit tests for calculations and the like, and one browser flow per screen |
| Config management | Wrangler + Terraform | Never manage the same thing in two places. Wrangler for what is close to the app, Terraform for account-wide settings |
The reason is not “Workers is fast” or “D1 is cheap” but that they need little operational effort.
A tip for growth: one tool, one folder
So that things do not get messy as tools multiply, each new tool keeps all its own parts in one folder. The screen, logic, unit tests and browser test live together, and registering it in one shared list is enough for it to appear in the top-page search and the sitemap.
- Run browser tests only for the tools a change can affect (all of them when shared code changes)
- Shared checks (opens in three languages, no errors, fits a phone width) apply to every tool automatically, with no test code
- Environment-specific code (Cloudflare-only features) is confined to server-only files
The last point serves “stay movable”. Keeping the environment-dependent part a thin layer keeps the impact small if you change the runtime.
The deployment flow
On a small team, the more you deploy by hand, the more mistakes you make. PAIBOT.app works like this:
- For every change (PR), type checks, unit tests, the build and browser tests run automatically
- When a change lands on main, it is deployed to the development environment automatically. The development environment is protected with Cloudflare Access so only allowed people can open it
- Production is run by hand. The production procedure confirms that the change succeeded in development. If something is wrong, you can roll back to an earlier version by specifying a tag
- Database changes (migrations) are part of the production deployment procedure
Production is manual because “everything automatically reaches production” is frightening. The manual run itself is the human check. (At the time, the plan we used lacked the team approval feature, which is why it is shaped this way.)
Traps we actually hit
The setup was not like this from the start. The app was first built on Next.js, carried onto Cloudflare Workers with a beta adapter. It worked, but it was a combination with few precedents, and we got stuck several times at the joins: how the authentication library was loaded, the internationalization library loaded twice, and so on. In the end we moved to React Router, which runs naturally on Workers.
What this shows:
- Pick well-trodden combinations for production: with a beta part in between, you cannot tell whether a bug is in your code or in the part
- When stuck, suspect a duplicated module: if the same library is loaded by two paths, one copy's state is invisible to the other, and the error shows up inside the library
- Keep it movable: because the boundaries were thin, it was a migration, not a rewrite
What changes between solo dev and an early startup
| Aspect | Solo developer | Early-stage startup |
|---|---|---|
| What matters | Being able to keep building | Speed of validation, and fixing things later |
| Authentication | Delegate (do not build it) | Delegate. Check early whether enterprise login (SSO) is needed |
| Database | Start small and keep it in one place | Gather database access in one place, expecting a future move |
| Cost | Start on free tiers | Estimate where the free tier runs out, and the price after it |
| Monitoring | Just enough to see errors | Decide who is notified of failures and who responds |
| When people join | Hardly a concern | Automated tests and deployment are what let you add people |
Cautions when choosing
- Dependence on a service: the more convenient the feature, the more you lean on that service. Keep the entrance to the dependency in a thin layer so you can move
- Free-tier traps: even a large free tier has conditions and limits that change. Look early at the price when your usage grows
- Where data lives: where user data is stored (region, regulations) is hard to change later. Decide early
- What you cannot delegate: even if a service handles it, designing permissions (who can see what) remains your job
Checklist
- Is your setup free of OS and server management by you?
- Have you avoided building authentication or payments yourself?
- Do tests run automatically for every change?
- Is there a human check before production, and a way to roll back?
- Are database changes recorded and managed?
- Is environment-dependent code gathered in one place?
- Have you looked up the free-tier limits and the price beyond them?
- Have you decided who notices and who fixes a failure?