> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tasks - Task Management

> Hierarchical task management with bucket dump and cross-board coordination

# Tasks - Smart Task Management

Tasks is Tuturuuu's intelligent task management system featuring a 6-level hierarchy, bucket dump for rapid capture, and cross-board project coordination.

## Overview

Tasks provides:

* **Hierarchical organization** - 6 levels from workspaces to individual tasks
* **Bucket dump** - Capture notes and convert to tasks/projects
* **Cross-board projects** - Coordinate tasks across multiple boards
* **Task cycles** - Sprint/iteration management
* **Smart estimation** - Fibonacci, exponential, linear, and t-shirt sizing
* **Custom statuses** - Per-board status workflows
* **Labels & priorities** - Flexible categorization

## Board And List Name Uniqueness

* Active and archived task board names are unique per workspace using `lower(btrim(name))`; only trashed boards with `deleted_at` set release the name.
* Non-deleted task list names are unique per board using `lower(btrim(name))`, regardless of status/category.
* Duplicate board writes return `409` with `code: "TASK_BOARD_NAME_EXISTS"`. Duplicate list writes return `409` with `code: "TASK_LIST_NAME_EXISTS"`.
* Existing duplicate rows are preserved by the forward migration that renames older duplicates with a deterministic ` (duplicate <id>)` suffix before the unique indexes are created. The migration does not move or delete tasks.

## Satellite Auth And API Ownership

`apps/tasks` is registered internally as the `tasks` app. All app-session
checks, cross-app token verification, and Web API allowlists must use
`targetApp: 'tasks'`.

The local `/api/auth/verify-app-token` route must delegate token validation to
the central Web verifier with `verificationBaseUrl: TTR_URL`. That handoff sets
both the host-only Tasks app-session cookie and the Web-issued app-session
cookie used by central API requests. Do not reintroduce Tasks-local Supabase
session validation for cross-app login.

Tasks must also keep a local `/login` page. It is a redirect-only route: valid
Tasks app-session cookies continue to `/personal/tasks` or the safe `next`
path, while missing or stale cookies redirect to `apps/web` login with a
`/verify-token` return URL. Without this route, a successful Web handoff can set
cookies and still leave the browser on a Next.js 404.

The Tasks app should not authenticate protected product APIs with local
Supabase Auth cookies. Fallback `/api/*` traffic is proxied through the local
catch-all API route, which strips `sb-*-auth-token` cookies before forwarding to
Web. Keep the corresponding Web routes app-session aware for
`targetApp: 'tasks'`; otherwise the browser can have valid Tasks cookies while
the data layer still loops through `401` and proxy-side `429` responses.

Task board/list count endpoints must compute counts in the database, preferably
through private service-role RPCs, and return one aggregate row per board/list.
Do not hydrate every matching `tasks` row in an API route just to count them.

## Rich Text Editor Notes

The task editor includes isolated React roots inside some Tiptap node views, especially task mention chips.

* Do not mount `next-themes` `ThemeProvider` inside those isolated `createRoot(...)` islands. `next-themes` injects an inline script, and Next.js 16 will warn or fail when that script is rendered from a client subtree instead of the app root.
* Resolve theme for those islands from `document.documentElement` and `prefers-color-scheme`, then pass that resolved value through local component state instead of re-creating app-wide providers.
* Add `referrerPolicy="strict-origin-when-cross-origin"` to every YouTube iframe path used by the task editor or its previews. Missing referrer policy can surface as YouTube `Error 153` even when the video URL itself is valid.

## Task Link Notes

* Canonical deep links for board tasks should use the board route with the task query parameter: `/{wsId}/tasks/boards/{boardId}?task={taskId}`.
* Tuturuuu Mobile normalizes production web links from `https://tuturuuu.com` into native routes for task boards, board task details, task estimates, task portfolio/project routes, and common workspace modules. Mobile accepts both `?task={taskId}` from web and `?taskId={taskId}` from native routes.
* Links can opt out of native app opening with `native=0` or `openInBrowser=1`. Use this for flows that must stay in the browser, such as debugging, web-only admin surfaces, or temporary compatibility issues.
* Avoid generating or persisting modal-only URLs such as `/{wsId}/tasks/{taskId}` for share/copy flows unless the dedicated task detail page is the explicit target. The board-backed URL is the reliable reloadable path used by the dashboard task dialog.
* When reloading or recovering a task deep link, resolve the task through the workspace-scoped task API (`/api/v1/workspaces/{wsId}/tasks/{taskId}`) rather than the current-user task dialog API. The current-user endpoint can 404 for valid board tasks because it is optimized for dialog hydration, not route recovery.
* Android opens supported links by default only after App Links verification succeeds. Keep `apps/web/public/.well-known/assetlinks.json` in sync with the production Android signing certificate fingerprint; if Play App Signing uses a different app-signing certificate than the repo release keystore, add the Play Console SHA-256 fingerprint before release.
* Keep `GoRouter.overridePlatformDefaultLocation` enabled in mobile. iOS can provide a universal link URL as Flutter's platform default route on cold start, and GoRouter will show a no-route page unless `app_links` is allowed to normalize that URL first.

