How to use Google Sheets as a database
A complete, honest guide to using Google Sheets as a database - what it's actually good for, how to set it up correctly, and when to switch to something else.
The short answer
Yes. Google Sheets can serve as a database. The first row of each tab defines your field names. Every row below it is a record. Your entire data model lives in a spreadsheet your team already knows how to use.
It is not a replacement for PostgreSQL. But for datasets under ~50,000 rows with low-to-medium write frequency, it holds up well - and it comes with a collaboration layer, version history, and zero infrastructure cost that no "real" database can match out of the box.
The missing piece is an HTTP API. That's what GKit SheetsAPI adds: it turns any Google Sheet into a REST endpoint your frontend can call with plain fetch(). Free in beta, MIT-licensed, runs on Cloudflare Workers, CORS-enabled.
Step 1: Structure your Sheet correctly
Before you write a line of code, get the spreadsheet structure right. Bad structure causes bugs that are hard to trace later.
Headers go in row 1. Use lowercase names with underscores instead of spaces: product_name, not Product Name. SheetsAPI maps these directly to JSON field names, so spaces become awkward keys.
| product_name | price | in_stock | created_at |
|--------------|-------|----------|---------------------|
| Mug | 12 | true | 2026-06-01T09:00:00 |
| Notebook | 8 | true | 2026-06-02T11:30:00 |
Rules that prevent problems later:
- No merged cells - they break row parsing entirely
- No formula-based columns in your data range - formulas don't survive a PUT or DELETE
- One entity type per tab - don't mix products and orders in the same sheet
- A
created_atcolumn in ISO 8601 format - gives you reliable ordering and makes migration clean - Consistent types per column - don't mix
"12",12, and"$12.00"in the same price column
Step 2: Connect to SheetsAPI
Sign in with Google at GKit, select the spreadsheet, and you get a live endpoint URL in under 30 seconds. No Cloud Console, no OAuth client setup, no service account key JSON.
Your base URL looks like this:
https://sheetsapi.gkit.mreshank.com/api/spreadsheets/YOUR_USER_KEY/YOUR_SHEET_NAME
The YOUR_SHEET_NAME segment is the tab name (lowercase, URL-encoded if it has spaces - another reason to keep tab names simple).
Step 3: Query your Sheet like a database
Read all records
const BASE = "https://sheetsapi.gkit.mreshank.com/api";
const KEY = "YOUR_USER_KEY";
const res = await fetch(`${BASE}/spreadsheets/${KEY}/products`);
const rows = await res.json();
// [{ product_name: "Mug", price: "12", in_stock: "true", ... }, ...]Filter records
// Only in-stock products
const res = await fetch(`${BASE}/spreadsheets/${KEY}/products?search=in_stock:true`);
// Exact match
const res2 = await fetch(`${BASE}/spreadsheets/${KEY}/products?search_exact=status:active`);Sort and paginate
// Cheapest first, 10 per page
const res = await fetch(`${BASE}/spreadsheets/${KEY}/products?sort=price&limit=10&offset=0`);
// Newest first
const res2 = await fetch(`${BASE}/spreadsheets/${KEY}/products?sort=-created_at&limit=20`);Create a record
await fetch(`${BASE}/spreadsheets/${KEY}/products`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
product_name: "Tote Bag",
price: "22",
in_stock: "true",
created_at: new Date().toISOString(),
}),
});Update a record
Rows are addressed by their 1-based position in the sheet (row 1 is the first data row, not the header).
// Update row 3
await fetch(`${BASE}/spreadsheets/${KEY}/products/3`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ price: "20" }),
});Delete a record
await fetch(`${BASE}/spreadsheets/${KEY}/products/3`, {
method: "DELETE",
});That's the full CRUD set: list, read one, create, update, delete - all against a Google Sheet, straight from the browser.
What makes a good Sheets database
Some datasets are a natural fit. Others aren't.
Good candidates:
- Simple tabular data with one clear entity per tab
- Data that non-technical teammates need to edit directly
- Datasets that change infrequently (reads heavily outnumber writes)
- A
created_atorupdated_atcolumn for ordering - Column values that are strings, numbers, or booleans - no nested objects
Warning signs:
- Multiple entity types that need to reference each other (you want joins - Sheets has none)
- Concurrent writers (Sheets processes one write at a time; parallel writes queue and can conflict)
- Columns with mixed types or irregular formatting
- Formulas that live in the data range
Real use cases where this works well
These are the patterns where the Sheets database approach holds up in production:
| Use case | Why it fits |
|---|---|
| Product catalog (<1,000 items) | Marketing edits prices directly; reads are fast |
| Blog post list | CMS-lite: draft posts stay in the sheet, published = status:published |
| Team directory | HR updates it; the website reads it |
| Event registrations | Form submissions POST rows; the sheet is the attendee list |
| Waitlist | Email + name, append-only, trivial to export |
| Feature flags / config | Engineering sets values; non-engineers can toggle them |
| FAQ content | Content team edits answers; the site fetches on each request |
| Changelog entries | Each release is a row; the site renders the table |
For more patterns, see the SheetsAPI use cases page and the no-code backend use case.
The limits you will actually hit
No honest guide skips this section.
Concurrent writes. Google Sheets processes one write at a time. Under load, writes queue. If your app has multiple users submitting forms simultaneously, some requests will be delayed. For a public waitlist form getting a burst of submissions, this is usually fine. For a multi-user collaborative tool with frequent writes, it is not.
No joins or foreign keys. A Sheets database is flat. If you need to relate products to orders to customers, you are doing that logic in your application code. This gets messy fast.
Performance degrades past ~50,000 rows. The Google Sheets API slows down on large ranges. Reads start taking multiple seconds. There is no index to add.
No server-side aggregation. You cannot GROUP BY or SUM at the API layer. You pull the rows and aggregate in JavaScript. Fine for small datasets; impractical for large ones.
10 million cell cap per spreadsheet. A 100-column, 100,000-row sheet hits the cap. Most practical use cases stay well below this.
No real-time subscriptions. SheetsAPI is a request-response API. There is no WebSocket or long-poll interface. Polling works, but it is not a live database.
Comparison: Google Sheets + SheetsAPI vs the alternatives
| Sheets + SheetsAPI | Firebase | Supabase | PocketBase | Airtable | |
|---|---|---|---|---|---|
| Setup time | ~2 min | 10–20 min | 10–20 min | 5 min (self-host) | ~5 min |
| Non-tech editor friendly | Yes - it's a spreadsheet | No | No | No | Yes |
| Free tier | Free in beta | Generous free tier | Generous free tier | Free (self-hosted) | Row-limited free tier |
| SQL | No | No | Yes (Postgres) | No (REST/realtime) | No |
| Real-time | No | Yes | Yes | Yes | No |
| Scale ceiling | ~50k rows | Millions of docs | Millions of rows | Depends on host | Tens of thousands |
| Open source | MIT | No | Yes | Yes | No |
| Runs on edge | Yes (Cloudflare Workers) | No | No | No | No |
The honest read: if a non-technical teammate needs to edit the data, Sheets + SheetsAPI is the only option in this table that actually works. Everything else requires either a developer or a paid Airtable plan with its own learning curve.
If your team is all developers and you need real-time or SQL, Supabase is the better fit. If you need a quick personal project backend, PocketBase is worth a look. See the compare page for a fuller breakdown.
Migration path: when you outgrow Sheets
The good news is that a well-structured Sheets database migrates cleanly to Supabase, PlanetScale, or any Postgres-compatible database - if you set it up correctly from the start.
What makes migration easy:
- Consistent column names in
snake_case(they become SQL column names directly) - No merged cells (rows import as flat CSV without issues)
- A
slugoridcolumn for each record (gives you stable references) - ISO 8601 dates in
created_at/updated_at(parse without ambiguity) - String booleans like
"true"/"false"consistently applied (cast toBOOLEANin one step)
What makes migration painful:
- Mixed types in a column (
"12",12,"N/A", empty) - Columns with display-only values (e.g.
"$12.00"instead of"12") - Data spread across multiple tabs with informal relationships
- Formulas that produce values - those values don't export as data
The migration process itself is straightforward: export the sheet as CSV, import into your target database, and update your API calls from the SheetsAPI endpoint to the new one. If you structured the sheet cleanly, the column names map directly to SQL columns with no transformation.
Get started
Connect your first Sheet at GKit. It takes about 2 minutes: sign in with Google, select a spreadsheet, and you have a live REST endpoint.
The full endpoint reference is in the docs. For examples beyond the ones in this post, see the SheetsAPI product page and the use cases.
SheetsAPI is free while in beta. No credit card required.
Go deeper on a specific question
This post is the practical setup guide. These cover the decisions around it:
- Should you use a spreadsheet at all? The honest case for and against, focused on concurrency, joins and scale limits before you commit.
- Google Sheets vs a real database A side-by-side comparison with Postgres and friends, and which workloads belong on each.
- What developers actually argue about The recurring r/webdev debate and where it lands, plus real-world patterns from people doing this for years.
- Google Sheets as a CMS The content-specific variant, where non-technical editors own the data.
- Query rows with SELECT and WHERE Filtering inside the spreadsheet with
QUERY, and the equivalent API parameters.
Frequently asked questions
Can you use Google Sheets as a database?
Yes, for the right workload. The first row of each tab defines your field names and every row below is a record. It works well for datasets under roughly 50,000 rows that are read more often than written, and it gives you a collaborative editing UI and version history for free. It is not a substitute for a relational database under heavy concurrent writes.
How many rows can Google Sheets handle as a database?
A spreadsheet caps at 10 million cells across all tabs, which is about 200,000 rows at 50 columns each. In practice performance and API latency degrade well before that, so plan to migrate somewhere around 50,000 rows.
How do I connect a Google Sheet to my website as a database?
You need an HTTP layer in front of the sheet, because credentials cannot live in browser JavaScript. Either build a small backend that calls the Google Sheets API, or use a service that exposes the sheet as a CORS-enabled REST endpoint your frontend can call with fetch directly.
Is using Google Sheets as a database safe for production?
It is safe for low-write, non-critical data such as configuration, content, directories and form submissions. Avoid it where you need transactions, row-level locking, referential integrity or guaranteed write ordering, because concurrent writes to the same row can overwrite each other.
What should I use instead when Google Sheets stops being enough?
Postgres through Supabase or Neon is the usual next step. A sheet structured with snake_case headers, one type per column, a stable id column and ISO 8601 dates migrates cleanly with a CSV export and import.