Start With the Data Model, Not the UI
When vibe coding a new app, I always advise starting with your Postgres schema — not a button or a layout. Once your tables exist on Railway, you hand the schema to your AI agent and let it introspect the data model to craft a purpose-built UI/UX that perfectly mirrors your structure.
Below are three production-ready schema boilerplates for specific business verticals, a Railway-compatible Python creation script, and a guide to running safe, non-destructive migrations.
Why Define the Schema First?
The schema is the spec. Everything else — routes, forms, views — flows from it.
Schema as Spec
Your tables define every entity, relationship, and constraint your app will ever handle. It is the single source of truth before a single line of Flask code is written.
AI Introspection
Give your AI agent the schema and it knows exactly what fields to put in forms, what columns to show in tables, and what relationships to navigate — zero guessing.
Zero Throwaway UI
When the UI is derived from the data model, it fits perfectly from day one. No "I need to add a column" mid-build. No data migrations born from UI afterthoughts.
Ready-to-Copy Schema Boilerplates
Three niche use cases. Production-grade schemas. Copy the SQL, run the script below, and start building.
CRM for Dog Walkers
Manage clients, their dogs, scheduled walks, and invoicing. Tracks walker assignment, walk duration and status, emergency contacts per client, vet info per dog, and line-item invoices.
-- ============================================================
-- Dog Walker CRM — PostgreSQL Schema
-- All tables use IF NOT EXISTS: safe to re-run at any time.
-- ============================================================
CREATE TABLE IF NOT EXISTS clients (
id SERIAL PRIMARY KEY,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE,
phone VARCHAR(20),
address TEXT,
emergency_contact VARCHAR(255),
emergency_phone VARCHAR(20),
notes TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS dogs (
id SERIAL PRIMARY KEY,
client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
breed VARCHAR(100),
age_years INTEGER,
weight_kg NUMERIC(5,2),
vet_name VARCHAR(255),
vet_phone VARCHAR(20),
medical_notes TEXT,
feeding_instructions TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS walks (
id SERIAL PRIMARY KEY,
dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
walker_name VARCHAR(100),
scheduled_at TIMESTAMP NOT NULL,
started_at TIMESTAMP,
ended_at TIMESTAMP,
duration_minutes INTEGER,
status VARCHAR(20) DEFAULT 'scheduled'
CHECK (status IN ('scheduled','in_progress','completed','cancelled')),
notes TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS invoices (
id SERIAL PRIMARY KEY,
client_id INTEGER NOT NULL REFERENCES clients(id),
period_start DATE NOT NULL,
period_end DATE NOT NULL,
total_amount NUMERIC(10,2) NOT NULL,
status VARCHAR(20) DEFAULT 'draft'
CHECK (status IN ('draft','sent','paid','overdue')),
issued_at TIMESTAMP,
paid_at TIMESTAMP,
notes TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS invoice_items (
id SERIAL PRIMARY KEY,
invoice_id INTEGER NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
walk_id INTEGER REFERENCES walks(id),
description VARCHAR(255) NOT NULL,
quantity INTEGER DEFAULT 1,
unit_price NUMERIC(10,2) NOT NULL,
line_total NUMERIC(10,2) NOT NULL
);
Create Your Tables on Railway
Copy the schema above, drop it into the script below, and run it once. Your tables will be created on your remote Railway Postgres database.
Install dependencies
psycopg2-binary and python-dotenv
Set your .env variables
Copy from your Railway project dashboard
Run the script
python create_tables.py — done
Step 1 — Install dependencies
pip install psycopg2-binary python-dotenv
Step 2 — Set your .env file
Get these values from your Railway project → your Postgres service → Connect tab.
PGDATABASE=railway
PGUSER=postgres
PGPASSWORD=your-railway-password
PGHOST=your-host.railway.app
PGPORT=5432
Step 3 — create_tables.py
Save your chosen schema as schema.sql and place both files in the same folder (or at the root of your project).
The script reads the SQL file directly — no pasting, no formatting risk.
import os
import psycopg2
from dotenv import load_dotenv
load_dotenv()
def get_connection():
"""Create a connection to your Railway Postgres database."""
return psycopg2.connect(
dbname=os.environ["PGDATABASE"],
user=os.environ["PGUSER"],
password=os.environ["PGPASSWORD"],
host=os.environ["PGHOST"],
port=os.environ.get("PGPORT", 5432),
sslmode="require" # required by Railway
)
def create_tables():
"""Read schema.sql from the same folder and run it against the database."""
script_dir = os.path.dirname(os.path.abspath(__file__))
sql_path = os.path.join(script_dir, "schema.sql")
with open(sql_path, "r") as f:
schema_sql = f.read()
conn = get_connection()
cur = conn.cursor()
try:
cur.execute(schema_sql)
conn.commit()
print("✓ Tables created successfully.")
except Exception as e:
conn.rollback()
print(f"✗ Error creating tables: {e}")
raise
finally:
cur.close()
conn.close()
if __name__ == "__main__":
create_tables()
Expected folder layout: schema.sql and create_tables.py side by side, plus your .env file.
Adding Columns Without Breaking Anything
Your app is live. Users are in the database. You need to add a column. Here is the only safe way to do it.
The Golden Rule: ADD COLUMN IF NOT EXISTS
PostgreSQL's ADD COLUMN IF NOT EXISTS is idempotent — it only adds the column if it doesn't already exist, and does nothing (without error) if it does.
This makes your migration scripts safe to run multiple times without side effects.
- No downtime required for adding a nullable column
- Existing rows get NULL (or your DEFAULT) for the new column
- Safe to re-run — running it twice does nothing on the second pass
- Never use
DROP COLUMNon a live database without a backup - Never use
ALTER COLUMN TYPEwithout explicit casting — it rewrites the table
migrate.py — run it from your machine, same as the creation script
Same connection setup as create_tables.py. Each migration is a labeled SQL string — append new ones at the bottom as your schema evolves.
Safe to re-run at any time: ADD COLUMN IF NOT EXISTS does nothing if the column already exists.
import os
import psycopg2
from dotenv import load_dotenv
load_dotenv()
def get_connection():
"""Create a connection to your Railway Postgres database."""
return psycopg2.connect(
dbname=os.environ["PGDATABASE"],
user=os.environ["PGUSER"],
password=os.environ["PGPASSWORD"],
host=os.environ["PGHOST"],
port=os.environ.get("PGPORT", 5432),
sslmode="require"
)
def run_migration(description, sql):
"""
Execute a single migration statement against the database.
Each call opens and closes its own connection so a failed
migration never blocks the ones that follow.
"""
conn = get_connection()
cur = conn.cursor()
try:
cur.execute(sql)
conn.commit()
print(f" ✓ {description}")
except Exception as e:
conn.rollback()
print(f" ✗ {description} — FAILED: {e}")
raise
finally:
cur.close()
conn.close()
if __name__ == "__main__":
print("Running migrations...")
# ── Append new migrations here. Safe to re-run — IF NOT EXISTS is idempotent.
run_migration(
"Add tags column to clients",
"ALTER TABLE clients ADD COLUMN IF NOT EXISTS tags TEXT[];"
)
run_migration(
"Add referred_by column to clients",
"ALTER TABLE clients ADD COLUMN IF NOT EXISTS referred_by VARCHAR(255);"
)
run_migration(
"Add github_url column to projects",
"ALTER TABLE projects ADD COLUMN IF NOT EXISTS github_url VARCHAR(255);"
)
run_migration(
"Add is_warranty_job flag to jobs",
"ALTER TABLE jobs ADD COLUMN IF NOT EXISTS is_warranty_job BOOLEAN DEFAULT FALSE;"
)
print("Done.")
Build the Rest of Your Stack
Schema done. Now scaffold the Flask app and generate your first prompts.
Join the Movement
Get tutorials, tools, and insights on building with Flask + PostgreSQL + Vanilla JS.
No spam. Unsubscribe anytime. We respect your inbox.