This part builds upon the previous GraphQL operations part by demonstrating how to implement file upload functionality with proper storage permissions. You’ll learn how to create storage buckets, configure upload permissions, and implement complete file management operations in a Next.js application using server and client components.
This is Part 5 in the Full-Stack Next.js Development with Nhost series. This part focuses on file storage, upload operations, and permission-based file access control in a production application using Next.js App Router patterns.

Full-Stack Next.js Development with Nhost

Prerequisites

What You’ll Build

By the end of this part, you’ll have:
  • A personal bucket so users can upload their own private files
  • File upload functionality
  • File management interface for viewing and deleting files
  • Security permissions ensuring users can only access their own files

Step-by-Step Guide

1

Create a Personal Storage Bucket

First, we’ll create a storage bucket where users can upload their personal files.In your Nhost project dashboard:
  1. Navigate to Database
  2. Change to schema.storage, then buckets
  3. Now click on + Insert on the top right corner.
  4. As id set personal, leave the rest of the fields blank and click on Insert at the bottom Create bucket
2

Configure Storage Permissions

Now we need to set up permissions for the storage bucket to ensure the user role can only upload, view, and delete their own files.
To upload files we need to grant permissions to insert on the table storage.files. Because we want to allow uploading files only to the personal bucket we will be using the bucket_id eq personal as a custom check. In addition, we are configuring a preset uploaded_by_user_id = X-Hasura-User-id, this will automatically extract the user_id from the session and set the column accordingly. Then we can use this in other permissions to allow downloading files and deleting them.upload files permissions
You can read more about storage permissions here
3

Create the File Upload System

Now let’s implement the Next.js file upload functionality using server actions, API routes, and client components. We’ll use server actions for upload and delete operations, and API routes for file viewing and downloading.
First, let’s create server actions to handle file upload and delete operations. Server actions run on the server and provide a secure way to handle file operations.
src/app/files/actions.ts
"use server";

import type { FileMetadata } from "@nhost/nhost-js/storage";
import { revalidatePath } from "next/cache";
import { createNhostClient } from "../../lib/nhost/server";

export interface ActionResult<T> {
  success: boolean;
  error?: string;
  data?: T;
}

export interface UploadFileData {
  file: FileMetadata;
  message: string;
}

export interface DeleteFileData {
  message: string;
}

export async function uploadFileAction(
  formData: FormData,
): Promise<ActionResult<UploadFileData>> {
  try {
    const nhost = await createNhostClient();
    const file = formData.get("file") as File;

    if (!file) {
      return { success: false, error: "Please select a file to upload" };
    }

    const response = await nhost.storage.uploadFiles({
      "bucket-id": "personal",
      "file[]": [file],
    });

    const uploadedFile = response.body.processedFiles?.[0];
    if (!uploadedFile) {
      return { success: false, error: "Failed to upload file" };
    }

    revalidatePath("/files");
    return {
      success: true,
      data: {
        file: uploadedFile,
        message: "File uploaded successfully!",
      },
    };
  } catch (error) {
    const message =
      error instanceof Error ? error.message : "An unknown error occurred";
    return { success: false, error: `Failed to upload file: ${message}` };
  }
}

export async function deleteFileAction(
  fileId: string,
  fileName: string,
): Promise<ActionResult<DeleteFileData>> {
  try {
    const nhost = await createNhostClient();

    if (!fileId) {
      return { success: false, error: "File ID is required" };
    }

    await nhost.storage.deleteFile(fileId);

    revalidatePath("/files");
    return {
      success: true,
      data: { message: `${fileName} deleted successfully` },
    };
  } catch (error) {
    const message =
      error instanceof Error ? error.message : "An unknown error occurred";
    return {
      success: false,
      error: `Failed to delete ${fileName}: ${message}`,
    };
  }
}
4

Update Navigation Component

Add a link to the files page in the server-side navigation component.
src/components/Navigation.tsx
import Link from "next/link";
import { createNhostClient } from "../lib/nhost/server";
import SignOutButton from "./SignOutButton";

export default async function Navigation() {
  const nhost = await createNhostClient();
  const session = nhost.getUserSession();

  return (
    <nav className="navigation">
      <div className="nav-container">
        <Link href="/" className="nav-logo">
          Nhost Next.js Demo
        </Link>

        <div className="nav-links">
          <Link href="/" className="nav-link">
            Home
          </Link>

          {session ? (
            <>
              <Link href="/todos" className="nav-link">
                Todos
              </Link>
              <Link href="/files" className="nav-link">
                Files
              </Link>
              <Link href="/profile" className="nav-link">
                Profile
              </Link>
              <SignOutButton />
            </>
          ) : (
            <>
              <Link href="/signin" className="nav-link">
                Sign In
              </Link>
              <Link href="/signup" className="nav-link">
                Sign Up
              </Link>
            </>
          )}
        </div>
      </div>
    </nav>
  );
}
5

Test Your File Upload System

Run your Next.js development server and test all the functionality:
npm run dev
Things to try out:
  1. Server-Side Protection: Try accessing /files while logged out - the middleware will redirect you to the home page.
  2. File Upload Flow: Sign in and navigate to the Files page using the navigation link. The server component will fetch your existing files, and the client component will handle uploads.
  3. Upload Different File Types: Upload various file types (images, documents, PDFs, etc.) to test the file type handling.
  4. View and Delete Files: Test the view functionality for different file types - images and PDFs will open in new tabs, while other files will download.
  5. User Isolation: Sign in with different accounts to verify users can only see their own files due to storage permissions.
  6. Server-Side Rendering: Notice how your file list loads immediately on page refresh since it’s fetched on the server, unlike client-only React apps.

Key Features Implemented