Using Google Sheets as a no-code backend: when it works and when it doesn't
Google Sheets as a backend is a real pattern used by indie devs, agencies, and internal tools teams. Here's an honest look at when it's the right call - and when you'll regret it.
The pattern that keeps working
An indie developer launches a side project. They need a data store - maybe a product catalog, a waitlist, a set of FAQ entries the client will edit. They reach for Google Sheets.
It sounds like a hack. But there's a reason this pattern shows up constantly on r/SideProject, r/webdev, and r/indiegaming: it works, within a specific envelope. A spreadsheet is free, zero-ops, and already has a UI your client or non-technical co-founder can use without training or support tickets.
The missing piece has always been the HTTP interface. Sheets has no built-in REST API suitable for a web app. This post is about what options exist, what they each cost you, where the pattern genuinely holds up, and where you'll regret using it.
Real use cases where this approach earns its place
Before getting into the wiring, it's worth being specific about where Sheets-as-backend is actually a good fit - not just a tolerable one.
Waitlists and signups. Low write volume, basically no reads from the app side. The data lives in a sheet where you or your co-founder can see exactly who signed up, annotate rows, and export to anything. This is a textbook case.
Small product catalogs. Under a few hundred items, updated a few times a week by a non-engineer. A sheet with columns for name, price, description, and a category slug is a perfectly functional CMS. A buyer, a copywriter, or an ops manager can own it without ever filing a ticket.
Internal dashboards and tooling. Ops teams tracking vendor SLAs, a finance team managing budget line items, an HR team with training records. If the users updating the data already live in Sheets, adding a REST layer is strictly additive.
Landing page content management. Hero headline, feature bullets, testimonials, pricing tiers - stored in a sheet, rendered by a static site. Non-technical editors can ship copy changes without a deploy. Agencies build client sites this way specifically because it removes the client's dependency on a developer for content updates.
Event registrations and feedback collectors. A conference registration form, an internal NPS survey, a feedback widget on a product. Write-once, read-rarely. The sheet is the admin panel.
Feature flags and config data. A row per flag, a boolean column, a description column. A product manager can toggle features without touching code or a deployment pipeline.
What all of these have in common: moderate data volumes, infrequent or low-concurrency writes, and a clear benefit to non-engineers owning the data directly.
The missing HTTP layer: your actual options
Google Sheets does not expose a CORS-enabled REST API that a browser can call directly. Here are the approaches that exist, and what they cost.
CSV export URL. Every sheet has a published CSV URL you can fetch. It's read-only, it breaks on re-publish, it returns a flat text format you have to parse, and there's no way to filter or paginate server-side. Fine for a truly static data source you control. Not suitable for anything users write to.
Apps Script doGet / doPost. You can publish a Web App from a Google Apps Script attached to the sheet. It works, but the developer experience is rough: cold starts of 2–5 seconds on the first request, a deployment model that requires manually versioning and re-publishing on every change, awkward auth, and Google's per-user quotas apply. r/googleappsscript threads regularly surface people hitting quota walls or dealing with stale deployments.
Google Sheets API directly. The real API is thorough but complex. OAuth 2.0 service account setup, credential management, a non-trivial request schema for reading ranges in A1 notation. Appropriate if you're already deep in a Google Cloud project. Overkill if you want to fetch a list of rows.
SheetsAPI. A purpose-built REST layer that sits in front of your sheet. Connect a sheet, get a URL, call it with plain fetch(). CORS is enabled by default. Runs on Cloudflare Workers at the edge, so there's no cold start problem. Free in beta, MIT-licensed, open source. It's the option this post uses in examples.
Reading and writing rows in practice
Here's what calling SheetsAPI looks like for the common cases. The base URL is https://sheetsapi.gkit.mreshank.com/api.
Fetching a product list in vanilla JS:
async function getProducts() {
const res = await fetch("https://sheetsapi.gkit.mreshank.com/api/spreadsheets/YOUR_KEY/products");
if (!res.ok) throw new Error(`Sheets error: ${res.status}`);
return res.json();
}The same call with filtering, sorting, and pagination:
const url = new URL("https://sheetsapi.gkit.mreshank.com/api/spreadsheets/YOUR_KEY/products");
url.searchParams.set("search", "inStock:true");
url.searchParams.set("sort", "price");
url.searchParams.set("fields", "name,price,category");
url.searchParams.set("limit", "20");
url.searchParams.set("offset", "0");
const products = await fetch(url).then((r) => r.json());Rendering in React:
import { useEffect, useState } from "react";
const BASE = "https://sheetsapi.gkit.mreshank.com/api";
export function ProductList({ userKey }) {
const [items, setItems] = useState([]);
useEffect(() => {
fetch(`${BASE}/spreadsheets/${userKey}/products?sort=price&limit=50`)
.then((r) => r.json())
.then(setItems);
}, [userKey]);
return (
<ul>
{items.map((item, i) => (
<li key={i}>
{item.name} - ${item.price}
</li>
))}
</ul>
);
}Appending a row (waitlist signup):
async function addToWaitlist(email, name) {
const res = await fetch(
"https://sheetsapi.gkit.mreshank.com/api/spreadsheets/YOUR_KEY/waitlist",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email,
name,
signedUpAt: new Date().toISOString(),
}),
},
);
return res.json();
}The sheet's first row defines the column names. Whatever fields you POST need to match those headers. That's the whole data model.
For the full API reference, see the docs.
The limits you will actually hit
Using this pattern honestly means knowing where it breaks.
Concurrent writes. Sheets locks the entire spreadsheet during a write operation. If two users submit a form at the exact same moment, one of them gets a 429 or a retry. For a waitlist with bursty signups, this is fine. For a checkout flow on a product with real demand, it is not.
No transactions. There is no atomicity. If you need to update two rows together as a unit - say, decrement inventory and log a sale - you cannot do that safely. Either write succeeds, or the sheet is in a half-updated state.
No server-side filtering on large datasets. SheetsAPI can filter the results it returns, but it has to read the whole sheet first. At 10,000 rows this is fast. At 200,000 rows it starts to matter. You will not outrun this with pagination alone.
Rate limits. The Google Sheets API allows roughly 100 read requests per second at the project level, and 60 write requests per minute per user. SheetsAPI batches and caches where it can, but high-traffic production apps will brush against this.
10 million cell cap. A single spreadsheet maxes out at 10 million cells. For most small projects this is a non-issue - a sheet with 100 columns and 10,000 rows uses 1 million cells. But it's a hard ceiling, not a soft one.
No relational queries. You can't join two sheets, enforce foreign key relationships, or run an aggregation query. If your data model starts growing inter-table relationships, you're at the edge of where Sheets belongs.
Decision table: which tool for which job
| Sheets + SheetsAPI | Airtable | Supabase | PocketBase | |
|---|---|---|---|---|
| Setup time | Under 5 min | Under 10 min | ~20 min | ~30 min (self-host) |
| Non-tech editor | Familiar UI, zero training | Good native UI | Requires custom admin | Requires custom admin |
| Cost | Free (beta) | Free tier, then $20+/mo | Free tier, then $25+/mo | Free (self-hosted) |
| Scale ceiling | ~50k rows comfortably | ~125k rows | Postgres - effectively unlimited | SQLite - large |
| SQL queries | No | No | Yes | No (but has REST filters) |
| Real-time | No | No | Yes (subscriptions) | Yes (SSE) |
| Concurrent writes | Weak | Moderate | Strong | Strong |
If you need real-time updates or transactional writes, Supabase or PocketBase are the right answer from the start. If you need a non-technical team member owning data in a familiar UI with zero onboarding, Sheets wins that comparison. Airtable sits in between - better for structured relational data with a non-tech editor, but it costs money and adds a vendor dependency.
The compare page covers some of these trade-offs from a different angle.
Start with Sheets, migrate later
The "start with Sheets" strategy works well when you structure the data for portability from day one. A few rules that make migration easy when you outgrow it:
One entity type per tab. Don't mix products and orders in the same sheet. Keep each logical table separate. When you migrate, each tab maps to a database table with minimal transformation.
Use consistent column names. created_at not Date Added, product_id not Product ID. Snake case throughout. Your future self writing a migration script will appreciate it.
Store IDs explicitly. Add an id column and populate it - even a simple incrementing number or a UUID you paste in. Don't rely on the row number as your primary key; row numbers shift when you delete rows, and they mean nothing in a database.
Avoid formula columns in app-consumed data. Formulas that compute values from other columns break in unpredictable ways when you export or migrate. If you need a computed field in the app, compute it in the code reading the data.
Keep the schema shallow. Avoid nested structures - don't store JSON strings in a cell, don't pack multiple values into one column with a delimiter. Flat rows with explicit columns export cleanly to SQL.
When you're ready to migrate, the path is straightforward: export to CSV, import to your target database, update the fetch URLs. If you've kept the column names consistent, the rest of the app doesn't need to change. The use cases page has examples of teams who've done exactly this.
The honest answer
Google Sheets as a backend is not an anti-pattern. It is a pattern with a specific scope. For solo developers, agencies, and internal tools teams, the combination of a familiar editing UI, zero infrastructure, and a simple HTTP layer covers a large class of real problems.
The failure mode is using it past that scope - treating it as a production database for high-concurrency, transactional, or large-scale workloads. It is not built for that, and it will show.
If your project fits the envelope - small dataset, low concurrent writes, a non-technical editor who benefits from owning the data - then starting with Sheets is a legitimate choice, not a compromise. Build it, ship it, validate the idea. You can always migrate when the success problem arrives.
If you want to try the approach without any of the Apps Script or OAuth complexity, SheetsAPI is free while in beta. Connect a sheet, get a URL, start fetching. Takes about three minutes.
Get started - or read how other teams are using it first.