Serialized writes. Idempotent retries. Typed TypeScript SDKs, generated live from your sheet's headers.
getsheetforge.vercel.app — one-click Google sign-in, no infra required. The MIT-licensed OSS core (
packages/queue,packages/codegen,packages/sdk-ts) stays free forever, and self-hosting is fully supported if you'd rather run your own stack. See Quickstart below for both paths.
TL;DR. The Google Sheets API drops rows under concurrent writes. Every "Sheets as a backend" wrapper (SheetDB, Sheety, NoCodeAPI) forwards your POST straight to
values.appendand inherits the bug. sheetforge wraps every write in a per-sheet queue fenced by a Postgres advisory lock: 50 parallel writes, 50 ordered rows, retry-safe by key. And because your header row is the schema, you get a typed TypeScript SDK you can commit alongside the call sites.
|
Raw Google Sheets API await Promise.all([
sheets.values.append({ ...a }),
sheets.values.append({ ...b }),
sheets.values.append({ ...c }),
sheets.values.append({ ...d }),
]);
// 4 POSTs, 3 rows.
// one silently clobbered. |
sheetforge await Promise.all([
sheetforge.rows.insert(a, { idempotencyKey: '1' }),
sheetforge.rows.insert(b, { idempotencyKey: '2' }),
sheetforge.rows.insert(c, { idempotencyKey: '3' }),
sheetforge.rows.insert(d, { idempotencyKey: '4' }),
]);
// 4 POSTs, 4 rows, in order.
// retry-safe by key. |
values.append isn't serializable. Two concurrent appends can resolve to the same target row and one silently overwrites the other. It's a documented Google bug whose upstream workaround boils down to "don't write concurrently." Fine for a demo. Not fine when your signup form catches an HN spike.
| Surface | Status |
|---|---|
| Hosted SaaS (sign up, skip local infra) | ✅ live at getsheetforge.vercel.app |
| Dashboard + API (write-queue, SDK codegen) | ✅ live (also runs locally via pnpm dev) |
| Self-host | ✅ supported — see quickstart |
OSS core (packages/queue, packages/codegen, packages/sdk-ts) |
✅ MIT, free forever |
- Go to getsheetforge.vercel.app
- Sign in with Google
- Connect a Sheet
git clone https://github.com/Devansh-365/sheetforge.git
cd sheetforge
pnpm install
cp .env.example .env # fill in Google OAuth + DATABASE_URL
pnpm db:push # apply migrations (works against Neon too)
pnpm dev # web :3000, api + processor :3001Prereqs. Node 20+, pnpm 9+, Postgres 14+, and either local Redis on :6379 or an Upstash REST endpoint (UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN).
What you do in the dashboard
- Open
http://localhost:3000and click Dashboard. Google OAuth consent with the Sheets scope. - Land on
/app. Click + new project, then + create key, then copy thesk_live_…value (shown once). - Click + connect sheet. Paste a Google Sheets URL. Pick a tab. The header row becomes your schema.
- On the sheet page, click ↓ download client.ts. That is your typed SDK.
.env.example (full list)
PORT=3001
PUBLIC_BASE_URL=http://localhost:3001
WEB_BASE_URL=http://localhost:3000
ALLOWED_WEB_ORIGINS= # optional, commas. e.g. preview deploys
DATABASE_URL=postgres://… # local or Neon
REDIS_URL=redis://localhost:6379 # either this…
UPSTASH_REDIS_REST_URL= # …or these two
UPSTASH_REDIS_REST_TOKEN=
GOOGLE_OAUTH_CLIENT_ID=
GOOGLE_OAUTH_CLIENT_SECRET=
GOOGLE_OAUTH_REDIRECT_URL=http://localhost:3001/v1/oauth/callback
SESSION_JWT_SECRET= # openssl rand -hex 32
PROCESSOR_ENABLED=true
PROCESSOR_TICK_MS=1000|
REST curl -X POST \
http://localhost:3001/v1/sheets/$SHEET_ID/rows \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"email":"hi@example.com","plan":"free"}'Returns |
Typed SDK (generated from your headers) import { createClient } from './sheetforge-client';
const sheet = createClient({
apiKey: process.env.SHEETFORGE_API_KEY!,
sheetId: 'sht_…',
});
await sheet.rows.insert(
{ email: 'hi@example.com', plan: 'free' },
{ idempotencyKey: crypto.randomUUID() },
);
|
|
One worker per sheet at a time. Ordering guaranteed by a Postgres advisory lock, not a token protocol or a lease clock. |
Every endpoint accepts |
Point at a sheet, get a typed TypeScript client. Literal unions inferred from sample cells. The compiler catches drift. |
|
Redis Streams with consumer groups. The processor blocks on |
Handler runs inside a Postgres transaction. |
No Docker required for local dev — one Postgres URL, one Redis URL (or Upstash REST). Next.js and Hono, both in one |
submitWrite() is the only ingress. It inserts a ledger row in Postgres (with a partial unique index on (sheet_id, idempotency_key) so retries dedupe), then XADDs an envelope to the sheet's Redis stream. The processor runs processNext() in a loop: XREADGROUP on the consumer group, take a Postgres advisory lock keyed by hashtextextended(streamKey, 0) inside a transaction, run the handler, XACK only after commit. A crash mid-handler rolls the transaction back, the PEL redelivers, and the idempotency key catches the replay. The lock is the fence. No token protocol. No lease clock skew. No leader election.
Touching slices/write-queue/ or packages/queue/? Every change lands with a concurrency test. That is the rule I do not break.
| sheetforge | SheetDB | Sheety | Raw Sheets API | |
|---|---|---|---|---|
| Concurrent-write safety | yes, always | no | no | no |
| Idempotent retries | yes | no | no | no |
| Typed SDK from live schema | TypeScript (V0) | none | none | none |
| Open source | yes (MIT packages) | no | no | N/A |
| Self-host | yes | no | no | N/A |
| Webhooks on write | V2 | yes | no | no |
| Free local dev | yes | trial | trial | yes |
| Python SDK | V1 | no | no | no |
If you need webhooks today, use SheetDB. If you need your rows to land, come back.
apps/
web/ Next.js 15 dashboard and marketing
api/ Hono on @hono/node-server plus inline processor
worker/ dedicated queue consumer (V1)
slices/ feature slices. barrel is the only public API
auth/ Google OAuth, sessions, refresh tokens
projects/ project and API-key CRUD
sheets/ sheet connect, schema infer, cached reads
schema/ header row to Zod schema derivation
write-queue/ submitWrite, processNext, advisory-lock fencing
rest-api/ Hono routes, CORS, idempotency, error boundary
sdk-codegen/ schema to TypeScript client
demo/ local-only hammer demo
packages/ MIT, npm-publishable, no shared/ imports
queue/ generic Redis-Streams consumer
codegen/ schema to typed-SDK engine
sdk-ts/ runtime for generated clients
shared/ infra clients, no business logic
db/ Drizzle schema and migrations
redis/ ioredis and Upstash REST adapters
google/ Sheets client and A1 quoting
logger/ pino
types/ typed DomainError hierarchy
Slice internals stay private. Cross-slice imports fail CI. packages/* stays OSS-safe with no shared/* imports, no secrets, publishable to npm tomorrow.
V0, shipping now
- Google OAuth and session management
- Sheet connect with header-row schema inference
- Per-sheet write queue (Redis Streams + pg advisory lock)
- Idempotency by key, partial unique index
- Upstash REST adapter (Workers-ready)
- TypeScript SDK download
V1
- Hammer demo on the hosted landing page
- Dedicated worker process (currently inline with API)
- Python SDK generator
- Write-status webhooks
- Per-API-key rate limits
- Richer cell types (dates, enums from data validation)
V2
- Sub-10ms cached reads via stream-tailing CDC
- Per-cell audit log
- Embeddable signup-form widget
- Team workspaces
- Cloudflare Workers deploy target
See the good-first-queue-hack and sdk-codegen labels if you want to help pull V1 in.
Most "Sheets as a backend" tools wrap a broken primitive. sheetforge fixes the primitive, then hands you a typed client on the way out. If you've ever shipped a form on Sheets and watched rows vanish mid-launch, a star is the cheapest vote for correctness you can cast.
Before sending a PR:
- Read the root
CLAUDE.mdfor slice boundaries and commit hygiene. - Changes to
slices/write-queue/orpackages/queue/need a concurrency test. No exceptions. packages/*stays OSS-safe. Noshared/*, noapps/*, no secrets.- Zod schemas are the source of truth.
z.infer<>the types. Do not hand-write duplicates. - Run
pnpm lint && pnpm typecheckbefore pushing.
Issues and discussions are open. I answer them.
MIT. Clone, fork, self-host, ship — no strings. The hosted SaaS runs on the same MIT core; the managed service is the product, the code stays free.