Add the Files Module
A ready-made /files experience installed via the Buildpad CLI (Copy & Own — the code is copied into your project). It sits on top of the DaaS Files API and the Upload interface, so you never hand-build upload zones, folder navigation, previews, or bulk actions.
Two screens ship together:
FileManager— the/fileslibrary: drag-and-drop upload, import-from-URL, folder navigation with breadcrumbs, grid/list views, search, multi-select, bulk delete.FileDetail— the/files/[id]page: a Preview tab (image/video/audio inline, PDF in an iframe) and a Details tab with an editable metadata form, plus single delete.
CRITICAL: Never Create These Manually
Like all Buildpad UI, the Files module is CLI-installed and owned as source. Do not hand-write components/ui/file-manager/*, the upload component, or the useFiles/useFolders hooks in lib/buildpad/hooks/ — scaffold them.
Prerequisites Check
node --version && pnpm --version && npx --version
Requires Node.js v24 LTS and pnpm v10+ (see add-buildpad for install guidance). The module must render under the authenticated layout (buildpad init generates app/(authenticated)/ with a DaaSProviderWrapper) so DaaSProvider header injection is in scope — the hooks call your /api/files/*, /api/folders/*, and /api/assets/* proxy routes. Inside the module use useDaaSContext (it throws when mounted outside the wrapper, which surfaces the layout mistake early); useDaaSContextOptional exists for components that must also render without a provider.
DaaS CORS must be configured first. FileManager/FileDetail fetch authenticated DaaS endpoints (/users/me, /permissions/me, file/folder items) from the browser with credentials: 'include'. DaaS's default cors_origins: ["*"] is incompatible with credentialed requests — the browser blocks every preflight (Access-Control-Allow-Origin header must not be the wildcard '*'). Set explicit origins before mounting the module (see daas-platform "CORS must use explicit origins" — Bugs 17+25 — and debugging-and-error-recovery). This is not Files-specific: it affects every authenticated Buildpad component.
Installation
The Files module is opt-in — it is not part of bootstrap.
# 1. Add the module (copies app/files/ shells, components/ui/file-manager/,
# the upload component, and the useFiles/useFolders hooks)
npx @buildpad/cli@latest add files-routes --cwd /path/to/project
# 2. Add the DaaS proxy routes it needs (if not already present)
npx @buildpad/cli@latest add api-routes --cwd /path/to/project
The docs use the shorthand
buildpad add files-routeswhen the CLI is installed globally;npx @buildpad/cli@latest add …is the equivalent no-install form used across these skills.
If the CLI command fails,
@buildpad/cliIS published to npm — verify withnpm view @buildpad/cli versionbefore assuming otherwise or reaching for a local clone. The CLI fetches its registry fromraw.githubusercontent.com, so confirm the environment can reach both npm and the GitHub raw CDN. A local clone is only needed for CLI development, never to consume components.
api-routes includes exactly the routes this module needs: app/api/files/route.ts, app/api/files/[id]/route.ts, app/api/files/[id]/download/route.ts, app/api/files/import/route.ts, app/api/folders/route.ts, app/api/folders/[id]/route.ts, and app/api/assets/[id]/route.ts. Each one is a thin proxy that forwards the request and the session's Authorization header to DaaS — none of them talks to Supabase Storage or daas_files directly. CLI releases that ship the direct-to-storage upload add one more proxy, app/api/files/signed-url/route.ts (see below).
How Uploads Work
Uploads go through useFiles().uploadFiles — never hand-write the upload flow or the routes behind it.
On CLI releases up to 2.2.0 the hook sends multipart/form-data to POST /api/files; the proxy forwards it to DaaS, which stores the object in Supabase Storage and creates the daas_files record. Both hops buffer the whole body, so uploads are bounded by the request body limit.
DaaS 0.1.93 added a direct-to-storage path for large files. CLI releases that ship app/api/files/signed-url/route.ts (a proxy to DaaS POST/DELETE /api/files/signed-url) use it from uploadFiles automatically:
POST /api/files/signed-urlwith{ filename_download, type, filesize, folder? }— the first three are required, andfoldermust be an existingdaas_foldersid — returns201 { data: { uploadUrl, token, uploadToken, primaryKey, storagePath, filenameDisk, storageBucket } }.PUT uploadUrlwith the raw bytes andContent-Type: file.type. This request goes to the Supabase Storage origin, not to your app, so that host must allow cross-originPUTfrom the app origin — otherwise step 1 succeeds and step 2 fails with a CORS error.POST /api/files(JSON) with{ upload_token: uploadToken, filename_download, title?, description? }returns201 { data: <file> }. DaaS takesid,filename_disk,storage, andfolderfrom the token and readsfilesizeandtypeback from the stored object — any of those fields sent by the client are ignored.
If step 2 or 3 fails, the hook calls DELETE /api/files/signed-url with { upload_token } (204) so the object is not orphaned in the bucket. Tokens are bound to the user who requested them, expire after two hours, and are single-use.
Against a DaaS instance older than 0.1.93 (no /api/files/signed-url), the hook detects the 404 on the first upload and falls back to multipart for the rest of the session. No configuration is needed either way.
Size and MIME limits come from the Supabase bucket (Supabase Studio → bucket settings), falling back to the DaaS env vars FILES_MAX_UPLOAD_SIZE (default 100 MB) and FILES_MIME_TYPE_ALLOW_LIST (default: all types). FILES_STORAGE_BUCKET_ALLOW_LIST limits which buckets a signed upload may target (default files).
Never insert into daas_files directly, and never mint signed URLs from the app. Bypassing DaaS skips its permission, size, type, bucket, and ownership checks, and the column default storage = 'local' names a bucket that does not exist — every later /api/assets/:id call then fails with FILE_NOT_FOUND ("Failed to retrieve file"). Through DaaS, storage comes from the upload token and defaults to the files bucket.
The Routes
app/files/
├── layout.tsx # optional shell wrapper (centers content)
├── page.tsx # /files — FileManager library view
└── [id]/page.tsx # /files/[id] — FileDetail preview + metadata view
Usage
List view — FileManager
// app/files/page.tsx
"use client";
import { useRouter } from "next/navigation";
import { FileManager } from "@/components/ui/file-manager";
export default function FilesPage() {
const router = useRouter();
return <FileManager => router.push(`/files/${file.id}`)} />;
}
| Prop | Type | Default | Purpose |
|---|---|---|---|
onFileClick |
(file: FileUpload) => void |
— | Open a file (e.g. navigate to its detail page) |
pageSize |
number |
24 |
Items per page |
defaultView |
'grid' | 'list' |
'grid' |
Initial view mode |
enableFolders |
boolean |
true |
Set false for a flat library with no folder UI |
filesCollection |
string |
'daas_files' |
DaaS collection used for RBAC checks |
Detail view — FileDetail
// app/files/[id]/page.tsx
"use client";
import { use } from "react";
import { useRouter } from "next/navigation";
import { FileDetail } from "@/components/ui/file-manager";
export default function FileDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params);
const router = useRouter();
return (
<FileDetail
id={id}
=> router.push("/files")}
=> router.push("/files")}
/>
);
}
FileDetail props: id (required), onBack?, onDeleted?, filesCollection? (default 'daas_files'). Metadata edits, folder moves, focal point, and delete are all permission-gated.
The Data Layer (reuse anywhere)
Both screens are thin wrappers over two hooks from @/lib/buildpad/hooks:
import { useFiles, useFolders } from "@/lib/buildpad/hooks";
useFiles()→uploadFiles,fetchFiles,getFile,updateFile,replaceFile,getDownloadUrl,deleteFile,deleteFiles,importFromUrl(+loading,error).updateFiletakes the full editable metadata set:await updateFile(fileId, { title: "Cover image", description: "Homepage hero", tags: ["marketing", "hero"], location: "HQ", filename_download: "hero.png", });useFolders()→fetchFolders,createFolder,updateFolder,deleteFolder(+loading,error). Passparent: nullfor the root:const rootFolders = await fetchFolders({ parent: null }); const folder = await createFolder({ name: "Campaigns", parent: null });
Thumbnails & downloads
Assets are served by /api/assets/[id]. Build transform URLs with getAssetUrl from @/lib/buildpad/types:
import { getAssetUrl } from "@/lib/buildpad/types";
getAssetUrl(id, { width: 240, height: 240, fit: "cover" }); // grid thumbnail
getAssetUrl(id, { download: true }); // download link
Never put a raw DaaS URL (https://<daas-host>/api/assets/:id) in an <img src>, <video src>, <iframe src>, or <a href>: the browser sends no Authorization header, so DaaS answers 401 Unauthorized. Always go through the app's /api/assets/[id] proxy, which attaches the session token — getAssetUrl already builds those URLs (getAssetUrl(id) for previews, getAssetUrl(id, { download: true }) for downloads).
Preview Behaviour (Details tab)
| MIME type | Rendering |
|---|---|
| Image | <img> inline |
| Video | HTML5 <video> player |
| Audio | HTML5 <audio> player |
Embedded <iframe> |
|
| Anything else | Icon + download button |
Post-Install Validation
npx @buildpad/cli@latest status --cwd /path/to/project
npx @buildpad/cli@latest validate --cwd /path/to/project
cd /path/to/project && pnpm build
See It Live
Files Storybook (port 6009): pnpm --filter @buildpad/ui-files storybook. The FileManager (DaaS) story connects to a real DaaS instance via the storybook-host proxy.
Related
- add-buildpad — the underlying CLI and the low-level
Upload/FileInterface/FileImagefield widgets. - buildpad-reference — full component catalog.