## Mobile Board List Mode Notes

* Mobile board list mode hides `documents`, `done`, and `closed` task lists by default when no explicit list or status filter is selected.
* The filter sheet must disclose that default exclusion and indicate when a selected list/status filter overrides it, because those filters intentionally reveal otherwise hidden list categories.
* Moving a task between lists, including marking it done or closed, should refresh the affected source and destination lists instead of relying on a full board reload.

## Task Board Realtime And Cache Notes

* Successful task mutations must update visible TanStack Query caches in place before broadcasting. Patch all visible task shapes that can render the same card: `['tasks', boardId]`, every `['tasks-full', boardId, ...]` query, `['task', taskId]`, matching `['workspaceTask', wsId, taskId]`, and personal task buckets when present.
* Relation-only updates for labels, projects, and assignees should broadcast `task:relations-changed` and call the active board refresh with `{ invalidateTasks: false }`. Do not invalidate `['tasks', boardId]` or `['tasks-full', boardId]` after successful relation-only changes; refetching those visible arrays causes card flicker while the optimistic cache already has the relation data.
* Scalar task changes such as priority, dates, estimate, name, and list should patch visible task caches and broadcast `task:upsert` with the changed fields. Rollbacks should restore snapshots of the touched task ids instead of invalidating the whole board task cache.
* Realtime receivers may reconcile task detail, history, counts, and personal task queries in the background. The board page should revalidate loaded progressive-list data without invalidating visible task arrays so shared boards stay current without remounting cards.
* Task edit dialogs should keep dropdowns and popovers controlled from a single active overlay id within each dialog section. Opening another menu must close the previous one, and outside-click/Escape close behavior should flow through the same `onOpenChange` path.

## AI Journal Capture Notes

* The quick-journal endpoint at `/api/v1/workspaces/{wsId}/tasks/journal` is a two-step flow: preview requests may invoke AI, but save requests that already include reviewed `tasks` plus a `listId` must persist directly.
* Do not re-resolve the AI model, re-check credits, or regenerate tasks during that save step. A successful preview must be able to save even if model allocation changes afterward.
* When the destination picker allows switching workspaces, clear the currently selected board/list immediately in the same UI event before loading the new workspace boards. Effect-only cleanup is too late for save flows and can send a stale `listId` that belongs to a different workspace.
* If the final create request fails, reopen the destination-selection step with the reviewed tasks still in memory. Do not clear the journal draft optimistically before the insert succeeds, or users can lose the prompt they just refined.
* Any workspace alias accepted by the board/list bootstrap endpoints, such as `personal`, must also be normalized by the journal save route before membership and list checks. Otherwise the picker can show valid destinations while the final save still rejects the same workspace context.
* The journal save route should verify workspace membership first with the request-scoped client, then validate the chosen list against that normalized workspace. Admin-backed list checks are fine for consistency with selector data, but they must never bypass the caller's workspace access gate.
* Any workspace-scoped project validation in that same save route should follow the same pattern: verify membership with the request-scoped client first, then resolve valid `task_projects` IDs through the admin client scoped to the normalized workspace. Do not read protected workspace project tables directly from the caller-scoped client after access has already been established.
* Any top-level `labelIds` or reviewed task `labels[].id` in that save route must be checked against `workspace_task_labels.ws_id` for the normalized workspace before task rows are inserted. Foreign workspace label IDs must fail the request instead of being carried into `task_labels`; labels created by name can still be upserted through the admin-backed workspace path after membership passes.
* After membership passes, persist the journal-created `tasks` rows and their relation-table writes (`task_labels`, `task_project_tasks`, `task_assignees`, label upserts) through the same admin-backed workspace path, and stamp actor-owned columns such as `creator_id` explicitly. Do not mix admin-backed validation with caller-scoped writes or the save step can still fail with table-level permission errors.

## Task Hierarchy

