Taskboard Data Model & Architecture

Understanding Waymaker's taskboard data model is essential for advanced integrations, API usage, custom reporting, and building tools that interact with taskboard data.

Architecture Overview

Waymaker taskboards are built on a hierarchical, relational data model with real-time capabilities.

Key Architectural Principles

1. Hierarchical Structure

Organization
  └── Boards
       └── Layers
            └── Sections
                 └── Tasks
                      ├── Comments
                      ├── Attachments
                      └── Dependencies

2. Separation of Concerns

  • Authentication: Separate auth layer
  • Application Data: Application database
  • Real-time: Live subscriptions for instant updates
  • Storage: Separate bucket for file attachments

3. Security Model

  • Access rules on all tables
  • Policy-based access control
  • Organization-scoped data isolation
  • Role-based permissions

4. Performance Optimization

  • Strategic indexing on foreign keys and filters
  • Materialized views for complex queries
  • Efficient denormalization for common reads
  • Pagination for large datasets

Core Entities

1. Organizations

Purpose: Top-level container for all workspace data

Schema:

CREATE TABLE organizations (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  name TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,
  owner_id UUID NOT NULL REFERENCES auth.users(id),
  settings JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

Key Fields:

  • id: Unique organization identifier
  • name: Display name (e.g., "Acme Corp")
  • slug: URL-friendly identifier (e.g., "acme-corp")
  • owner_id: Organization owner (billing responsible party)
  • settings: Organization-wide configuration

Settings JSONB Structure:

{
  "features": {
    "taskboards": true,
    "ai_assistance": false,
    "custom_fields": true
  },
  "limits": {
    "max_boards": 50,
    "max_members": 100,
    "storage_gb": 50
  },
  "branding": {
    "logo_url": "https://...",
    "primary_color": "#001126"
  }
}

Relationships:

  • Has Many: boards, organization_members, teams
  • Belongs To: users (owner)

Indexes:

CREATE INDEX idx_organizations_slug ON organizations(slug);
CREATE INDEX idx_organizations_owner ON organizations(owner_id);

2. Organization Members

Purpose: User membership and roles within organizations

Schema:

CREATE TABLE organization_members (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member', 'guest')),
  permissions JSONB DEFAULT '{}',
  invited_by UUID REFERENCES auth.users(id),
  joined_at TIMESTAMPTZ DEFAULT now(),
  created_at TIMESTAMPTZ DEFAULT now(),

  UNIQUE(organization_id, user_id)
);

Key Fields:

  • role: User's role (owner, admin, member, guest)
  • permissions: Custom permission overrides
  • invited_by: User who sent the invitation

Role Hierarchy:

Owner (1 per org)
  ├── Full organizational control
  ├── Billing and subscription management
  └── Can delete organization

Admin
  ├── Manage members and teams
  ├── Create and delete boards
  └── Configure organization settings

Member
  ├── Access assigned boards
  ├── Create and edit tasks
  └── Collaborate on projects

Guest
  ├── Limited board access (explicitly invited)
  ├── View and comment only (by default)
  └── No organizational visibility

Indexes:

CREATE INDEX idx_org_members_org ON organization_members(organization_id);
CREATE INDEX idx_org_members_user ON organization_members(user_id);
CREATE INDEX idx_org_members_role ON organization_members(organization_id, role);

3. Boards

Purpose: Container for tasks organized by workflow

Schema:

CREATE TABLE boards (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  description TEXT,
  settings JSONB DEFAULT '{}',
  created_by UUID NOT NULL REFERENCES auth.users(id),
  archived BOOLEAN DEFAULT false,
  archived_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

Key Fields:

  • name: Board title (e.g., "Q1 Product Launch")
  • description: Board purpose and context
  • settings: Board-specific configuration
  • archived: Soft delete flag

Settings JSONB Structure:

{
  "workflow": {
    "default_sections": ["To Do", "In Progress", "Done"],
    "auto_archive_completed": false,
    "task_id_prefix": "TASK"
  },
  "permissions": {
    "default_member_role": "editor",
    "allow_guest_comments": true,
    "require_assignee": false
  },
  "integrations": {
    "slack_channel": "#project-updates",
    "github_repo": "org/repo"
  },
  "notifications": {
    "task_assigned": true,
    "task_completed": false,
    "comment_mentioned": true
  }
}

Relationships:

  • Belongs To: organizations, users (creator)
  • Has Many: layers, board_members, tasks

Indexes:

CREATE INDEX idx_boards_org ON boards(organization_id);
CREATE INDEX idx_boards_archived ON boards(organization_id, archived);
CREATE INDEX idx_boards_created_by ON boards(created_by);

4. Board Members

Purpose: User access and permissions per board

Schema:

CREATE TABLE board_members (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  board_id UUID NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  role TEXT NOT NULL CHECK (role IN ('owner', 'editor', 'viewer', 'commenter')),
  added_by UUID REFERENCES auth.users(id),
  added_at TIMESTAMPTZ DEFAULT now(),

  UNIQUE(board_id, user_id)
);

Board Role Permissions:

Owner
  ├── Full board control
  ├── Manage board members
  ├── Archive/delete board
  └── Configure board settings

Editor
  ├── Create and edit all tasks
  ├── Manage layers and sections
  ├── Add comments and attachments
  └── Cannot manage board settings

Viewer
  ├── View all tasks and comments
  ├── Export and print
  └── Cannot edit or comment

Commenter
  ├── View all tasks
  ├── Add comments
  └── Cannot edit tasks

Indexes:

CREATE INDEX idx_board_members_board ON board_members(board_id);
CREATE INDEX idx_board_members_user ON board_members(user_id);

5. Layers

Purpose: Organize tasks into logical groupings (sprints, epics, phases)

Schema:

CREATE TABLE layers (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  board_id UUID NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  description TEXT,
  layer_type TEXT NOT NULL CHECK (layer_type IN ('sprint', 'epic', 'milestone', 'phase', 'custom')),
  status TEXT NOT NULL CHECK (status IN ('active', 'completed', 'archived')),
  start_date DATE,
  end_date DATE,
  color TEXT,
  sort_order INTEGER NOT NULL,
  metadata JSONB DEFAULT '{}',
  created_by UUID NOT NULL REFERENCES auth.users(id),
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

Key Fields:

  • layer_type: Classification (sprint, epic, milestone, phase, custom)
  • status: Lifecycle state (active, completed, archived)
  • start_date, end_date: Time boundaries
  • sort_order: Display order on board
  • metadata: Layer-specific data

Metadata JSONB Structure:

{
  "sprint": {
    "sprint_number": 23,
    "velocity_target": 25,
    "actual_velocity": 23
  },
  "epic": {
    "epic_key": "EPIC-45",
    "theme": "User Onboarding",
    "business_value": "high"
  },
  "milestone": {
    "deliverables": ["Feature A", "Feature B"],
    "stakeholders": ["user1", "user2"]
  },
  "progress": {
    "total_tasks": 15,
    "completed_tasks": 8,
    "completion_percentage": 53
  }
}

Relationships:

  • Belongs To: boards, users (creator)
  • Has Many: tasks

Indexes:

CREATE INDEX idx_layers_board ON layers(board_id);
CREATE INDEX idx_layers_status ON layers(board_id, status);
CREATE INDEX idx_layers_sort ON layers(board_id, sort_order);
CREATE INDEX idx_layers_dates ON layers(board_id, start_date, end_date);

6. Tasks

Purpose: Individual work items with full lifecycle tracking

Schema:

CREATE TABLE tasks (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  board_id UUID NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
  layer_id UUID REFERENCES layers(id) ON DELETE SET NULL,
  section TEXT NOT NULL,
  title TEXT NOT NULL,
  description TEXT,
  assignee_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
  reporter_id UUID NOT NULL REFERENCES auth.users(id),
  priority TEXT CHECK (priority IN ('lowest', 'low', 'medium', 'high', 'highest')),
  status TEXT NOT NULL,
  task_number INTEGER NOT NULL,
  sort_order REAL NOT NULL,
  due_date DATE,
  completed_at TIMESTAMPTZ,
  estimated_hours DECIMAL(5,2),
  actual_hours DECIMAL(5,2),
  tags TEXT[],
  metadata JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now(),

  UNIQUE(board_id, task_number)
);

Key Fields:

  • section: Workflow column (e.g., "To Do", "In Progress", "Done")
  • title: Task name (required)
  • description: Detailed task information (Markdown supported)
  • assignee_id: Assigned user (optional)
  • reporter_id: Task creator
  • priority: Urgency level
  • status: Current state (matches section)
  • task_number: Auto-incrementing board-specific number
  • sort_order: Position within section (REAL for between-insert flexibility)
  • tags: Array of label strings

Metadata JSONB Structure:

{
  "checklist": [
    {
      "id": "check1",
      "text": "Review requirements",
      "completed": true
    },
    {
      "id": "check2",
      "text": "Write tests",
      "completed": false
    }
  ],
  "custom_fields": {
    "customer": "Acme Corp",
    "environment": "production",
    "severity": "critical"
  },
  "progress": {
    "total_subtasks": 5,
    "completed_subtasks": 2,
    "percentage": 40
  }
}

Task Lifecycle States:

Created → To Do → In Progress → In Review → Done
                       ↓
                   Blocked (temporary)
                       ↓
                   In Progress (resumed)

Relationships:

  • Belongs To: boards, layers, users (assignee, reporter)
  • Has Many: comments, attachments, task_dependencies
  • Has Many Through: blocking_tasks, blocked_by_tasks

Indexes:

CREATE INDEX idx_tasks_board ON tasks(board_id);
CREATE INDEX idx_tasks_layer ON tasks(layer_id);
CREATE INDEX idx_tasks_section ON tasks(board_id, section);
CREATE INDEX idx_tasks_assignee ON tasks(assignee_id);
CREATE INDEX idx_tasks_reporter ON tasks(reporter_id);
CREATE INDEX idx_tasks_status ON tasks(board_id, status);
CREATE INDEX idx_tasks_sort ON tasks(board_id, section, sort_order);
CREATE INDEX idx_tasks_due_date ON tasks(board_id, due_date) WHERE due_date IS NOT NULL;
CREATE INDEX idx_tasks_tags ON tasks USING GIN(tags);
CREATE INDEX idx_tasks_number ON tasks(board_id, task_number);

7. Task Dependencies

Purpose: Model relationships between tasks

Schema:

CREATE TABLE task_dependencies (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
  depends_on_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
  dependency_type TEXT NOT NULL CHECK (dependency_type IN ('FS', 'SS', 'FF', 'SF')),
  created_by UUID NOT NULL REFERENCES auth.users(id),
  created_at TIMESTAMPTZ DEFAULT now(),

  UNIQUE(task_id, depends_on_id),
  CHECK (task_id != depends_on_id)
);

Dependency Types:

FS (Finish-to-Start): Task A must finish before Task B starts
SS (Start-to-Start): Task A must start before Task B starts
FF (Finish-to-Finish): Task A must finish before Task B finishes
SF (Start-to-Finish): Task A must start before Task B finishes (rare)

Example Dependency Graph:

Task A (Design)
  ↓ FS
Task B (Development) ←── SS ──→ Task C (Testing setup)
  ↓ FS                              ↓ FF
Task D (QA)                     Task E (Test execution)

Validation Rules:

  • No circular dependencies
  • Dependencies must be within same board
  • Cannot depend on self

Indexes:

CREATE INDEX idx_task_deps_task ON task_dependencies(task_id);
CREATE INDEX idx_task_deps_depends ON task_dependencies(depends_on_id);
CREATE INDEX idx_task_deps_type ON task_dependencies(dependency_type);

8. Comments

Purpose: Discussion and collaboration on tasks

Schema:

CREATE TABLE comments (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  content TEXT NOT NULL,
  mentions UUID[],
  edited BOOLEAN DEFAULT false,
  edited_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

Key Fields:

  • content: Comment text (Markdown supported)
  • mentions: Array of mentioned user IDs
  • edited: Whether comment has been modified
  • edited_at: Last edit timestamp

Mention Pattern:

@[User Name](user_id) - this is a mention

Example:
"@[Sarah Johnson](uuid-123) could you review the API integration?"

Relationships:

  • Belongs To: tasks, users (author)

Indexes:

CREATE INDEX idx_comments_task ON comments(task_id);
CREATE INDEX idx_comments_user ON comments(user_id);
CREATE INDEX idx_comments_created ON comments(task_id, created_at DESC);
CREATE INDEX idx_comments_mentions ON comments USING GIN(mentions);

9. Attachments

Purpose: Files associated with tasks

Schema:

CREATE TABLE attachments (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES auth.users(id),
  file_name TEXT NOT NULL,
  file_path TEXT NOT NULL,
  file_size BIGINT NOT NULL,
  mime_type TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);

Key Fields:

  • file_name: Original filename
  • file_path: File storage path
  • file_size: Size in bytes
  • mime_type: File type (e.g., "image/png", "application/pdf")

Storage Path Structure:

organizations/{org_id}/boards/{board_id}/tasks/{task_id}/{file_id}-{filename}

Example:
organizations/abc-123/boards/def-456/tasks/ghi-789/attach-001-screenshot.png

Relationships:

  • Belongs To: tasks, users (uploader)

Indexes:

CREATE INDEX idx_attachments_task ON attachments(task_id);
CREATE INDEX idx_attachments_user ON attachments(user_id);

Data Relationships Diagram

┌─────────────────┐
│  Organizations  │
└────────┬────────┘
         │ 1:N
    ┌────┴────┐
    ↓         ↓
┌────────┐  ┌──────────────────┐
│ Boards │  │ Org Members      │
└───┬────┘  └──────────────────┘
    │ 1:N
    ├─────────────┐
    ↓             ↓
┌────────┐   ┌──────────────┐
│ Layers │   │ Board Members│
└───┬────┘   └──────────────┘
    │ 1:N
    ↓
┌────────────────┐
│     Tasks      │
└───┬────────┬───┘
    │ 1:N    │ 1:N
    ↓        ↓
┌──────────┐ ┌──────────────────┐
│Comments  │ │   Attachments    │
└──────────┘ └──────────────────┘

┌──────────────────────┐
│  Task Dependencies   │
│  (Self-referencing)  │
└──────────────────────┘

Data Access Rules

All tables use access rules for data security.

Organizations Access Rules

-- Users can only see organizations they're members of
CREATE POLICY "Users can view their organizations"
  ON organizations FOR SELECT
  USING (
    id IN (
      SELECT organization_id
      FROM organization_members
      WHERE user_id = auth.uid()
    )
  );

-- Only organization owners can update
CREATE POLICY "Owners can update their organizations"
  ON organizations FOR UPDATE
  USING (owner_id = auth.uid());

Boards Access Rules

-- Users can see boards in their organizations or boards they're explicitly members of
CREATE POLICY "Users can view accessible boards"
  ON boards FOR SELECT
  USING (
    organization_id IN (
      SELECT organization_id
      FROM organization_members
      WHERE user_id = auth.uid()
    )
    OR
    id IN (
      SELECT board_id
      FROM board_members
      WHERE user_id = auth.uid()
    )
  );

-- Board owners and org admins can update boards
CREATE POLICY "Authorized users can update boards"
  ON boards FOR UPDATE
  USING (
    created_by = auth.uid()
    OR
    organization_id IN (
      SELECT organization_id
      FROM organization_members
      WHERE user_id = auth.uid()
        AND role IN ('owner', 'admin')
    )
  );

Tasks Access Rules

-- Users can see tasks on boards they have access to
CREATE POLICY "Users can view tasks on accessible boards"
  ON tasks FOR SELECT
  USING (
    board_id IN (
      SELECT id FROM boards
      WHERE organization_id IN (
        SELECT organization_id
        FROM organization_members
        WHERE user_id = auth.uid()
      )
      OR id IN (
        SELECT board_id
        FROM board_members
        WHERE user_id = auth.uid()
      )
    )
  );

-- Users with editor role or higher can create tasks
CREATE POLICY "Editors can create tasks"
  ON tasks FOR INSERT
  WITH CHECK (
    board_id IN (
      SELECT board_id
      FROM board_members
      WHERE user_id = auth.uid()
        AND role IN ('owner', 'editor')
    )
  );

-- Users with editor role can update tasks
CREATE POLICY "Editors can update tasks"
  ON tasks FOR UPDATE
  USING (
    board_id IN (
      SELECT board_id
      FROM board_members
      WHERE user_id = auth.uid()
        AND role IN ('owner', 'editor')
    )
  );

Materialized Views

For performance optimization on common queries:

Board Summary View

CREATE MATERIALIZED VIEW board_summaries AS
SELECT
  b.id AS board_id,
  b.name AS board_name,
  COUNT(DISTINCT t.id) AS total_tasks,
  COUNT(DISTINCT t.id) FILTER (WHERE t.section = 'Done') AS completed_tasks,
  COUNT(DISTINCT t.id) FILTER (WHERE t.section != 'Done') AS active_tasks,
  COUNT(DISTINCT l.id) AS total_layers,
  COUNT(DISTINCT bm.user_id) AS member_count,
  MAX(t.updated_at) AS last_activity
FROM boards b
LEFT JOIN tasks t ON b.id = t.board_id
LEFT JOIN layers l ON b.id = l.board_id
LEFT JOIN board_members bm ON b.id = bm.board_id
WHERE b.archived = false
GROUP BY b.id, b.name;

CREATE INDEX idx_board_summaries_id ON board_summaries(board_id);

-- Refresh periodically
REFRESH MATERIALIZED VIEW CONCURRENTLY board_summaries;

Task Activity View

CREATE MATERIALIZED VIEW task_activity_summary AS
SELECT
  t.id AS task_id,
  t.board_id,
  t.title,
  COUNT(DISTINCT c.id) AS comment_count,
  COUNT(DISTINCT a.id) AS attachment_count,
  COUNT(DISTINCT td.id) AS dependency_count,
  MAX(c.created_at) AS last_comment_at,
  COALESCE(
    EXTRACT(EPOCH FROM (t.completed_at - t.created_at)) / 86400,
    EXTRACT(EPOCH FROM (NOW() - t.created_at)) / 86400
  ) AS age_days
FROM tasks t
LEFT JOIN comments c ON t.id = c.task_id
LEFT JOIN attachments a ON t.id = a.task_id
LEFT JOIN task_dependencies td ON t.id = td.task_id
GROUP BY t.id, t.board_id, t.title, t.completed_at, t.created_at;

CREATE INDEX idx_task_activity_board ON task_activity_summary(board_id);

Real-Time Subscriptions

Waymaker uses real-time subscriptions for live updates.

Subscription Patterns

Board-Level Updates:

// Subscribe to all task changes on a board
supabase
  .channel(`board:${boardId}`)
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'tasks',
      filter: `board_id=eq.${boardId}`
    },
    (payload) => {
      handleTaskUpdate(payload);
    }
  )
  .subscribe();

Task-Level Updates:

// Subscribe to comments on a specific task
supabase
  .channel(`task:${taskId}:comments`)
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'comments',
      filter: `task_id=eq.${taskId}`
    },
    (payload) => {
      handleNewComment(payload.new);
    }
  )
  .subscribe();

