Model the product first
Write down the records the application owns, such as profiles, projects, submissions or orders. Give each record an ID, timestamps and only the fields the product needs. Use a relationship when two records need to stay connected.
- Create the tables from the workflow, not from the first screen.
- Add foreign keys for relationships and decide what should happen when a parent record is deleted.
- Grant only the database access the policies require.
- Enable row level security on every user-facing table.
- Create policies for each operation instead of relying on the table existing.
Relationships
The list below breaks this down into the parts that matter most. Read through each one and note anything that applies to your own setup.
- One-to-many: one user owns many submissions.
- Many-to-many: one project has many tags and one tag has many projects.
- Reference user identity through the platform auth user ID, not a copied email.
- Keep roles in a separate roles table and check them server-side.
Schema review checklist
Before making changes, work through the points below. Each one covers a different part of the picture, so treat them as a set rather than picking favourites.
- Every table has a primary key and created_at timestamp.
- Every foreign key has an explicit on delete behaviour, not the default.
- Nullable columns are nullable on purpose, not by oversight.
- Enum-like values are constrained with a check constraint or a lookup table.
- Indexes exist on columns used in filters, joins and RLS policy comparisons.
- No table stores a duplicate copy of data owned by another table.
Growing the schema safely
Work through these steps in order. Each one builds on the last, so resist skipping ahead even if a step feels familiar.
- Add new columns as nullable first, then backfill before making them required.
- Write migrations that can run against existing production data without dropping rows.
- Rename columns by adding the new one, migrating reads, then removing the old one.
- Version any breaking change to a table the UI depends on directly.
- Re-check RLS policies whenever a column used in a policy expression changes.