Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Aking16/timify/llms.txt

Use this file to discover all available pages before exploring further.

Timify’s project actions are Next.js 16 Server Actions that run exclusively on the server. Every action first verifies the caller’s session via auth.api.getSession({ headers: await headers() }) and returns an error state immediately if no valid session is found. Successful mutations call revalidatePath and revalidateTag so the Next.js data cache stays consistent without a full page reload.

createProject

import { createProject } from "@/actions/projects/create-project";
Inserts a new project row into the projects table, scoped to the authenticated user’s userId. After a successful insert the action revalidates the root path and two cache tags so project lists and individual project views are refreshed.

Signature

export async function createProject(
  _prevState: CreateProjectState,
  formData: FormData
): Promise<CreateProjectState>

Parameters

name
string
required
Display name of the project. Must be at least 4 characters long. Validated by Zod before the database insert.
description
string
Optional free-text description of the project. Stored as-is; null is accepted.
hourlyRate
string
Optional billing rate as a numeric string (e.g. "75"). Converted to a number before storage. Defaults to 0 when omitted.

Return Type

success
boolean
true when the project was created successfully; false on validation or database failure.
message
string
Human-readable status message. Present on both success and failure.
project
Project
The newly created project row returned by Drizzle’s .returning() clause. Only present when success is true.

Side Effects

OperationValue
revalidatePath/
revalidateTagget-projects
revalidateTagretrieve-project

Usage Example

"use client";

import { useActionState } from "react";
import { createProject } from "@/actions/projects/create-project";

export function CreateProjectForm() {
  const [state, formAction, isPending] = useActionState(createProject, null);

  return (
    <form action={formAction}>
      <input name="name" placeholder="Project name (min 4 chars)" required />
      <input name="description" placeholder="Description (optional)" />
      <input name="hourlyRate" type="number" placeholder="Hourly rate (optional)" />

      <button type="submit" disabled={isPending}>
        {isPending ? "Creating…" : "Create Project"}
      </button>

      {state && (
        <p style={{ color: state.success ? "green" : "red" }}>
          {state.message}
        </p>
      )}
    </form>
  );
}
The project field in the return value is typed as typeof projects.$inferSelect, which is Drizzle’s inferred row type for the projects table. Import CreateProjectState from the same module if you need the full state type.

editProject

import { editProject } from "@/actions/projects/edit-project";
Updates an existing project record identified by id. The fields id, name, and isActive are required; description, hourlyRate, and color are optional. The isActive field must always be supplied as a string ("true" or "false") because it travels through FormData.

Signature

export async function editProject(
  _prevState: EditProjectState,
  formData: FormData
): Promise<EditProjectState>

Parameters

id
string
required
UUID of the project to update.
name
string
required
New display name. Must be between 4 and 32 characters.
isActive
string
required
Active status as a string boolean: "true" or "false". Converted to a real boolean via convertStringToBoolean before the database update.
description
string
Updated description. Optional.
hourlyRate
string
Updated billing rate as a numeric string. Optional; converted to number before storage.
color
string
Hex colour string (e.g. "#ef4444"). Optional.

Return Type

success
boolean
true when the update succeeded.
message
string
Human-readable status message.

Side Effects

OperationValue
revalidatePath/projects/
revalidateTagget-projects
revalidateTagretrieve-project

Usage Example

"use client";

import { useActionState } from "react";
import { editProject } from "@/actions/projects/edit-project";

export function EditProjectForm({ project }: { project: { id: string; name: string; isActive: boolean } }) {
  const [state, formAction, isPending] = useActionState(editProject, null);

  return (
    <form action={formAction}>
      <input type="hidden" name="id" value={project.id} />
      <input name="name" defaultValue={project.name} />
      <input name="hourlyRate" type="number" placeholder="Hourly rate" />
      <input name="color" type="color" defaultValue="#3b82f6" />
      {/* isActive must be the string "true" or "false" */}
      <input type="hidden" name="isActive" value={String(project.isActive)} />

      <button type="submit" disabled={isPending}>
        {isPending ? "Saving…" : "Save Changes"}
      </button>

      {state?.message && (
        <p>{state.message}</p>
      )}
    </form>
  );
}
isActive is required even when you are not changing it. Always forward the current boolean value as the string "true" or "false". Passing an empty string causes Zod validation to succeed but results in convertStringToBoolean returning false.

