Module F-7·14 min read

Translating a real-world problem into tables, connecting from application code, and basic backup and restore.

Written by Jatin Jain Saraf · Architect of SupraScan

F-7 — Your First Real Application Schema

Who this module is for: You have completed all of Phase 1. You can create tables, query, filter, aggregate, join, and enforce constraints. Now you need to put it all together: design a schema for a real application, connect to it from application code, and understand the tools that keep schemas manageable as they evolve. This module closes the Foundation phase and prepares you for the Practitioner topics ahead.


From Concepts to Real Design

A schema for a real application starts with questions, not SQL:

  1. What are the entities? The "things" your application manages — users, tasks, projects, comments
  2. What attributes does each entity have? The data that describes each thing
  3. What are the relationships? How entities relate — a task belongs to a project, a user can have many tasks

This process is called data modelling. You do not need formal methodology — just answer these three questions before writing any SQL.

Example: A Task Management Application

Entities:

  • Users (who uses the app)
  • Projects (containers for tasks)
  • Tasks (the work items)
  • Comments (discussion on tasks)

Attributes:

  • User: username, email, password hash, created date
  • Project: name, description, owner user, created date
  • Task: title, description, status, assignee, project, due date, created date
  • Comment: content, author, task, created date

Relationships:

  • A user owns zero or more projects (one-to-many)
  • A project has zero or more tasks (one-to-many)
  • A task belongs to exactly one project (many-to-one)
  • A task can be assigned to zero or one user (optional many-to-one)
  • A task has zero or more comments (one-to-many)
  • A user can be a member of multiple projects, and a project has multiple members (many-to-many)

The Complete Schema

CREATE DATABASE taskmanager;
\c taskmanager
 
-- Users of the application
CREATE TABLE users (
  id            BIGSERIAL     PRIMARY KEY,
  username      TEXT          NOT NULL UNIQUE,
  email         TEXT          NOT NULL UNIQUE,
  password_hash TEXT          NOT NULL,       -- never store plain passwords
  display_name  TEXT,
  created_at    TIMESTAMPTZ   NOT NULL DEFAULT NOW(),
  last_login_at TIMESTAMPTZ
);
 
-- Projects group tasks together
CREATE TABLE projects (
  id          BIGSERIAL   PRIMARY KEY,
  name        TEXT        NOT NULL,
  description TEXT,
  owner_id    BIGINT      NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
  -- Restrict: can't delete a user who owns projects
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  archived_at TIMESTAMPTZ           -- NULL = active, non-NULL = archived
);
 
-- Many-to-many: project membership (who can see and work in a project)
CREATE TABLE project_members (
  project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  user_id    BIGINT NOT NULL REFERENCES users(id)    ON DELETE CASCADE,
  role       TEXT   NOT NULL DEFAULT 'member'
             CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
  joined_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  PRIMARY KEY (project_id, user_id)  -- a user can only be in a project once
);
 
-- Tasks are the work items
CREATE TABLE tasks (
  id           BIGSERIAL   PRIMARY KEY,
  project_id   BIGINT      NOT NULL REFERENCES projects(id)  ON DELETE CASCADE,
  -- If a project is deleted, all its tasks are deleted too
  created_by   BIGINT      NOT NULL REFERENCES users(id)     ON DELETE RESTRICT,
  assigned_to  BIGINT               REFERENCES users(id)     ON DELETE SET NULL,
  -- assigned_to is optional; if the assignee is deleted, set to NULL
  title        TEXT        NOT NULL,
  description  TEXT,
  status       TEXT        NOT NULL DEFAULT 'todo'
               CHECK (status IN ('todo', 'in_progress', 'done', 'cancelled')),
  priority     INTEGER     NOT NULL DEFAULT 2
               CHECK (priority BETWEEN 1 AND 5),  -- 1=lowest, 5=highest
  due_date     DATE,
  completed_at TIMESTAMPTZ,          -- NULL until task is marked done
  created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
 
-- Comments on tasks
CREATE TABLE comments (
  id         BIGSERIAL   PRIMARY KEY,
  task_id    BIGINT      NOT NULL REFERENCES tasks(id)  ON DELETE CASCADE,
  author_id  BIGINT      NOT NULL REFERENCES users(id)  ON DELETE RESTRICT,
  content    TEXT        NOT NULL CHECK (LENGTH(TRIM(content)) > 0),
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  edited_at  TIMESTAMPTZ           -- NULL until edited
);

Verifying the Schema

-- List all tables
\dt
 
-- Check the structure of each table
\d users
\d projects
\d tasks
\d comments
\d project_members
 
-- See all foreign key relationships
SELECT
  tc.table_name,
  kcu.column_name,
  ccu.table_name  AS foreign_table,
  ccu.column_name AS foreign_column
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
  ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
  ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
ORDER BY tc.table_name;

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.