```
Workspaces (Top Level)
└── Task Initiatives (Strategic grouping)
    └── Task Projects (Cross-board coordination)
        └── Workspace Boards (Kanban boards)
            └── Task Lists (Columns)
                └── Workspace Tasks (Individual items)
```

### 1. Workspaces

Container for all workspace resources.

```typescript theme={null}
// Get workspace
const { data: workspace } = await supabase
  .from("workspaces")
  .select("*")
  .eq("id", wsId)
  .single();
```

### 2. Task Initiatives

Strategic initiatives grouping related projects.

```sql theme={null}
CREATE TABLE task_initiatives (
  id text PRIMARY KEY,
  ws_id text REFERENCES workspaces(id) ON DELETE CASCADE,
  name text NOT NULL,
  description text,
  start_date date,
  end_date date,
  created_at timestamptz DEFAULT now()
);
```

**Usage:**

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function createInitiative(data: {
  wsId: string;
  name: string;
  description?: string;
  startDate?: Date;
  endDate?: Date;
}) {
  const supabase = await createClient();

  const { data: initiative, error } = await supabase
    .from("task_initiatives")
    .insert({
      ws_id: data.wsId,
      name: data.name,
      description: data.description,
      start_date: data.startDate?.toISOString().split("T")[0],
      end_date: data.endDate?.toISOString().split("T")[0],
    })
    .select()
    .single();

  if (error) throw error;
  return initiative;
}
```

### 3. Task Projects

Cross-functional projects coordinating tasks across boards.

```sql theme={null}
CREATE TABLE task_projects (
  id text PRIMARY KEY,
  ws_id text REFERENCES workspaces(id) ON DELETE CASCADE,
  name text NOT NULL,
  description text,
  created_at timestamptz DEFAULT now()
);

-- Link projects to initiatives
CREATE TABLE task_project_initiatives (
  project_id text REFERENCES task_projects(id) ON DELETE CASCADE,
  initiative_id text REFERENCES task_initiatives(id) ON DELETE CASCADE,
  PRIMARY KEY (project_id, initiative_id)
);

-- Link tasks to projects (cross-board)
CREATE TABLE task_project_tasks (
  project_id text REFERENCES task_projects(id) ON DELETE CASCADE,
  task_id text REFERENCES workspace_tasks(id) ON DELETE CASCADE,
  PRIMARY KEY (project_id, task_id)
);
```

**Usage:**

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function createProject(data: {
  wsId: string;
  name: string;
  description?: string;
  initiativeIds?: string[];
}) {
  const supabase = await createClient();

  // Create project
  const { data: project, error } = await supabase
    .from("task_projects")
    .insert({
      ws_id: data.wsId,
      name: data.name,
      description: data.description,
    })
    .select()
    .single();

  if (error) throw error;

  // Link to initiatives
  if (data.initiativeIds && data.initiativeIds.length > 0) {
    await supabase.from("task_project_initiatives").insert(
      data.initiativeIds.map((initiativeId) => ({
        project_id: project.id,
        initiative_id: initiativeId,
      })),
    );
  }

  return project;
}

export async function addTaskToProject(projectId: string, taskId: string) {
  const supabase = await createClient();

  const { error } = await supabase.from("task_project_tasks").insert({
    project_id: projectId,
    task_id: taskId,
  });

  if (error) throw error;
}
```

### 4. Workspace Boards

Kanban-style task boards.

Personal workspaces should always have a default active task board named
`Tasks`. Web and mobile task-board listing APIs ensure that board exists before
returning board data, so users should not see a prompt just to bootstrap their
personal task workflow. Mobile opens that default board directly from both the
Tasks and Boards surfaces unless the user disables default task-board navigation
in mobile Settings. The web settings dialog exposes the same preference for the
Boards page, keeping the board picker available for users who prefer the
previous flow.

#### Task list status semantics

Task list `status` is a workflow category, not the list display name. The
`review` status sits between `active` and `done`: moving a task into a review
list resolves the task for deadline/reminder filtering, but it must not set
`completed`, `completed_at`, or `closed_at`. Review tasks should keep the same
unchecked checkbox and non-completed visual treatment used by active tasks.

Review lists are for walkthrough or acceptance queues. Task cards in `review`,
`done`, or `closed` lists should not show due-date or overdue treatment because
those tasks are already resolved for deadline purposes. Mobile list mode still
shows `review` lists by default; only `documents`, `done`, and `closed` are
hidden when no explicit list or status filter is selected.

#### Task description realtime

