A zero-cost backend architecture that actually holds
Twenty-three products, no servers, no monthly bill. Here is what the constraint removes, what it forces you to design properly, and the day a free tierβ¦
Every product I run costs me nothing to operate. Not "cheap" β nothing. Twenty-three of them are live across the web, Google Play and the extension stores, and the monthly infrastructure bill is zero. That is a constraint I chose, and it has shaped more of my architecture than any framework decision I have ever made.
This is what it actually removes, what it forces you to get right, and the day it went wrong.
The rule is about cost to me, not about the product being free
Worth separating these two, because people collapse them constantly. My products have paid tiers. What they do not have is a server I rent, a per-seat SaaS bill, or a metered API that charges me while somebody browses. The constraint is on my side of the line.
In practice the allowed list is short: client-side processing through browser APIs, a database free tier, a self-hosted platform I already run, and free npm packages with real adoption behind them. The forbidden list is shorter and more useful: anything with usage-based billing, anything that needs a VPS, and any service whose free tier is a trial.
The test I apply before adding anything: if this product got popular tomorrow, does my bill change? If the answer is yes, it is not free β it is deferred.
What the constraint removes, honestly
It is not free of cost, it is free of money cost. You pay in other currencies:
- Background jobs. No cron server means anything scheduled either runs in the client when someone opens the app, or runs on a free scheduled worker with a tight time budget. Several features I would have written as a nightly job became "resolve it on read" instead.
- Heavy computation. Nothing that needs sustained CPU. Image processing, PDF generation and export all moved into the browser, which is genuinely better for privacy and genuinely worse for a ten-year-old phone.
- Comfortable quotas. This is the one that bites, and I will come back to it.
What it does not remove, and I want to be exact here, is capability. Authentication, a real relational database with row-level security, file storage, transactional email, push notifications, analytics and error tracking are all available at no cost if you are willing to pick the specific provider that offers them rather than the one with the nicest marketing site.
The stack, and why each piece is there
There are two database answers, not one, and which I pick depends on whether the product needs real SQL.
Firebase's free tier β Firestore, Auth, Hosting β is the right answer for a product whose reads are mostly "give me this document" and "give me this list". It is generous, the client SDK is good, and Hosting is genuinely free for a portfolio-scale site. What I never use is Cloud Functions or Cloud Storage: both need the paid plan, and the moment you enable it you have a billing account attached to a project that can, in principle, run up a bill.
Supabase's free tier is the answer when I need joins, full-text search, or constraints the database enforces rather than the client. Postgres with row-level security is a different class of tool from a document store, and text search is the specific thing Firestore cannot do at all. Its Edge Functions cover the compute I would otherwise have needed a server for.
Storage and outbound email both go to FilesHub, which is my own self-hosted Laravel platform. That is the piece people usually push back on β it is a server, so surely it costs something? It does, but it is one platform serving an entire portfolio of products rather than a per-product line item, and building it once removed both Firebase Storage and every transactional-email SaaS from every project I run.
Where a product has no backend of its own, a Cloudflare Worker fills the gap. One shared account, every secret prefixed with the project it belongs to, every worker named for what it does. Where a product does have a backend β a Supabase project, or FilesHub itself β the work goes there. Adding a Worker in front of a backend that could already do the job is a second moving part earning nothing.
The day the free tier stopped being free
Firestore's free tier allows 50,000 document reads a day. That sounds enormous until you write one query without a limit.
On ImtehanHub β a bilingual exam-prep platform β a listing screen fetched a collection to filter it in the browser. It worked in development, where the collection had a few dozen documents. In production, with real content, every visit to that screen burned through hundreds of reads, and one afternoon the daily quota was gone. The app did not degrade. It stopped, for every real user, until the quota reset at midnight UTC.
Nothing caught it. It typechecked, it linted, it built, and it passed review, because the code was correct β it just fetched more than it needed. That is the failure mode of a quota: your bug is invisible until it is an outage.
What I do now, in every project, without exception:
- Every list read is paginated, default twenty, hard maximum fifty. Not as a guideline β as a helper function the read cannot be written without.
- The database filters, sorts and counts. Never the client. A count is a
head request that returns a number and zero rows, not a list I measure the length of.
- A query with no explicit limit is a defect even on a table with four rows, because tables grow and nobody revisits the query that was fine in 2024.
export const listRange = (limit: number, page = 0) => {
const size = Math.max(1, Math.min(Math.trunc(limit) || 20, 50));
const from = page * size;
return { from, to: from + size - 1 };
};
The helper matters more than the rule. A rule is something you remember; a required argument with a clamped ceiling is something the compiler remembers for you. Making the limit impossible to omit turned a production incident into a TypeScript error.
What I would tell someone starting this
Pick the constraint deliberately or not at all. Half-committing gives you the worst of both: the awkwardness of the free tier and a bill anyway.
Then design as though the quota is the spec, because it is. Read budgets, pagination and server-side filtering are all things a well-built application does anyway β the free tier just makes skipping them expensive immediately instead of eventually. I have shipped more careful data layers because of this constraint than I ever did when a bigger instance was one click away.
And keep every project movable. The database ref and URL live in that app's configuration and nowhere else, so pointing a product at a different project β or eventually a paid one, once it earns β is a config change rather than a code change. The constraint is a starting position, not a vow.
https://aoneahsan.com/blog/zero-cost-backend-architecture