Project Management Use Case
Build a project management backend with Backflow.
Overview
This guide demonstrates building a project management system with:
- Projects and workspaces
- Tasks with assignments
- Time tracking
- Comments and activity
- Notifications
Route Configuration
json
{
"routeAliases": {
"projects": "/api/v1/projects",
"tasks": "/api/v1/tasks",
"time": "/api/v1/time-entries"
}
}Projects
Create Project
json
{
"path": "/projects",
"method": "post",
"requireAuth": true,
"validation": {
"body": {
"name": { "type": "string", "required": true },
"description": { "type": "string" },
"workspace_id": { "type": "string", "required": true },
"start_date": { "type": "string", "format": "date" },
"due_date": { "type": "string", "format": "date" }
}
},
"supabaseQueries": [{
"table": "projects",
"operation": "insert",
"data": {
"name": "{{body.name}}",
"description": "{{body.description}}",
"workspace_id": "{{body.workspace_id}}",
"owner_id": "{{auth.sub}}",
"start_date": "{{body.start_date}}",
"due_date": "{{body.due_date}}",
"status": "active"
},
"select": "*"
}]
}List Projects
json
{
"path": "/workspaces/:workspaceId/projects",
"method": "get",
"requireAuth": true,
"supabaseQueries": [{
"table": "projects",
"operation": "select",
"select": "*, tasks(count), project_members(user_id)",
"filters": [
{ "column": "workspace_id", "operator": "eq", "value": "{{params.workspaceId}}" },
{ "column": "status", "operator": "neq", "value": "archived" }
],
"order": { "column": "updated_at", "ascending": false }
}]
}Get Project with Stats
json
{
"path": "/projects/:id",
"method": "get",
"requireAuth": true,
"supabaseQueries": [{
"table": "projects",
"operation": "select",
"select": "*, project_members(users(id, name, avatar)), tasks(id, status)",
"filters": [
{ "column": "id", "operator": "eq", "value": "{{params.id}}" }
],
"single": true
}],
"transform": {
"operations": [{
"type": "compute",
"fields": {
"task_stats": {
"total": "{{result.tasks.length}}",
"completed": "{{result.tasks.filter(t => t.status === 'done').length}}",
"in_progress": "{{result.tasks.filter(t => t.status === 'in_progress').length}}"
}
}
}]
}
}Tasks
Create Task
json
{
"path": "/projects/:projectId/tasks",
"method": "post",
"requireAuth": true,
"validation": {
"body": {
"title": { "type": "string", "required": true },
"description": { "type": "string" },
"assignee_id": { "type": "string" },
"priority": { "type": "string", "enum": ["low", "medium", "high", "urgent"] },
"due_date": { "type": "string", "format": "date" },
"labels": { "type": "array", "items": { "type": "string" } }
}
},
"workflow": {
"steps": [
{
"id": "create-task",
"type": "database",
"params": {
"table": "tasks",
"operation": "insert",
"data": {
"project_id": "{{params.projectId}}",
"title": "{{body.title}}",
"description": "{{body.description}}",
"assignee_id": "{{body.assignee_id}}",
"reporter_id": "{{auth.sub}}",
"priority": "{{body.priority || 'medium'}}",
"due_date": "{{body.due_date}}",
"status": "todo"
},
"select": "*, assignee:users!assignee_id(name, email)"
}
},
{
"id": "notify-assignee",
"type": "integration",
"condition": "{{body.assignee_id}}",
"integration": "sendgrid",
"action": "sendTemplate",
"params": {
"to": "{{steps.create-task.result.assignee.email}}",
"templateId": "task-assigned",
"dynamicTemplateData": {
"taskTitle": "{{steps.create-task.result.title}}",
"taskUrl": "{{env.APP_URL}}/tasks/{{steps.create-task.result.id}}"
}
}
},
{
"id": "log-activity",
"type": "database",
"params": {
"table": "activity_log",
"operation": "insert",
"data": {
"entity_type": "task",
"entity_id": "{{steps.create-task.result.id}}",
"action": "created",
"user_id": "{{auth.sub}}"
}
}
}
]
}
}Update Task Status
json
{
"path": "/tasks/:id/status",
"method": "patch",
"requireAuth": true,
"validation": {
"body": {
"status": { "type": "string", "enum": ["todo", "in_progress", "review", "done"], "required": true }
}
},
"supabaseQueries": [
{
"table": "tasks",
"operation": "update",
"data": {
"status": "{{body.status}}",
"completed_at": "{{body.status === 'done' ? 'now()' : null}}"
},
"filters": [
{ "column": "id", "operator": "eq", "value": "{{params.id}}" }
],
"select": "*"
},
{
"table": "activity_log",
"operation": "insert",
"data": {
"entity_type": "task",
"entity_id": "{{params.id}}",
"action": "status_changed",
"user_id": "{{auth.sub}}",
"metadata": { "new_status": "{{body.status}}" }
}
}
]
}List Tasks
json
{
"path": "/projects/:projectId/tasks",
"method": "get",
"requireAuth": true,
"supabaseQueries": [{
"table": "tasks",
"operation": "select",
"select": "*, assignee:users!assignee_id(id, name, avatar), labels(*)",
"filters": [
{ "column": "project_id", "operator": "eq", "value": "{{params.projectId}}" },
{ "column": "status", "operator": "in", "value": "{{query.status ? query.status.split(',') : ['todo', 'in_progress', 'review', 'done']}}" }
],
"order": [
{ "column": "priority", "ascending": false },
{ "column": "due_date", "ascending": true }
]
}]
}My Tasks
json
{
"path": "/my/tasks",
"method": "get",
"requireAuth": true,
"supabaseQueries": [{
"table": "tasks",
"operation": "select",
"select": "*, projects(id, name)",
"filters": [
{ "column": "assignee_id", "operator": "eq", "value": "{{auth.sub}}" },
{ "column": "status", "operator": "neq", "value": "done" }
],
"order": { "column": "due_date", "ascending": true }
}]
}Time Tracking
Start Timer
json
{
"path": "/tasks/:taskId/time/start",
"method": "post",
"requireAuth": true,
"supabaseQueries": [{
"table": "time_entries",
"operation": "insert",
"data": {
"task_id": "{{params.taskId}}",
"user_id": "{{auth.sub}}",
"started_at": "now()"
},
"select": "*"
}]
}Stop Timer
json
{
"path": "/time-entries/:id/stop",
"method": "post",
"requireAuth": true,
"supabaseQueries": [{
"table": "time_entries",
"operation": "update",
"data": {
"ended_at": "now()",
"duration": "EXTRACT(EPOCH FROM (now() - started_at))::integer"
},
"filters": [
{ "column": "id", "operator": "eq", "value": "{{params.id}}" },
{ "column": "user_id", "operator": "eq", "value": "{{auth.sub}}" }
],
"select": "*"
}]
}Log Time Entry
json
{
"path": "/tasks/:taskId/time",
"method": "post",
"requireAuth": true,
"validation": {
"body": {
"duration": { "type": "number", "required": true },
"description": { "type": "string" },
"date": { "type": "string", "format": "date" }
}
},
"supabaseQueries": [{
"table": "time_entries",
"operation": "insert",
"data": {
"task_id": "{{params.taskId}}",
"user_id": "{{auth.sub}}",
"duration": "{{body.duration}}",
"description": "{{body.description}}",
"started_at": "{{body.date || 'now()'}}"
},
"select": "*"
}]
}Comments
Add Comment
json
{
"path": "/tasks/:taskId/comments",
"method": "post",
"requireAuth": true,
"validation": {
"body": {
"content": { "type": "string", "required": true }
}
},
"workflow": {
"steps": [
{
"id": "add-comment",
"type": "database",
"params": {
"table": "comments",
"operation": "insert",
"data": {
"task_id": "{{params.taskId}}",
"user_id": "{{auth.sub}}",
"content": "{{body.content}}"
},
"select": "*, users(name, avatar)"
}
},
{
"id": "get-watchers",
"type": "database",
"params": {
"table": "task_watchers",
"operation": "select",
"select": "users(id, email)",
"filters": [
{ "column": "task_id", "operator": "eq", "value": "{{params.taskId}}" },
{ "column": "user_id", "operator": "neq", "value": "{{auth.sub}}" }
]
}
},
{
"id": "notify-watchers",
"type": "forEach",
"items": "{{steps.get-watchers.result}}",
"step": {
"type": "integration",
"integration": "sendgrid",
"action": "send",
"params": {
"to": "{{item.users.email}}",
"subject": "New comment on task",
"text": "{{steps.add-comment.result.content}}"
}
}
}
]
}
}List Comments
json
{
"path": "/tasks/:taskId/comments",
"method": "get",
"requireAuth": true,
"supabaseQueries": [{
"table": "comments",
"operation": "select",
"select": "*, users(id, name, avatar)",
"filters": [
{ "column": "task_id", "operator": "eq", "value": "{{params.taskId}}" }
],
"order": { "column": "created_at", "ascending": true }
}]
}Database Schema
sql
CREATE TABLE workspaces (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
owner_id UUID NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID REFERENCES workspaces(id),
name TEXT NOT NULL,
description TEXT,
owner_id UUID NOT NULL,
status TEXT DEFAULT 'active',
start_date DATE,
due_date DATE,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID REFERENCES projects(id),
title TEXT NOT NULL,
description TEXT,
assignee_id UUID,
reporter_id UUID NOT NULL,
status TEXT DEFAULT 'todo',
priority TEXT DEFAULT 'medium',
due_date DATE,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE time_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
task_id UUID REFERENCES tasks(id),
user_id UUID NOT NULL,
started_at TIMESTAMPTZ NOT NULL,
ended_at TIMESTAMPTZ,
duration INTEGER,
description TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
task_id UUID REFERENCES tasks(id),
user_id UUID NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE activity_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entity_type TEXT NOT NULL,
entity_id UUID NOT NULL,
action TEXT NOT NULL,
user_id UUID NOT NULL,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE task_watchers (
task_id UUID REFERENCES tasks(id),
user_id UUID NOT NULL,
PRIMARY KEY (task_id, user_id)
);