Task descriptions use a Yjs CRDT document stored in
`tasks.description_yjs_state`. Once that state exists, treat it as the
authoritative collaborative source. The JSON `tasks.description` field is the
read/search/cache projection and must not overwrite a valid Yjs state just
because it differs; board caches and recently opened dialogs can hold stale
description JSON while another collaborator has newer Yjs edits.

Initialization or repair writes that seed the local Yjs document from persisted
state must use the provider sync origin so those changes are not broadcast as
fresh user edits. Persistence should serialize the description projection from
the Yjs update being saved, keeping `description` and `description_yjs_state`
from diverging under multi-user edit churn.

The `ttr` CLI uses the same description endpoint and TipTap/Yjs codec as the web
editor. Prefer first-class description commands over raw task update payloads:

```bash theme={null}
ttr tasks description get <task-id>
ttr tasks description set <task-id> --file notes.md --format markdown
ttr tasks description append <task-id> --text "Follow-up note"
ttr tasks description edit <task-id>
```

For local conversion or debugging, use the login-free codec commands:

```bash theme={null}
ttr tiptap parse --input notes.md --format markdown --output json
ttr tiptap encode --input description.json --format json --output yjs-base64
ttr tiptap decode --input state.txt --format yjs-base64 --output text
```

#### Task templates

Task templates are reusable single-task starters and are intentionally separate
from board templates. Store workspace templates in `task_templates` with
creator ownership, `private` or `workspace` visibility, archived timestamps,
default board/list ids, priority, dates, estimate, labels, assignees, projects,
and optional TipTap/Yjs description state.

Template API routes live under
`/api/v1/workspaces/{wsId}/task-templates`. They must accept authenticated
workspace members and `tasks` app-session JWTs from `ttr`, reject direct
task-board guests, and keep private templates readable only by their creator.
Workspace-visible template mutation should follow the task/project management
permission used by task-board management flows; private template mutation stays
scoped to the member owner.

The web templates hub at `/{locale}/{wsId}/tasks/templates` should default to
the Task Templates tab while preserving Board Templates as the secondary tab and
keeping existing board-template detail links stable.

The CLI supports both workspace-stored and local markdown templates:

```bash theme={null}
ttr task-templates list --no-update-check
ttr task-templates create "Bug report" --key bug-report --title "Investigate bug"
ttr task-templates export bug-report --file .tuturuuu/task-templates/bug-report.md
ttr task-templates import .tuturuuu/task-templates/bug-report.md
ttr tasks create --template bug-report --list <list-id> --name "Investigate checkout bug"
```

Local files under `.tuturuuu/task-templates/*.md` use YAML frontmatter for
template fields and the markdown body as the task description. Explicit
`ttr tasks create` flags must override template defaults so users can reuse a
starter without losing the current task context.

#### Mobile board caching

The mobile task-board flow restores workspace boards, board metadata, and per-list
task pages from `CacheStore` before network revalidation. Board detail pages
prefetch the first three lists on open and the focused Kanban list plus the next
two lists on horizontal page changes, so column swipes should not wait for one
on-demand request at a time. Cached task-page payloads must preserve the display
metadata used by cards, including assignees, labels, projects, and relationship
summaries.

The mobile board List tab hides tasks in `done` and `closed` lists by default.
Explicit status filters or list filters are treated as user intent and can show
those terminal lists again.

Mobile task details open description in a read-first fullscreen surface. Keep
that surface borderless and height-bounded under the task title, with editing
entered only from the edit FAB and save actions kept inside the editor chrome.

Task create/update API routes silently drop assignee ids that no longer belong
to the workspace. This lets stale task assignments be cleaned up during the next
save instead of blocking unrelated edits such as description updates.

#### Timeline board view

The task board Timeline is task-first: `TimelineBoard` keeps its public props
stable for board and project views, while the internal model renders one
scheduled task per readable row grouped by task list. Task titles and source
metadata belong in the sticky left column; timeline bars should communicate the
date range only, so narrow zoom levels never hide task names behind clipped bar
labels.

Keep drag, resize, schedule-from-unscheduled, edit, delete, and context-menu
actions routed through the same board mutation path used by the other views.
The row model must preserve personal and external task metadata and keep
unknown-list tasks grouped under the virtual unknown-list section instead of
dropping them from the schedule.

#### Personal external tasks

Personal task boards can reference tasks from other workspaces without cloning
or moving the source task. Assigned tasks from accessible source workspaces
appear by default in a virtual `External tasks` lane on each personal board.
The tasks API may use the server-side admin client to hydrate candidate source
tasks, but it must filter those rows through the current user's source
`workspace_members` rows with `type = 'MEMBER'` before returning personal
default or placed external cards. A guest row in the source workspace is not
enough to reveal task data through the personal board.

