Define and apply your schema
Your app needs to remember things — forms people create and submissions people send in. That's what a database is for. Think of it as a spreadsheet that your app can read and write to.
Your starter already has the database wiring done. In this step, you define your app's first real tables.
?Why SQLite? ?Why Drizzle ORM?The .server folder
Before you start writing schema code, notice where the database files live: app/.server/db/. That .server in the path means this code stays on the server and never ships to the browser.
Database connections, passwords, API keys — anything that would be dangerous if a visitor could see it lives behind that wall. You'll see this pattern throughout the project: if a folder or file has .server in its name, it's server-only.
Define your schema
Before you can store data, you need to tell the database what shape the data will have. That's called a schema — it's like setting up the column headers in a spreadsheet before entering any rows.
?What are schemas and migrations?Your form builder needs two tables:
- forms — Each row is a form someone created. Columns: id, title, the list of fields, a unique token for viewing results, and a timestamp.
- submissions — Each row is one form submission. Columns: id, which form it belongs to, the submitted data, and a timestamp.
Ask your AI:
Create a database schema with two tables. A "forms" table with columns for id, title, fields (as JSON), a unique token, and created_at. A "submissions" table with columns for id, form_id (linking to the forms table), data (as JSON), and created_at.
The AI will create a schema file that describes your tables. It uses helper functions from helpers.ts that come with the starter — id() for auto-incrementing primary keys, defaultHex() for unique tokens, foreign() for foreign keys, and defaultNow() for timestamps. It should look something like this:
import { sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { defaultHex, defaultNow, foreign, id } from './helpers'
export const forms = sqliteTable('forms', {
id: id(),
title: text().notNull(),
fields: text({ mode: 'json' }).notNull(),
token: defaultHex(),
created_at: defaultNow(),
})
export const submissions = sqliteTable('submissions', {
id: id(),
form_id: foreign(() => forms).notNull(),
data: text({ mode: 'json' }).notNull(),
created_at: defaultNow(),
})
Notice the import at the top and the export on each table? That's how files share code with each other in JavaScript — you'll see this pattern everywhere.
The first import pulls in Drizzle's building blocks, and the second imports Gista.js's helper functions that wrap common patterns.
Notice form_id on the submissions table — it's a foreign key. It points to a row in the forms table. This is how the database knows which submissions belong to which form.
Apply the schema to the database
Now tell the database to actually create those tables. Run:
atlas schema apply --env dev

Atlas will show you the planned changes it's about to make and ask you to approve.
Hit Enter to approve and apply. This creates the tables in your local SQLite database.
See it for yourself
Want proof it worked? Refresh local.drizzle.studio in your browser.

You should see two empty tables: forms and submissions. The column headers match what you defined in the schema. Ignore sqlite_sequence — SQLite creates it automatically.
No data yet. Let's fix that.