create table public.tenants ( id uuid primary key default gen_random_uuid(), name text not null check (char_length(name) between 1 and 120), slug text not null unique check (slug ~ '^[a-z0-9]+(?:-[a-z0-9]+)*$'), created_by uuid not null references public.profiles (id) on delete restrict, created_at timestamptz not null default now(), archived_at timestamptz);create index projects_tenant_idx on public.projects (tenant_id);
Purpose: Workspace/organization container for projectsKey Features:
URL-safe slug for routing (e.g., /app/:tenantSlug/projects)
Soft delete via archived_at
Unique constraint on slug
create type public.tenant_role as enum ('owner', 'admin', 'member');create table public.tenant_members ( tenant_id uuid not null references public.tenants (id) on delete cascade, user_id uuid not null references public.profiles (id) on delete cascade, role public.tenant_role not null, joined_at timestamptz not null default now(), primary key (tenant_id, user_id));create index tenant_members_user_idx on public.tenant_members (user_id);
Roles:
owner: Full control, can delete tenant
admin: Manage members and projects
member: Access tenant projects
create table public.profiles ( id uuid primary key references auth.users (id) on delete cascade, display_name text not null default 'User', avatar_url text, created_at timestamptz not null default now(), updated_at timestamptz not null default now());
Purpose: User profile informationTriggers:
handle_new_user(): Auto-creates profile on signup
Extracts display_name from OAuth metadata or email
create table public.projects ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants (id) on delete cascade, name text not null check (char_length(name) between 1 and 120), description text, created_by uuid not null references public.profiles (id) on delete restrict, created_at timestamptz not null default now(), archived_at timestamptz);
Key Relationships:
Belongs to exactly one tenant
Has many tasks, workflow columns, labels
Soft delete via archived_at
create type public.project_role as enum ('owner', 'editor', 'viewer');create table public.project_members ( project_id uuid not null references public.projects (id) on delete cascade, user_id uuid not null references public.profiles (id) on delete cascade, role public.project_role not null, joined_at timestamptz not null default now(), primary key (project_id, user_id));create index project_members_user_idx on public.project_members (user_id);
Roles:
owner: Full project control
editor: Create/edit tasks and settings
viewer: Read-only access
create type public.workflow_column_kind as enum ( 'backlog', 'todo', 'in_progress', 'done', 'custom');create table public.workflow_columns ( id uuid primary key default gen_random_uuid(), project_id uuid not null references public.projects (id) on delete cascade, name text not null check (char_length(name) between 1 and 80), kind public.workflow_column_kind not null default 'custom', position integer not null, wip_limit integer, definition_of_done text, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (project_id, position));
create type public.task_priority as enum ('p0', 'p1', 'p2');create table public.tasks ( id uuid primary key default gen_random_uuid(), project_id uuid not null references public.projects (id) on delete cascade, title text not null check (char_length(title) between 1 and 240), status_column_id uuid not null references public.workflow_columns (id) on delete restrict, start_date date, due_date date, priority public.task_priority not null default 'p1', order_rank numeric(18, 6) not null default 1000, description text, estimate numeric(10, 2), is_milestone boolean not null default false, completed_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint task_dates_valid check ( due_date is null or start_date is null or due_date >= start_date ));create index tasks_project_column_rank_idx on public.tasks (project_id, status_column_id, order_rank);create index tasks_project_due_date_idx on public.tasks (project_id, due_date);
Key Features:
Lexicographic order_rank for drag-and-drop ordering
Date validation constraint
Milestone support
Effort estimation
create table public.task_assignees ( task_id uuid not null references public.tasks (id) on delete cascade, user_id uuid not null references public.profiles (id) on delete cascade, created_at timestamptz not null default now(), primary key (task_id, user_id));create index task_assignees_user_idx on public.task_assignees (user_id);
Purpose: Many-to-many relationship for task assignments
create table public.task_labels ( id uuid primary key default gen_random_uuid(), project_id uuid not null references public.projects (id) on delete cascade, name text not null, color text not null, created_at timestamptz not null default now(), unique (project_id, name));create table public.task_label_links ( task_id uuid not null references public.tasks (id) on delete cascade, label_id uuid not null references public.task_labels (id) on delete cascade, created_at timestamptz not null default now(), primary key (task_id, label_id));
Purpose: Project-scoped tags for task categorization
create table public.task_checklist_items ( id uuid primary key default gen_random_uuid(), task_id uuid not null references public.tasks (id) on delete cascade, title text not null check (char_length(title) between 1 and 240), is_done boolean not null default false, position integer not null default 0, created_at timestamptz not null default now(), updated_at timestamptz not null default now());
Purpose: Sub-tasks or action items within a task
create table public.task_attachments ( id uuid primary key default gen_random_uuid(), task_id uuid not null references public.tasks (id) on delete cascade, url text not null, title text, created_at timestamptz not null default now());
Purpose: File uploads or external links
create type public.dependency_type as enum ('FS');create table public.task_dependencies ( id uuid primary key default gen_random_uuid(), project_id uuid not null references public.projects (id) on delete cascade, predecessor_task_id uuid not null references public.tasks (id) on delete cascade, successor_task_id uuid not null references public.tasks (id) on delete cascade, type public.dependency_type not null default 'FS', created_at timestamptz not null default now(), constraint task_dependency_no_self check (predecessor_task_id <> successor_task_id), unique (project_id, predecessor_task_id, successor_task_id, type));create index task_dependencies_project_link_idx on public.task_dependencies (project_id, predecessor_task_id, successor_task_id);
Dependency Types:
FS (Finish-to-Start): Predecessor must finish before successor starts
Future support planned for:
SS (Start-to-Start)
FF (Finish-to-Finish)
SF (Start-to-Finish)
create table public.task_activity ( id uuid primary key default gen_random_uuid(), project_id uuid not null references public.projects (id) on delete cascade, task_id uuid not null references public.tasks (id) on delete cascade, actor_id uuid not null references public.profiles (id) on delete restrict, event_type text not null, payload jsonb not null default '{}'::jsonb, created_at timestamptz not null default now());
alter table public.tenants enable row level security;alter table public.tenant_members enable row level security;-- Users can only see tenants they belong tocreate policy "tenants_select_members"on public.tenants for select to authenticatedusing (public.is_tenant_member(id));-- Only owners can update/delete tenantscreate policy "tenants_update_owner"on public.tenants for update to authenticatedusing (public.is_tenant_owner(id))with check (public.is_tenant_owner(id));
alter table public.projects enable row level security;-- Users can only see projects they're members ofcreate policy "projects_select_members"on public.projects for select to authenticatedusing (public.is_project_member(id));-- Only editors can modify taskscreate policy "tasks_update_editors"on public.tasks for update to authenticatedusing (public.can_edit_project(project_id))with check (public.can_edit_project(project_id));
RLS policies are automatically enforced on all queries, even from backend functions. This provides defense-in-depth security.
alter table public.tasks enable row level security;-- Members can view all project taskscreate policy "tasks_select_members"on public.tasks for select to authenticatedusing (public.is_project_member(project_id));-- Editors can create/update/delete taskscreate policy "tasks_insert_editors"on public.tasks for insert to authenticatedwith check (public.can_edit_project(project_id));
create index tasks_project_column_rank_idx on tasks (project_id, status_column_id, order_rank);create index tasks_project_due_date_idx on tasks (project_id, due_date);