The virtual lane is presentation-only: it has no `task_lists` row, no list
actions, no create-task form, no list reordering, and can be collapsed per board
when the user wants to focus on planned lists. Personal boards default the lane
to collapsed when the board has no assigned external task references; once
assigned external tasks exist, the lane opens by default unless the user has
saved an explicit collapsed or expanded preference for that board.

By default the external lane shows only open source tasks from normal task
lists. Source tasks in `documents`, `review`, `done`, or `closed` lists are
filtered out unless the lane-level compact filters explicitly include resolved
tasks. Lane sorting is
also local to the external lane, so changing it does not reorder the user's real
personal lists.

External task cards should show the source workspace and the source board's
ticket identifier. Opening an external task must resolve the task by id through
the current-user task route so the dialog receives the source workspace, source
board, and source lists instead of the personal board context.

Per-user placement lives on `task_user_overrides` with `personal_board_id`,
`personal_list_id`, `personal_sort_key`, `personal_added_at`, and
`personal_placed_at`.

Placement writes must go through `upsert_personal_task_placement`. Drag clients
should pass the previous and next visible task ids when available, and the RPC
uses `calculate_personal_task_placement_sort_key` to compute a sort key against
the combined personal list of native personal tasks and placed external task
references.

Per-list task loading for personal boards must include the personal `boardId`
alongside `listId`; the API needs both values to load placed external
references for real personal lists and to keep already-placed external tasks out
of the virtual external lane.

Optimistic personal-placement moves must keep a fresh `_localMutationAt` marker
after the placement API response is merged. Loaded-list revalidation may still
return a pre-move page briefly, and the marker prevents that stale response from
evicting the placed external task until the server view catches up.
Optimistic placement writes should upsert the task into both the progressive
board task cache and any full-board cache that is already mounted, because stale
list reloads can briefly remove the card before the destination list refetches.
The card renderer should hide only the active pre-drop drag source; once a
placement mutation is optimistic, the real destination card must stay visible
even if the sortable layer still reports that task id as dragging.
Native task reorders and external personal-placement moves should both stamp a
fresh `_localMutationAt` marker on optimistic writes and on the server-confirmed
cache merge. Progressive list loading treats that marker as authority for
movement fields such as `list_id`, `sort_key`, `personal_list_id`, and
`personal_sort_key`, which prevents stale list pages from flashing a card back
to its previous position while the mutation response is still settling.

Personal-board list badges must use exact `includeCount` responses, not a
page-size estimate from progressive loading. External reference counts should be
resolved through `get_personal_task_board_external_counts` so placed external
tasks, default external-lane tasks, source-workspace access, and external-lane
filters stay aligned with the records the board can actually show.

Board source filters use `TaskFilters.sourceScope` to decide which task source
set is visible. `all_visible` preserves the existing board behavior,
`current_board` returns only tasks from the active board, and the two external
scopes return assigned-to-me tasks whose source board is not the active board.
`external_current_workspace` stays inside the current workspace, while
`external_specific` requires selected source workspace ids or source board ids;
when nothing is selected, the client should show an empty prompt instead of
running an unbounded external query.

Source-filtered list and timeline reads must go through the tasks API route,
which normalizes the route workspace, verifies request-scoped membership, then
uses the server-side Supabase admin client to call the
`private.list_task_source_filter_ids` RPC. The RPC rechecks target workspace
membership and source workspace access before returning paginated task ids plus
`total_count` for API-side hydration. Client UI must not call the private RPC
directly or trust client-provided source ids as authorization.

* `personal_board_id` points to the destination personal board after an
  external task is planned there.
* `personal_list_id` points to the real personal list that owns the planning
  position.
* Dragging from the virtual lane into a real personal list creates or updates
  the personal placement.
* Dragging a placed external task back to the virtual lane removes the personal
  placement so the task returns to the default external view.
* Personal-only label and project overlays live in
  `task_user_override_labels` and `task_user_override_projects`. Those rows
  reference labels/projects owned by the user's personal workspace and are
  loaded only for that owner.

Source task fields, including `tasks.list_id`, `task_labels`, and
`task_project_tasks`, must not be changed when a task appears in the external
lane, is planned into a personal list, is moved within the personal board, or
returns to the external lane unless the personal move changes the task's
workflow status. Routes that read personal board tasks should keep filtering
inaccessible source tasks out instead of surfacing stale placements to users who
no longer have source-workspace access.

