Skip to Content

Tutorial 7: File Uploads & Media

Tasks in the project management app often need supporting materials — design mockups, spreadsheets, screenshots. In this tutorial you’ll add file attachment and cover image support to tasks using DaaS’s file management API and Buildpad’s media components.

By the end you’ll have:

  • A cover_image field (single image) on tasks
  • An attachments field (multiple files) on tasks
  • Drag-and-drop file upload from the task form
  • Image thumbnails displayed in the task list

Prerequisites

DaaS file storage — Files are stored in Supabase Storage and tracked in the daas_files system collection. File fields on your collections store a reference (UUID) to the file record. The DaaS API at GET /api/assets/:id serves the file with access control applied.

Add File Fields to the Tasks Collection

Ask your AI assistant to add two new fields to tasks:

/create-collection add field cover_image (file-image) and attachments (files) to tasks

This generates a migration adding:

  • cover_image — stores a single file UUID (interface: file-image)
  • attachments — stores a M2M relation to daas_files (interface: files)

Verify the Media Components is Installed

In the first part of tutorial you already installed all buildpad ui components including file related components. Confirm it appears in buildpad.json under installedComponents. If it isn’t there (e.g. in a custom setup), install it:

npx @buildpad/cli@latest add file-image files upload

This installs three components:

  • file-image — Single image picker with preview and crop
  • files — Multiple file manager with upload, reorder, and delete
  • upload — Drag-and-drop upload zone (used inside file-image and files)

Add Media Fields to the Task Form

CollectionForm automatically renders file-image and files interfaces directly from the DaaS field configuration — no code changes are needed in your task detail page. Once the fields are added in Step 1, open a task to confirm:

app/(authenticated)/content/tasks/[id]/page.tsx
"use client"; import { use } from "react"; import { useRouter } from "next/navigation"; import { CollectionForm } from "@/components/ui"; import { isNewItem } from "@/lib/buildpad/utils"; export default function TaskDetailPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = use(params); const router = useRouter(); return ( <CollectionForm collection="tasks" id={isNewItem(id) ? undefined : id} mode={isNewItem(id) ? "create" : "edit"} onSuccess={() => router.push("/content/tasks")} /> ); }

CollectionForm reads the field interface type from DaaS and renders the matching Buildpad component automatically. There is no fieldOverrides prop — the interface is configured in DaaS when the field is created.

Open a task at /content/tasks/<id>. You should now see:

  • An image picker with drag-and-drop upload for cover_image
  • A file list with upload support for attachments

Task form showing cover image picker and file attachments section

Show Cover Image Thumbnails in the Task List

File access is private by default. Always serve files via /api/assets/:id — not the raw Supabase Storage URL. The app proxy forwards your auth token to DaaS, which enforces the same RBAC rules as your collection permissions.

Add a renderCell callback to your tasks list page to display a thumbnail for the cover_image field:

app/(authenticated)/content/tasks/page.tsx
import { useCallback } from "react"; import { Image, Box } from "@mantine/core"; import { CollectionList } from "@/components/ui"; import type { Header } from "@/components/ui/vtable-types"; import type { AnyItem } from "@/lib/buildpad/types"; const renderCell = useCallback( (item: AnyItem, header: Header): React.ReactNode | null => { if (header.value === "cover_image") { const fileId = item["cover_image"] as string | null | undefined; if (!fileId) return <Box w={40} />; return ( <Image src={`/api/assets/${fileId}?width=40&height=40&fit=cover`} w={40} h={40} radius="sm" alt="Cover" /> ); } return null; }, [], ); // In your JSX: <CollectionList collection="tasks" fields={["cover_image", "title", "status", "priority", "due_date", "project_id.name"]} enableSearch enableFilter renderCell={renderCell} onCreate={() => router.push("/content/tasks/+")} onItemClick={(item) => router.push(`/content/tasks/${item.id}`)} />

cover_image stores a raw UUID string — not an object. Pass it directly as the file ID to /api/assets/:id. The app’s /api/assets/[id]/route.ts proxy forwards the request to DaaS with your auth token, enforcing RBAC.

The task list now shows a 40×40 thumbnail for any task that has a cover image.

Upload a File and Test

  1. Open a task at /content/tasks/<id>.
  2. Click the image picker and upload a PNG or JPEG — you should see a preview immediately.
  3. Click the attachments section and upload a PDF.
  4. Save the task.
  5. Return to /content/tasks — the thumbnail should appear in the list.
  6. Open the task again — the image and file should still be there.

File Field Reference

InterfaceMax filesSupported typesComponent
file1Anyfile
file-image1Images onlyfile-image
filesUnlimitedAnyfiles
upload1Configurableupload

DaaS Asset URL Parameters

The DaaS asset proxy supports image transformation via query parameters:

ParameterValuesDescription
widthnumberTarget width in pixels
heightnumberTarget height in pixels
fitcover, contain, fillResize mode
formatwebp, png, jpgOutput format
quality1–100JPEG/WebP quality

What’s Next

File uploads are working. In the final tutorial you’ll add server-side logic — validating data on creation and sending notifications when tasks are assigned.

Continue to Tutorial 8: Event Hooks & Extensions →

Last updated on