Presence (Who's Online):

// Track users viewing a board
const channel = supabase.channel(`board:${boardId}:presence`);

channel
  .on('presence', { event: 'sync' }, () => {
    const users = channel.presenceState();
    updateOnlineUsers(users);
  })
  .subscribe(async (status) => {
    if (status === 'SUBSCRIBED') {
      await channel.track({
        user_id: currentUserId,
        online_at: new Date().toISOString(),
      });
    }
  });

Data Integrity Constraints

Referential Integrity

All foreign keys use appropriate ON DELETE actions:

  • CASCADE: Delete dependent records (e.g., delete board → delete all tasks)
  • SET NULL: Nullify reference (e.g., delete user → set task.assignee_id to null)
  • RESTRICT: Prevent deletion if dependents exist (default)

Check Constraints

Enforce data validity at the database level:

-- Priority must be valid value
ALTER TABLE tasks ADD CONSTRAINT valid_priority
  CHECK (priority IN ('lowest', 'low', 'medium', 'high', 'highest'));

-- Dates must be logical
ALTER TABLE layers ADD CONSTRAINT logical_dates
  CHECK (end_date IS NULL OR end_date >= start_date);

-- Sort order must be positive
ALTER TABLE tasks ADD CONSTRAINT positive_sort_order
  CHECK (sort_order > 0);

Unique Constraints

Prevent duplicate data:

-- One membership per user per organization
ALTER TABLE organization_members
  ADD CONSTRAINT unique_org_user
  UNIQUE (organization_id, user_id);

-- Unique task numbers per board
ALTER TABLE tasks
  ADD CONSTRAINT unique_board_task_number
  UNIQUE (board_id, task_number);

Performance Considerations

Query Optimization

Use Indexes Wisely:

  • Index foreign keys for joins
  • Index commonly filtered fields (status, section, assignee)
  • Use partial indexes for filtered queries (e.g., WHERE archived = false)

Avoid N+1 Queries:

// Bad: N+1 query
const tasks = await getTasks(boardId);
for (const task of tasks) {
  const comments = await getComments(task.id); // N queries
}

// Good: Single query with join
const tasksWithComments = await getTasksWithComments(boardId);

Use Pagination:

-- Cursor-based pagination (efficient for large datasets)
SELECT * FROM tasks
WHERE board_id = $1
  AND sort_order > $2
ORDER BY sort_order
LIMIT 50;

Denormalization

Strategic denormalization improves read performance:

Task Count on Layers:

-- Store computed count for fast access
ALTER TABLE layers ADD COLUMN task_count INTEGER DEFAULT 0;

-- Update via trigger
CREATE OR REPLACE FUNCTION update_layer_task_count()
RETURNS TRIGGER AS $$
BEGIN
  UPDATE layers
  SET task_count = (
    SELECT COUNT(*) FROM tasks WHERE layer_id = NEW.layer_id
  )
  WHERE id = NEW.layer_id;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER task_count_trigger
AFTER INSERT OR UPDATE OR DELETE ON tasks
FOR EACH ROW EXECUTE FUNCTION update_layer_task_count();

Best Practices

For Application Developers

Use transactions for multi-table operations ✅ Validate data before database operations ✅ Handle null values appropriately ✅ Use indexes for common query patterns ✅ Paginate large result setsSubscribe to real-time only for active views ✅ Clean up subscriptions when components unmount

For Database Queries

Filter early in WHERE clauses ✅ Select only needed columns (avoid SELECT *) ✅ Use appropriate join types (INNER vs LEFT) ✅ Leverage indexes in ORDER BY and WHERE ✅ Batch operations when possible ✅ Use prepared statements to prevent SQL injection

For Data Modeling

Normalize appropriately (balance normal forms with performance) ✅ Use meaningful names for tables and columns ✅ Document JSONB structures in code comments ✅ Version your schema with migration files ✅ Test access rules thoroughly ✅ Monitor query performance and optimize as needed

Conclusion

Waymaker's taskboard data model provides:

  • Hierarchical organization from orgs to tasks
  • Flexible schema with JSONB for extensibility
  • Strong security via database-level access rules
  • Real-time capabilities for collaboration
  • Performance optimization through indexing and materialized views

Understanding this data model enables you to build powerful integrations, custom reports, and advanced features on top of Waymaker's taskboard platform.

Related Resources


Last updated: January 10, 2025