When a placed external task moves to a personal list with a different workflow
status, the source task must also move to the first matching list on its original
board. `done` and `closed` targets require the matching terminal source list.
Non-terminal targets first try the same source status, then fall back to
`active` and `not_started` lists so tasks moved out of source `done` or `closed`
queues do not keep completed or closed card state.

```sql theme={null}
CREATE TABLE workspace_boards (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  ws_id text REFERENCES workspaces(id) ON DELETE CASCADE,
  name text NOT NULL,
  description text,
  created_at timestamptz DEFAULT now()
);
```

**Usage:**

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function createBoard(wsId: string, name: string) {
  const supabase = await createClient();

  const { data: board, error } = await supabase
    .from("workspace_boards")
    .insert({
      ws_id: wsId,
      name,
    })
    .select()
    .single();

  if (error) throw error;

  // Create default lists
  const defaultLists = ["To Do", "In Progress", "Done"];

  await supabase.from("task_lists").insert(
    defaultLists.map((listName, index) => ({
      board_id: board.id,
      name: listName,
      position: index,
    })),
  );

  return board;
}
```

### 5. Task Lists

Columns within boards (e.g., "To Do", "In Progress", "Done").

Task boards may contain multiple `closed` lists when teams need distinct terminal buckets such as `Abandoned`, `Not Planned`, or `Duplicate`. Treat `closed` as a status group for sorting/reporting, not a singleton board column.

```sql theme={null}
CREATE TABLE task_lists (
  id text PRIMARY KEY,
  board_id uuid REFERENCES workspace_boards(id) ON DELETE CASCADE,
  name text NOT NULL,
  position integer NOT NULL,
  created_at timestamptz DEFAULT now()
);
```

**Usage:**

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function reorderLists(boardId: string, listIds: string[]) {
  const supabase = await createClient();

  // Update positions based on array order
  const updates = listIds.map((listId, index) => ({
    id: listId,
    position: index,
  }));

  for (const update of updates) {
    await supabase
      .from("task_lists")
      .update({ position: update.position })
      .eq("id", update.id);
  }
}
```

### 6. Workspace Tasks

Individual work items.

Task titles support up to `1024` characters. Keep task-only validation on the dedicated task-title limit rather than the generic `name` limit so task routes, drafts, and database checks stay aligned.

```sql theme={null}
CREATE TABLE workspace_tasks (
  id text PRIMARY KEY,
  ws_id text REFERENCES workspaces(id) ON DELETE CASCADE,
  list_id text REFERENCES task_lists(id) ON DELETE SET NULL,
  name text NOT NULL,
  description text,
  priority integer CHECK (priority BETWEEN 0 AND 5),
  completed boolean DEFAULT false,
  start_date date,
  due_date date,
  created_by uuid REFERENCES workspace_users(id),
  created_at timestamptz DEFAULT now(),
  updated_at timestamptz DEFAULT now()
);
```

## Bucket Dump Feature

Rapidly capture thoughts as notes, then convert to tasks or projects.

### Notes Table

```sql theme={null}
CREATE TABLE notes (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  ws_id text REFERENCES workspaces(id) ON DELETE CASCADE,
  title text NOT NULL,
  content text,
  created_by uuid REFERENCES workspace_users(id),
  created_at timestamptz DEFAULT now()
);
```

### Convert Note to Task

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function convertNoteToTask(data: {
  noteId: string;
  listId: string;
  priority?: number;
}) {
  const supabase = await createClient();

  // Get note
  const { data: note } = await supabase
    .from("notes")
    .select("*")
    .eq("id", data.noteId)
    .single();

  if (!note) throw new Error("Note not found");

  // Create task from note
  const { data: task, error } = await supabase
    .from("workspace_tasks")
    .insert({
      ws_id: note.ws_id,
      list_id: data.listId,
      name: note.title,
      description: note.content,
      priority: data.priority,
      created_by: note.created_by,
    })
    .select()
    .single();

  if (error) throw error;

  // Optionally delete note
  await supabase.from("notes").delete().eq("id", data.noteId);

  return task;
}
```

### Convert Note to Project

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function convertNoteToProject(data: {
  noteId: string;
  initiativeId?: string;
}) {
  const supabase = await createClient();

  const { data: note } = await supabase
    .from("notes")
    .select("*")
    .eq("id", data.noteId)
    .single();

  if (!note) throw new Error("Note not found");

  // Create project from note
  const { data: project, error } = await supabase
    .from("task_projects")
    .insert({
      ws_id: note.ws_id,
      name: note.title,
      description: note.content,
    })
    .select()
    .single();

  if (error) throw error;

  // Link to initiative if provided
  if (data.initiativeId) {
    await supabase.from("task_project_initiatives").insert({
      project_id: project.id,
      initiative_id: data.initiativeId,
    });
  }

  // Delete note
  await supabase.from("notes").delete().eq("id", data.noteId);

  return project;
}
```

