Server Actions
Last updated on 2026-09-07
Every mutation in the kit uses Next.js Server Actions. They run server-side, use the Supabase server client for reads and writes, and call revalidatePath() to refresh page data.
Architecture
Client Component --> Server Action --> Supabase Client --> Database
--> revalidatePath() --> Page Refresh
All Server Actions are in lib/actions/ and marked with "use server" at the top of each file.
Auth Actions (lib/actions/auth.ts)
| Action | Parameters | Description |
|---|---|---|
signIn |
email, password | Sign in with email/password |
signUp |
email, password, fullName | Create account; sets full_name in user metadata |
signOut |
-- | Clear session and redirect to /login |
resetPassword |
Send password reset email with redirect to /settings/password | |
updatePassword |
newPassword | Update password for authenticated user |
signInWithProvider |
provider ("google" or "github") |
Redirect to OAuth provider via Supabase Auth |
Project Actions (lib/actions/projects.ts)
| Action | Parameters | Description |
|---|---|---|
getProjects |
-- | All projects ordered by created_at desc |
getProject |
id | Single project by ID |
getProjectMembers |
projectId | Team members linked via project_members join table |
createProject |
name, prefix, emoji?, color?, description?, status?, default_view?, lead_id?, features? | Create project; maps features object to feat_cycles, feat_modules, etc. columns |
updateProject |
id, data | Update project details and feature flags |
deleteProject |
id | Delete project (cascades to tasks, cycles, modules, pages) |
addProjectMember |
projectId, memberId | Insert into project_members junction table |
removeProjectMember |
projectId, memberId | Delete from project_members junction table |
Task Actions (lib/actions/tasks.ts)
| Action | Parameters | Description |
|---|---|---|
getTasks |
filters (status?, priority?, assigneeId?, projectId?, cycleId?, moduleId?, limit?, offset?) | Paginated tasks with subtask joins and count |
getTask |
id | Single task with subtasks and comments (parallel queries) |
getTasksByProject |
projectId | All tasks for a project with subtasks |
getTasksByAssignee |
assigneeId | Open tasks for a user, ordered by priority then due_date |
createTask |
project_id, title, description?, status?, priority?, assignee_id?, labels?, due_date?, start_date?, cycle_id?, module_id?, estimate?, parent_id? | Auto-generates task number; writes to tasks and task_labels |
updateTask |
id, data (title?, description?, status?, priority?, assignee_id?, labels?, due_date?, start_date?, cycle_id?, module_id?, estimate?, parent_id?, project_id?) | Update task fields and sync label associations (delete + re-insert) |
updateTaskStatus |
id, status | Update only status (used by Kanban drag-and-drop) |
deleteTask |
id | Fetch project_id first, delete task, revalidate paths |
createSubtask |
taskId, data (title, completed?, assignee_id?, due_date?) | Create subtask linked to parent task |
updateSubtask |
id, data (title?, completed?, assignee_id?, due_date?) | Update subtask fields |
deleteSubtask |
id | Delete subtask |
addComment |
taskId, content | Create comment with authenticated user as author |
updateComment |
id, content | Update comment content |
deleteComment |
id | Delete comment |
addTaskLabel |
taskId, labelId | Insert into task_labels junction table |
removeTaskLabel |
taskId, labelId | Delete from task_labels junction table |
searchTasks |
query | Search tasks by title and description via ilike, limit 50 |
Cycle Actions (lib/actions/cycles.ts)
| Action | Parameters | Description |
|---|---|---|
getCycles |
projectId | All cycles for a project with task IDs, ordered by start_date desc |
getCycle |
id | Single cycle with task IDs |
getActiveCycle |
projectId | The active cycle for a project (status = 'Active') |
createCycle |
projectId, name, description?, startDate, endDate | Server-side date validation; inserts with status Upcoming, velocity 0 |
updateCycle |
id, data (name?, description?, startDate?, endDate?, status?, velocity?, plannedPoints?, completedPoints?) | Update cycle fields |
deleteCycle |
id | Delete cycle |
addTaskToCycle |
cycleId, taskId | Insert into cycle_tasks and set task's cycle_id |
removeTaskFromCycle |
cycleId, taskId | Delete from cycle_tasks and null task's cycle_id |
completeCycle |
id | Calculate velocity from cycle tasks, update status to Completed with computed points |
Module Actions (lib/actions/modules.ts)
| Action | Parameters | Description |
|---|---|---|
getModules |
projectId | All modules for a project with task IDs |
getModule |
id | Single module with task IDs |
createModule |
projectId, name, description?, leadId?, startDate?, endDate? | Insert with default status Planned |
updateModule |
id, data (name?, description?, leadId?, startDate?, endDate?, status?) | Update module fields |
deleteModule |
id | Delete module |
addTaskToModule |
moduleId, taskId | Insert into module_tasks and set task's module_id |
removeTaskFromModule |
moduleId, taskId | Delete from module_tasks and null task's module_id |
Goal Actions (lib/actions/goals.ts)
| Action | Parameters | Description |
|---|---|---|
getGoals |
-- | All goals with key results, ordered by due_date |
getGoal |
id | Single goal with key results |
createGoal |
title, description?, ownerId, dueDate, status? | Insert with default status On Track, progress 0 |
updateGoal |
id, data (title?, description?, ownerId?, dueDate?, status?, progress?) | Update goal fields |
deleteGoal |
id | Delete goal (cascade-deletes key results) |
createKeyResult |
goalId, data (title, target, current?, unit) | Insert KR, then recalculate goal progress |
updateKeyResult |
id, data (title?, current?, target?, unit?) | Update KR, then recalculate goal progress |
deleteKeyResult |
id | Lookup goal_id, delete KR, then recalculate goal progress |
recalculateGoalProgress |
goalId | Fetch all KRs, compute average (current/target * 100), update goal.progress |
Page Actions (lib/actions/pages.ts)
| Action | Parameters | Description |
|---|---|---|
getPages |
projectId | All pages for a project, ordered by created_at desc |
getPage |
id | Single page |
createPage |
projectId, title, content?, parentId? | Insert with authenticated user as author |
updatePage |
id, data (title?, content?) | Update page title and content |
deletePage |
id | Delete page |
Label Actions (lib/actions/labels.ts)
| Action | Parameters | Description |
|---|---|---|
getLabels |
-- | All labels ordered by group then name |
getLabel |
id | Single label |
createLabel |
name, color, group, description? | Insert label with label_group column |
updateLabel |
id, data (name?, color?, group?, description?) | Update label fields |
deleteLabel |
id | Delete label |
View Actions (lib/actions/views.ts)
| Action | Parameters | Description |
|---|---|---|
getSavedViews |
-- | Public views + current user's private views |
getSavedView |
id | Single saved view |
createSavedView |
name, description?, icon?, layout, filters?, groupBy?, sortBy?, isPublic? | Insert with current user as creator |
updateSavedView |
id, data (name?, description?, icon?, layout?, filters?, groupBy?, sortBy?, isPublic?) | Update view fields |
deleteSavedView |
id | Delete saved view |
togglePinView |
id | Toggle the is_pinned boolean |
Team Actions (lib/actions/team.ts)
| Action | Parameters | Description |
|---|---|---|
getTeamMembers |
-- | All profiles ordered by full_name |
getTeamMember |
id | Single profile |
updateProfile |
id, data (full_name?, avatar_url?, title?) | Update own profile (verifies user owns the profile) |
inviteMember |
email, role | Admin-only; sends invite via Supabase Auth admin client |
updateMemberRole |
id, role | Admin-only; update member role |
deactivateMember |
id | Admin-only; set status to Deactivated (prevents self-deactivation) |
Settings Actions (lib/actions/settings.ts)
API Keys
| Action | Parameters | Description |
|---|---|---|
getAPIKeys |
-- | API keys for current user |
createAPIKey |
name | Generate key with kpm_ prefix using crypto.randomUUID |
deleteAPIKey |
id | Delete API key (scoped to current user) |
Webhooks
| Action | Parameters | Description |
|---|---|---|
getWebhooks |
-- | Webhooks for current user |
createWebhook |
url, events[] | Insert with status Active |
updateWebhook |
id, data (url?, events?, status?) | Update webhook fields |
deleteWebhook |
id | Delete webhook |
Integrations
| Action | Parameters | Description |
|---|---|---|
getIntegrations |
-- | All integration configs ordered by name |
toggleIntegration |
id | Toggle connected boolean; sets last_synced when connecting |
updateIntegration |
id, data (connected?, lastSynced?) | Update integration fields |
Project Templates
| Action | Parameters | Description |
|---|---|---|
getProjectTemplates |
-- | All templates ordered by category then name |
getProjectTemplate |
id | Single template |
Notification Preferences
| Action | Parameters | Description |
|---|---|---|
getNotificationPreferences |
-- | Preferences for current user |
updateNotificationPreference |
id, data (inApp?, email?, slack?) | Update notification channels |
Analytics Actions (lib/actions/analytics.ts)
| Action | Parameters | Description |
|---|---|---|
getDashboardStats |
-- | Aggregate counts: total, completed, in-progress, overdue tasks, projects, members |
getDailyMetrics |
projectId?, days (default 30) | Daily created/completed counts from daily_metrics |
getBurndownData |
cycleId | Burndown points (ideal, actual, scope) from burndown_points |
getVelocityData |
projectId | Planned vs completed per sprint from velocity_points |
getCumulativeFlowData |
projectId, days (default 30) | Status counts per day from cumulative_flow_points |
getContributionData |
projectId? | Per-member created, completed, avg cycle time from contribution_data |
getStatusDistribution |
projectId? | Task count per status (computed from tasks table) |
getPriorityDistribution |
projectId? | Task count per priority (computed from tasks table) |
Activity Actions (lib/actions/activities.ts)
| Action | Parameters | Description |
|---|---|---|
getActivities |
projectId?, limit (default 50), offset (default 0) | Paginated activities with profile joins |
getRecentActivities |
limit (default 20) | Latest activities with profile joins |
createActivity |
type, entityType, entityId, entityTitle, projectId?, metadata? | Log activity with authenticated user |
Automation Actions (lib/actions/automations.ts)
| Action | Parameters | Description |
|---|---|---|
getAutomationRules |
projectId | All rules for a project |
getAutomationRule |
id | Single rule |
createAutomationRule |
projectId, name, trigger, condition?, action | Insert with enabled: true |
updateAutomationRule |
id, data (name?, trigger?, condition?, action?, enabled?) | Update rule fields |
deleteAutomationRule |
id | Delete rule |
toggleAutomationRule |
id | Toggle the enabled boolean |
Milestone Actions (lib/actions/milestones.ts)
| Action | Parameters | Description |
|---|---|---|
getMilestones |
projectId | All milestones for a project, ordered by date |
createMilestone |
projectId, name, date | Insert with completed: false |
updateMilestone |
id, data (name?, date?, completed?) | Update milestone fields |
deleteMilestone |
id | Delete milestone |
toggleMilestoneComplete |
id | Toggle the completed boolean |
Notification Actions (lib/actions/notifications.ts)
| Action | Parameters | Description |
|---|---|---|
getNotifications |
-- | All notifications for current user, ordered by created_at desc |
getUnreadCount |
-- | Count of unread notifications for current user |
markAsRead |
id | Set read: true on single notification |
markAllAsRead |
-- | Set read: true on all unread notifications for current user |
deleteNotification |
id | Delete notification |
Error Handling Pattern
All Server Actions follow the same pattern:
"use server"
import { createClient } from "@/lib/supabase/server"
import { revalidatePath } from "next/cache"
export async function updateTaskStatus(id: string, status: string) {
try {
const supabase = await createClient()
const { data: task, error } = await supabase
.from("tasks")
.update({ status } as never)
.eq("id", id)
.select("*, subtasks(*)")
.single()
if (error) {
return { error: error.message, data: null }
}
revalidatePath("/tasks")
if (task && task.project_id) {
revalidatePath(`/projects/${task.project_id}`)
}
return { error: null, data: mapTask(task) }
} catch (e) {
return { error: (e as Error).message, data: null }
}
}
Using Server Actions in Components
"use client"
import { updateTaskStatus } from "@/lib/actions/tasks"
import { toast } from "sonner"
function onDragEnd(result: DropResult) {
// Optimistic UI update first
setColumns(prev => { /* ... */ })
// Then persist to database
toast.success(`Moved "${movedTask.title}" to ${destStatus}`)
void updateTaskStatus(movedTask.id, destStatus).then(result => {
if (result.error) {
toast.error(result.error)
}
})
}
Supabase Client Selection
| Context | Client | RLS |
|---|---|---|
| Dashboard reads (all project data) | Server client | Yes (user session) |
| Analytics aggregates | Server client | Yes (user session) |
| Write operations (create, update) | Server client | Yes (user session) |
| Delete operations | Server client | Yes (user session) |
| Auth operations | Server client | N/A (Supabase Auth API) |
| Admin operations (invite, role change) | Admin client | Bypassed (service role) |
| Notifications (own only) | Server client | Yes (user session) |
The admin client (lib/supabase/admin.ts) uses SUPABASE_SERVICE_ROLE_KEY and is only used for admin team operations (inviting members, changing roles). It should never be exposed to the client.