getProjects

import { getProjects } from "@/actions/projects/get-projects";
Returns all rows from the projects table. This action uses Next.js "use cache" (PPR-compatible) with the get-projects cache tag, so repeated calls are served from the data cache until the tag is revalidated.

Signature

export async function getProjects(): Promise<InferSelectModel<typeof projects>[]>

Parameters

This function accepts no parameters.

Return Type

(array)
Project[]
An array of all project rows. Each element has the same shape as the project field described under createProject above. Returns an empty array when no projects exist.
getProjects does not filter by userId at the query level — it relies on the cache tag get-projects being scoped correctly and is typically called inside a Server Component where route-level authentication is already enforced by the auth guard. Avoid calling it from unauthenticated contexts.

Usage Example

// app/page.tsx (Server Component)
import { getProjects } from "@/actions/projects/get-projects";

export default async function DashboardPage() {
  const projects = await getProjects();

  return (
    <ul>
      {projects.map((p) => (
        <li key={p.id}>
          {p.name} — {p.isActive ? "Active" : "Archived"}
        </li>
      ))}
    </ul>
  );
}

retrieveProject

import { retrieveProject } from "@/actions/projects/retrieve-project";
Fetches a single project by its UUID. Like getProjects, it uses "use cache" with the retrieve-project cache tag. Results are revalidated whenever createProject, editProject, or deleteProject mutates project data.

Signature

export async function retrieveProject(id: string): Promise<InferSelectModel<typeof projects>>

Parameters

id
string
required
UUID of the project to retrieve.

Return Type

(object)
Project
The matching project row. Has the same shape as the project field described under createProject. The function returns undefined (as an untyped gap) if no row matches — callers should guard against this.

Usage Example

// app/projects/[id]/page.tsx (Server Component)
import { retrieveProject } from "@/actions/projects/retrieve-project";
import { notFound } from "next/navigation";

export default async function ProjectPage({ params }: { params: { id: string } }) {
  const project = await retrieveProject(params.id);

  if (!project) notFound();

  return (
    <div>
      <h1>{project.name}</h1>
      <p>{project.description}</p>
      <p>Rate: ${project.hourlyRate}/hr</p>
    </div>
  );
}
Because retrieveProject is cached under the retrieve-project tag, you can safely call it multiple times in the same render tree without triggering redundant database queries.

deleteProject

import { deleteProject } from "@/actions/projects/delete-time-entry";
Permanently deletes a project row by ID. Because the projects table has onDelete: "cascade" on the time_entries.project_id foreign key, deleting a project also nullifies the projectId column on any associated time entries (set to null). The function takes a plain string argument rather than FormData.

Signature

export async function deleteProject(id: string): Promise<DeleteProjectState>

Parameters

id
string
required
UUID of the project to delete. Returns an error state if the string is empty.

Return Type

success
boolean
true when deletion succeeded.
message
string
Human-readable status message.

Side Effects

OperationValue
revalidatePath/projects/
revalidateTagget-projects
revalidateTagretrieve-project

Usage Example

"use client";

import { deleteProject } from "@/actions/projects/delete-time-entry";
import { useTransition } from "react";

export function DeleteProjectButton({ projectId }: { projectId: string }) {
  const [isPending, startTransition] = useTransition();

  function handleDelete() {
    startTransition(async () => {
      const result = await deleteProject(projectId);
      if (!result?.success) {
        alert(result?.message ?? "Deletion failed");
      }
    });
  }

  return (
    <button onClick={handleDelete} disabled={isPending}>
      {isPending ? "Deleting…" : "Delete Project"}
    </button>
  );
}
Deleting a project does not delete its time entries — it sets their projectId to null due to the onDelete: "set null" rule on the foreign key. Make sure your UI communicates this to the user before confirming deletion.

Build docs developers (and LLMs) love