## Task Cycles (Sprints)

```sql theme={null}
CREATE TABLE task_cycles (
  id text PRIMARY KEY,
  ws_id text REFERENCES workspaces(id) ON DELETE CASCADE,
  name text NOT NULL,
  start_date date NOT NULL,
  end_date date NOT NULL,
  created_at timestamptz DEFAULT now()
);

-- Link tasks to cycles
ALTER TABLE workspace_tasks
ADD COLUMN cycle_id text REFERENCES task_cycles(id) ON DELETE SET NULL;
```

**Usage:**

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function createCycle(data: {
  wsId: string;
  name: string;
  startDate: Date;
  endDate: Date;
}) {
  const supabase = await createClient();

  const { data: cycle, error } = await supabase
    .from("task_cycles")
    .insert({
      ws_id: data.wsId,
      name: data.name,
      start_date: data.startDate.toISOString().split("T")[0],
      end_date: data.endDate.toISOString().split("T")[0],
    })
    .select()
    .single();

  if (error) throw error;
  return cycle;
}

export async function getActiveCycle(wsId: string) {
  const supabase = await createClient();
  const today = new Date().toISOString().split("T")[0];

  const { data: cycle } = await supabase
    .from("task_cycles")
    .select("*")
    .eq("ws_id", wsId)
    .lte("start_date", today)
    .gte("end_date", today)
    .single();

  return cycle;
}
```

## Task Estimation

```sql theme={null}
CREATE TABLE task_estimates (
  task_id text PRIMARY KEY REFERENCES workspace_tasks(id) ON DELETE CASCADE,
  type text NOT NULL, -- fibonacci, exponential, linear, tshirt
  value numeric NOT NULL,
  created_at timestamptz DEFAULT now()
);
```

**Estimation Types:**

* **Fibonacci**: 1, 2, 3, 5, 8, 13, 21
* **Exponential**: 1, 2, 4, 8, 16, 32
* **Linear**: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
* **T-Shirt**: XS (1), S (2), M (3), L (5), XL (8), XXL (13)

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function estimateTask(
  taskId: string,
  estimationType: "fibonacci" | "exponential" | "linear" | "tshirt",
  value: number,
) {
  const supabase = await createClient();

  const { error } = await supabase.from("task_estimates").upsert({
    task_id: taskId,
    type: estimationType,
    value,
  });

  if (error) throw error;
}
```

## Task Labels

```sql theme={null}
CREATE TABLE task_labels (
  id text PRIMARY KEY,
  ws_id text REFERENCES workspaces(id) ON DELETE CASCADE,
  name text NOT NULL,
  color text,
  created_at timestamptz DEFAULT now()
);

-- Link tasks to labels (many-to-many)
CREATE TABLE task_label_assignments (
  task_id text REFERENCES workspace_tasks(id) ON DELETE CASCADE,
  label_id text REFERENCES task_labels(id) ON DELETE CASCADE,
  PRIMARY KEY (task_id, label_id)
);
```

**Usage:**

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function createLabel(wsId: string, name: string, color: string) {
  const supabase = await createClient();

  const { data: label, error } = await supabase
    .from("task_labels")
    .insert({ ws_id: wsId, name, color })
    .select()
    .single();

  if (error) throw error;
  return label;
}

export async function assignLabel(taskId: string, labelId: string) {
  const supabase = await createClient();

  const { error } = await supabase
    .from("task_label_assignments")
    .insert({ task_id: taskId, label_id: labelId });

  if (error) throw error;
}
```

## Custom Task Statuses

```sql theme={null}
CREATE TABLE task_statuses (
  id text PRIMARY KEY,
  board_id uuid REFERENCES workspace_boards(id) ON DELETE CASCADE,
  name text NOT NULL,
  color text,
  position integer NOT NULL,
  is_completed boolean DEFAULT false,
  created_at timestamptz DEFAULT now()
);

