1. Before You Create the Table






2. Table Schema

Required Columns — Every Table

Column Type Notes
id uuid DEFAULT gen_random_uuid() Primary key
created_at timestamptz DEFAULT now() Always included
updated_at timestamptz DEFAULT now() Always included

Workspace-Scoped Tables — Multi-Tenant

Column Type Notes
workspace_id uuid NOT NULL REFERENCES workspaces(id) Required for any tenant-scoped table

User-Owned Records

Column Type Notes
created_by uuid NOT NULL REFERENCES auth.users(id) Track who created the record

Soft Delete — If Applicable

Column Type Notes
deleted_at timestamptz DEFAULT NULL NULL = not deleted, timestamp = deleted

3. Naming Standards






4. Constraints








Example CHECK Constraint

CHECK (status IN ('active', 'inactive', 'archived'))
CHECK (percentage >= 0 AND percentage <= 100)

ON DELETE Decision Guide

Scenario Use
Child records have no meaning without parent CASCADE
Child records should be preserved for audit RESTRICT or SET NULL
Foreign key is optional reference SET NULL
Default — block parent deletion RESTRICT

5. Indexes







Composite Index Example

CREATE INDEX ON projects (workspace_id, status);

6. RLS Setup










RLS Policy Patterns

-- SELECT: workspace members only
CREATE POLICY "workspace_members_select"
ON projects
FOR SELECT
USING (
  workspace_id IN (
    SELECT workspace_id
    FROM workspace_members
    WHERE user_id = auth.uid()
  )
);

-- INSERT: workspace members only, sets created_by automatically
CREATE POLICY "workspace_members_insert"
ON projects
FOR INSERT
WITH CHECK (
  workspace_id IN (
    SELECT workspace_id
    FROM workspace_members
    WHERE user_id = auth.uid()
  )
  AND created_by = auth.uid()
);

-- UPDATE: only creator or admin
CREATE POLICY "owner_or_admin_update"
ON projects
FOR UPDATE
USING (
  created_by = auth.uid()
  OR EXISTS (
    SELECT 1
    FROM workspace_members
    WHERE workspace_id = projects.workspace_id
      AND user_id = auth.uid()
      AND role = 'admin'
  )
);

-- DELETE: only admin
CREATE POLICY "admin_delete"
ON projects
FOR DELETE
USING (
  EXISTS (
    SELECT 1
    FROM workspace_members
    WHERE workspace_id = projects.workspace_id
      AND user_id = auth.uid()
      AND role = 'admin'
  )
);

7. Audit Fields




Auto-Update Trigger for updated_at

CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = now();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER set_updated_at
BEFORE UPDATE ON your_table_name
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();

8. Migration File






9. Documentation






Before Marking Done

10. New Table Checklist

Schema







Naming



Constraints



Indexes



RLS







Migration & Documentation





Practice Task

Apply what you learned by creating a real table with the correct schema, constraints, indexes, soft delete, and migration file.

Open Task 01: Create the Projects Table