How to build an Airtable alternative
How to build an Airtable alternative as typed fields, one table, and a CSV import that writes nothing until you confirm the rows. Prompts inside.
How to build an Airtable alternative is one fixed set of fields, a table of records, and a CSV import that previews before it writes. Airtable is that product. The catalog line is “One opinionated workflow beats a blank database builder.”
You pay $24–$54 a person a month. The card scales with the team. The first estimate is 3 days, 24 hours. The verdict is “Build the workflow, not Airtable”.
The Airtable post is the teardown. It covers the in-memory base, formulas, and live views. This guide builds the 3-day row, with the screens and the prompts in order.
A wiki of markdown pages is the Notion guide, at $12–$24 a person a month, about 10 hours. A board of cards is the Trello guide, at $10–$17.50 a person a month, about 3 hours. An internal screen on data you already have is Retool, at $12–$65 a person a month, about 4 days. Tableflow is the shot list.
How to build an Airtable alternative as one table
The Airtable page splits the work into three rows.
The first row is 3 days. Typed fields, a table view, and a CSV import. This guide keeps that row. The catalog names the project Tableflow.
The second row is 2 weeks and up, 80 hours. Kanban views, comments, attachments, and simple automations. The third row is 3 months and up, 480 hours. A schema designer, formulas, linked bases, and an extension platform. The non-goal names arbitrary formulas, linked bases, extensions, and a schema designer.
If you tell an agent to “clone Airtable”, it aims at the third row. Paste the prompts below. One workflow. Fields declared in code. One table.
The example is a shot list. Shot shot-184 is “Kitchen scene, take 3”, status in-progress, owner Flavio, due 2026-10-24, 45 minutes. Status values are todo, in-progress, and done. Ada is the other known owner. A person named Sam is not in the list, so a CSV row for Sam fails the preview.
Where the table and the import split
Every product prompt ships in two flavors: Node.js plus Astro, and TanStack Start. The card lists Astro, D1, and R2. This site is an Astro app on Cloudflare, so the pages are Astro and the rows are D1. Files in R2 arrive with attachments, which are the 2-week row. Use TanStack Start if React is your weekday language. The stack post is which tab you copy. The prompt post is how you pin the first estimate.
The browser shows a table, a record page, and an import preview. Astro on the request validates every write against the field list, and it holds the import in memory until you confirm. D1 stores the records and a version number on each one. Nothing in this v1 waits on a queue. Two people saving the same shot resolve to one winner on that request.
TanStack Start keeps these screens, with server functions and a SQL database. Confirm is still one transaction.
What one shot carries
The card lists Record, FieldDefinition, View, Comment, and AutomationRule. This v1 uses the record and the field definitions. One table is the only view. Comments, attachments, and automation rules are the 2-week row.
The field list is code, not a screen you edit. Each field has a key, a type, and a required flag. Title is text. Status is one of todo, in-progress, and done. Owner is a person, and the only people are Flavio and Ada. Due date is a date. Minutes is a number, zero or more. Unknown keys are rejected, so a stray CSV column never becomes a field.
A record stores those values and a version integer. Creating shot-184 sets version 1. A save sends the version you loaded. If D1 already has a higher version, the save is refused and the form reloads the winner. The same check runs for the table’s status menu and the record page.
const shot = {
id: 'shot-184',
version: 2,
}
Overdue is not a field the user types. The table marks a row when the due date is before today and the status is not done. Shot-184 is due 2026-10-24, so on a later day it shows overdue while it stays in-progress. Marking it done clears the mark. The Airtable post puts that rule next to the validator, not in a formula string.
An import is a preview, then a confirm. The preview maps columns, checks every row, and writes nothing. Errors carry the source row number. Confirm inserts only the valid rows, in one transaction. A crash mid-confirm leaves the previous rows as they were.
The screens you ship
Three surfaces, all signed in. There is no public link in this v1.
The table is /shots. Columns are title, status, owner, due date, minutes, and an overdue mark. A status menu on the row uses the same save as the record page. Create opens a blank record. Import opens the preview. A filter for status is the only extra control. There is no column designer.
The record page is /shots/shot-184. It shows the five fields, the version, and Save. A rejected status such as blocked stays on the page with the field name. If someone else saved first, Save explains the conflict and reloads version 2 instead of overwriting it.
The import preview lists every source row. Row 2, “Kitchen scene, take 4”, owner Ada, due 2026-11-12, is valid. Row 3, owner Sam and status blocked, lists those errors with the row number. The summary says one row will be created and one will be skipped. Confirm is the only control that writes. Cancel leaves the table unchanged.
The path from a blank table to a confirmed row
Flavio creates shot-184. The table shows it as overdue because the due date is already past and the status is not done. He changes the status. The version becomes 2. A second tab that still has version 1 cannot overwrite that save.
He imports the two-row file. Cancel leaves a single shot. Confirm adds take 4 and leaves Night exterior out. The error text still names row 3.
Prompt 1. Fields in code and a table
Build Tableflow, a shot
tracker with one table.
Use Node.js and Astro.
Server-rendered pages.
Ordinary HTML forms.
Cloudflare D1 for the rows.
If you use TanStack Start
instead, use server functions
and a SQL database with
migrations. Same screens.
Declare the fields in code,
not in a designer. Title is
required text. Status is
todo, in-progress, or done.
Owner is Flavio or Ada.
Due date is a date. Minutes
is a number, zero or more.
Reject unknown keys.
The table at /shots shows
title, status, owner, due
date, minutes, and an
overdue mark. Overdue means
the due date is before today
and the status is not done.
A status filter is the only
extra view. There is no
kanban and no public page.
New shot creates a record
at version 1. The sample
is shot-184, title
"Kitchen scene, take 3",
status in-progress, owner
Flavio, due 2026-10-24,
minutes 45.
Every write calls the same
validator. A bad status
names the field and stores
nothing.
No formulas. No linked
records. No file uploads.
No comments.
Done when shot-184 appears
on the table with the
overdue mark, and a status
of blocked is refused.
What to check after the first shot
Create shot-184 and reload /shots. The title, owner, and 45 minutes are there, and the overdue mark is on because the due date is in the past.
Submit status blocked from the record page. The page names status, and the stored value is still in-progress.
Sign out and open /shots. You get the login wall, and the HTML does not include the shot title.
Prompt 2. A version on every save
Add a version check to
every save, including the
status menu on the table.
The form sends the version
it loaded. The update
matches that version and
increments it in the same
transaction. If the stored
version is newer, refuse
the save, keep the stored
shot, and reload the form
with the current values
and a conflict message.
Write one activity line
on success: actor, field,
old value, new value, and
time. The record page shows
the latest line. Do not
add a comment box.
The table status menu and
the record form call this
same save. An illegal
status fails the same way
from both.
Done when two saves of
shot-184 leave version 2,
and a stale version 1
save does not change the
title or the status.
What to check after a conflict
Open shot-184 in two tabs. In the first, set status to done and save. The overdue mark clears, and the version is 2.
In the second tab, change the title and save without reloading. The save is refused. Reload and confirm the title is still “Kitchen scene, take 3” and the status is done.
The activity line names the status change and the actor. There is still one shot row.
Prompt 3. A preview that writes nothing
Add CSV import with a
dry run and a confirm.
The preview asks for the
date format. This file
uses YYYY-MM-DD. Do not
guess another format.
Map columns to the field
keys. Validate every data
row with the same
validator as the form.
Show the source row number
on each error. The sample
file has a header on row 1.
Row 2 is "Kitchen scene,
take 4", status todo, owner
Ada, due 2026-11-12,
minutes 20. Row 3 is
"Night exterior", status
blocked, owner Sam, due
10/24/2026, minutes -5.
The summary says how many
rows will be created and
how many will be skipped.
Nothing is stored before
Confirm. Confirm inserts
only the valid rows in one
transaction. Cancel stores
nothing. A repeated confirm
of the same preview does
not insert row 2 twice.
Done when row 2 exists
once, row 3 does not, and
the error list still says
row 3.
What to check after confirm
Upload the file and read the preview before you press Confirm. The table still has only shot-184.
Press Cancel and reload /shots. Still one row. Run the preview again and press Confirm. Take 4 is there, owned by Ada, and Night exterior is absent.
Press Confirm on that same preview again. Take 4 is still one row. The error for row 3 still mentions the unknown owner and the status.
Failures the agent ships anyway
A second validator for the CSV
The form rejects blocked, and the importer stores it. Both paths have to call the field list in code. The Airtable post already says one validate function covers the form, the table, and the import.
A preview that inserts
You open the preview and the table already contains Night exterior. The dry run wrote as it parsed. Confirm should be the only insert, and Cancel should be a no-op you can prove by counting rows.
A guessed date
10/24/2026 becomes 24 October on one machine and 10 December on another, and the preview says the row is valid. The importer uses the format you picked, YYYY-MM-DD, and row 3 stays an error.
A stale tab that wins
Two people edit shot-184. The slower save lands last and the status you just set disappears. The version has to be in the update condition. The loser reloads the winner.
A formula box
The record page grows a formula field, a link to another table, or a button that adds a column. That is the month row. Overdue stays a mark computed from the due date and the status. The non-goal names formulas, linked bases, extensions, and a schema designer.
A kanban that copies the rows
Drag-and-drop columns appear, and each card is a second record. The 2-week row can group the same shots by status. Copying them into a board table will drift. A board-first app is the Trello guide.
Deploy the table
Tableflow wants Astro and D1. There is no cron and no file bucket until you add attachments. The hosting post is the adapter, the D1 binding, and the domain.
Before you import a real sheet, run the two-row sample. Confirm creates one shot. Cancel creates zero. Then edit shot-184 from two tabs and confirm the stale save loses.
Use it for the one process you track. Kanban, comments, and files are the 80-hour row. Then cancel the Airtable seats you no longer open, at $24–$54 a person a month.
What to skip on purpose
Leave out kanban, comments, attachments, automations, formulas, linked records, and a schema designer.
A pipe that posts into this table is the Zapier guide. A page of nested notes is the Notion guide. Tableflow is the records for one repeating process.
Start now
Go to Build your own Airtable. Copy the Astro prompt, or TanStack Start if that is what you write.
Tell the agent the first estimate. Typed fields, a table view, and a CSV import. One version on the shot. One confirm before any import row exists.
Create shot-184, reject a stale save, and confirm the sample file before you add a formula box.