-- Link tasks to statuses
ALTER TABLE workspace_tasks
ADD COLUMN status_id text REFERENCES task_statuses(id) ON DELETE SET NULL;
```

**Usage:**

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function createBoardWithStatuses(wsId: string, boardName: string) {
  const supabase = await createClient();

  // Create board
  const { data: board } = await supabase
    .from("workspace_boards")
    .insert({ ws_id: wsId, name: boardName })
    .select()
    .single();

  if (!board) throw new Error("Failed to create board");

  // Create default statuses
  const statuses = [
    { name: "Backlog", color: "#gray", position: 0, is_completed: false },
    { name: "To Do", color: "#blue", position: 1, is_completed: false },
    { name: "In Progress", color: "#yellow", position: 2, is_completed: false },
    { name: "Review", color: "#purple", position: 3, is_completed: false },
    { name: "Done", color: "#green", position: 4, is_completed: true },
  ];

  await supabase.from("task_statuses").insert(
    statuses.map((status) => ({
      board_id: board.id,
      ...status,
    })),
  );

  return board;
}
```

## Task Dependencies

Track task relationships and blockers.

```sql theme={null}
CREATE TABLE task_dependencies (
  task_id text REFERENCES workspace_tasks(id) ON DELETE CASCADE,
  depends_on_task_id text REFERENCES workspace_tasks(id) ON DELETE CASCADE,
  PRIMARY KEY (task_id, depends_on_task_id),
  CHECK (task_id != depends_on_task_id)
);
```

**Usage:**

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function addDependency(taskId: string, dependsOnTaskId: string) {
  const supabase = await createClient();

  const { error } = await supabase.from("task_dependencies").insert({
    task_id: taskId,
    depends_on_task_id: dependsOnTaskId,
  });

  if (error) throw error;
}

export async function getBlockedTasks(wsId: string) {
  const supabase = await createClient();

  const { data: tasks } = await supabase
    .from("workspace_tasks")
    .select(
      `
      *,
      task_dependencies!task_id (
        depends_on:workspace_tasks!depends_on_task_id (
          id,
          name,
          completed
        )
      )
    `,
    )
    .eq("ws_id", wsId)
    .eq("completed", false);

  // Filter tasks with incomplete dependencies
  return tasks?.filter((task) =>
    task.task_dependencies.some((dep: any) => !dep.depends_on.completed),
  );
}
```

## User Interface Components

### Board View

```tsx theme={null}
"use client";

import { trpc } from "@/trpc/client";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";

export function BoardView({
  wsId,
  boardId,
}: {
  wsId: string;
  boardId: string;
}) {
  const { data: lists } = trpc.boards.lists.useQuery({ boardId });
  const moveTask = trpc.tasks.move.useMutation();

  function handleDragEnd(result: any) {
    if (!result.destination) return;

    moveTask.mutate({
      taskId: result.draggableId,
      listId: result.destination.droppableId,
      position: result.destination.index,
    });
  }

  return (
    <DragDropContext onDragEnd={handleDragEnd}>
      <div className="flex gap-4">
        {lists?.map((list) => (
          <Droppable key={list.id} droppableId={list.id}>
            {(provided) => (
              <div
                ref={provided.innerRef}
                {...provided.droppableProps}
                className="min-w-75 bg-dynamic-gray/5 p-4 rounded-lg"
              >
                <h3>{list.name}</h3>
                {/* Task cards */}
                {provided.placeholder}
              </div>
            )}
          </Droppable>
        ))}
      </div>
    </DragDropContext>
  );
}
```

## Best Practices

### ✅ DO

1. **Use hierarchy appropriately**

   ```typescript theme={null}
   // Strategic: Initiative
   // Tactical: Project
   // Operational: Board → List → Task
   ```

2. **Leverage bucket dump**

   ```typescript theme={null}
   // Capture quickly, organize later
   await createNote({ title, content });
   ```

3. **Set task estimates**

   ```typescript theme={null}
   await estimateTask(taskId, "fibonacci", 8);
   ```

4. **Use labels for categorization**

   ```typescript theme={null}
   await assignLabel(taskId, "bug");
   await assignLabel(taskId, "high-priority");
   ```

5. **Track dependencies**
   ```typescript theme={null}
   await addDependency(taskId, blockerTaskId);
   ```

### ❌ DON'T

1. **Don't create deep hierarchies**

   ```typescript theme={null}
   // ❌ Bad: Too many nested levels
   ```

2. **Don't skip workspace isolation**

   ```typescript theme={null}
   // ❌ Bad
   .eq('id', taskId)

   // ✅ Good
   .eq('id', taskId).eq('ws_id', wsId)
   ```

## Related Documentation

* [Database Schema](/reference/database/schema-overview)
* [Data Fetching](/platform/architecture/data-fetching)
* [Authorization](/platform/architecture/authorization)
