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 SvelteKit application.
This is Part 5 in the Full-Stack SvelteKit Development with Nhost series. This part focuses on file storage, upload operations, and permission-based file access control in a production application.
Full-Stack SvelteKit 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
Create a Personal Storage Bucket First, we’ll create a storage bucket where users can upload their personal files. In your Nhost project dashboard:
Navigate to Database
Change to schema.storage , then buckets
Now click on + Insert
on the top right corner.
As id set personal
, leave the rest of the fields blank and click on Insert at the bottom
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. You can read more about storage permissions here
Create the File Upload Component Now let’s implement the SvelteKit page component for file upload functionality. src/routes/files/+page.svelte
< script lang = "ts" >
import type { FileMetadata } from "@nhost/nhost-js/storage" ;
import { goto } from "$app/navigation" ;
import { auth } from "$lib/nhost/auth" ;
interface DeleteStatus {
message : string ;
isError : boolean ;
}
interface GraphqlGetFilesResponse {
files : FileMetadata [];
}
let fileInputRef = $ state < HTMLInputElement >();
let selectedFile = $ state < File | null >( null );
let uploading = $ state ( false );
let uploadResult = $ state < FileMetadata | null >( null );
let isFetching = $ state ( true );
let error = $ state < string | null >( null );
let files = $ state < FileMetadata []>([]);
let viewingFile = $ state < string | null >( null );
let deleting = $ state < string | null >( null );
let deleteStatus = $ state < DeleteStatus | null >( null );
// Redirect if not authenticated
$ effect (() => {
if ( ! $ auth . isLoading && ! $ auth . isAuthenticated ) {
void goto ( "/signin" );
}
});
// Format file size in a readable way
function formatFileSize ( bytes : number ) : string {
if ( bytes === 0 ) return "0 Bytes" ;
const sizes : string [] = [ "Bytes" , "KB" , "MB" , "GB" , "TB" ];
const i : number = Math . floor ( Math . log ( bytes ) / Math . log ( 1024 ));
return ` ${ parseFloat (( bytes / 1024 ** i ). toFixed ( 2 )) } ${ sizes [ i ] } ` ;
}
async function fetchFiles () {
isFetching = true ;
error = null ;
try {
// Use GraphQL to fetch files from the storage system
// Files are automatically filtered by user permissions
const response = await $ auth . nhost . graphql . request < GraphqlGetFilesResponse >(
{
query: `query GetFiles {
files {
id
name
size
mimeType
bucketId
uploadedByUserId
}
}` ,
},
);
if ( response . body . errors ) {
throw new Error (
response . body . errors [ 0 ]?. message || "Failed to fetch files" ,
);
}
files = response . body . data ?. files || [];
} catch ( err ) {
console . error ( "Error fetching files:" , err );
error = "Failed to load files. Please try refreshing the page." ;
} finally {
isFetching = false ;
}
}
// Fetch files when user session is available
$ effect (() => {
if ($ auth . session ) {
fetchFiles ();
}
});
function handleFileChange ( e : Event ) {
const target = e . target as HTMLInputElement ;
if ( target . files && target . files . length > 0 ) {
const file = target . files [ 0 ];
if ( file ) {
selectedFile = file ;
error = null ;
uploadResult = null ;
}
}
}
async function handleUpload () {
if ( ! selectedFile ) {
error = "Please select a file to upload" ;
return ;
}
uploading = true ;
error = null ;
try {
// Upload file to the personal bucket
// The uploadedByUserId is automatically set by the storage permissions
const response = await $ auth . nhost . storage . uploadFiles ({
"bucket-id" : "personal" ,
"file[]" : [ selectedFile ],
});
const uploadedFile = response . body . processedFiles ?.[ 0 ];
if ( uploadedFile === undefined ) {
throw new Error ( "Failed to upload file" );
}
uploadResult = uploadedFile ;
// Clear the form
selectedFile = null ;
if ( fileInputRef ) {
fileInputRef . value = "" ;
}
// Update the files list
files = [ uploadedFile , ... files ];
await fetchFiles ();
// Clear success message after 3 seconds
setTimeout (() => {
uploadResult = null ;
}, 3000 );
} catch ( err : unknown ) {
const message = ( err as Error ). message || "An unknown error occurred" ;
error = `Failed to upload file: ${ message } ` ;
} finally {
uploading = false ;
}
}
async function handleViewFile (
fileId : string ,
fileName : string ,
mimeType : string ,
) {
viewingFile = fileId ;
try {
// Get the file from storage
const response = await $ auth . nhost . storage . getFile ( fileId );
const url = URL . createObjectURL ( response . body );
// Handle different file types appropriately
if (
mimeType . startsWith ( "image/" ) ||
mimeType === "application/pdf" ||
mimeType . startsWith ( "text/" ) ||
mimeType . startsWith ( "video/" ) ||
mimeType . startsWith ( "audio/" )
) {
// Open viewable files in new tab
window . open ( url , "_blank" );
} else {
// Download other file types
const link = document . createElement ( "a" );
link . href = url ;
link . download = fileName ;
document . body . appendChild ( link );
link . click ();
document . body . removeChild ( link );
// Show download confirmation
const newWindow = window . open ( "" , "_blank" , "width=400,height=200" );
if ( newWindow ) {
newWindow . document . documentElement . innerHTML = `
<head>
<title>File Download</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; text-align: center; }
</style>
</head>
<body>
<h3>Downloading: ${ fileName } </h3>
<p>Your download has started. You can close this window.</p>
</body>
` ;
}
}
} catch ( err ) {
const message = ( err as Error ). message || "An unknown error occurred" ;
error = `Failed to view file: ${ message } ` ;
console . error ( "Error viewing file:" , err );
} finally {
viewingFile = null ;
}
}
async function handleDeleteFile ( fileId : string ) {
if ( ! fileId || deleting ) return ;
deleting = fileId ;
error = null ;
deleteStatus = null ;
const fileToDelete = files . find (( file ) => file . id === fileId );
const fileName = fileToDelete ?. name || "File" ;
try {
// Delete file from storage
// Permissions ensure users can only delete their own files
await $ auth . nhost . storage . deleteFile ( fileId );
deleteStatus = {
message: ` ${ fileName } deleted successfully` ,
isError: false ,
};
// Remove from local state
files = files . filter (( file ) => file . id !== fileId );
await fetchFiles ();
// Clear success message after 3 seconds
setTimeout (() => {
deleteStatus = null ;
}, 3000 );
} catch ( err ) {
const message = ( err as Error ). message || "An unknown error occurred" ;
deleteStatus = {
message: `Failed to delete ${ fileName } : ${ message } ` ,
isError: true ,
};
console . error ( "Error deleting file:" , err );
} finally {
deleting = null ;
}
}
</ script >
{# if ! $ auth . session }
< div class = "auth-message" >
< p > Please sign in to access file uploads. </ p >
</ div >
{: else }
< div class = "container" >
< header class = "page-header" >
< h1 class = "page-title" > File Upload </ h1 >
</ header >
< div class = "form-card" >
< h2 class = "form-title" > Upload a File </ h2 >
< div class = "field-group" >
< input
type = "file"
bind : this = { fileInputRef }
onchange = { handleFileChange }
style = "position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0;"
aria-hidden = "true"
tabindex = "-1"
/>
< button
type = "button"
class = "btn btn-secondary file-upload-btn"
onclick = { () => fileInputRef ?. click () }
>
< svg
width = " 40 "
height = " 40 "
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
role = "img"
aria-label = "Upload file"
>
< path
stroke-linecap = "round"
stroke-linejoin = "round"
stroke-width = " 2 "
d = "M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</ svg >
< p > Click to select a file </ p >
{# if selectedFile }
< p class = "file-upload-info" >
{ selectedFile . name } ( { formatFileSize ( selectedFile . size ) } )
</ p >
{/ if }
</ button >
</ div >
{# if error }
< div class = "error-message" > { error } </ div >
{/ if }
{# if uploadResult }
< div class = "success-message" > File uploaded successfully! </ div >
{/ if }
< button
type = "button"
onclick = { handleUpload }
disabled = { ! selectedFile || uploading }
class = "btn btn-primary"
style = "width: 100%"
>
{ uploading ? "Uploading..." : "Upload File" }
</ button >
</ div >
< div class = "form-card" >
< h2 class = "form-title" > Your Files </ h2 >
{# if deleteStatus }
< div class = { deleteStatus . isError ? "error-message" : "success-message" } >
{ deleteStatus . message }
</ div >
{/ if }
{# if isFetching }
< div class = "loading-container" >
< div class = "loading-content" >
< div class = "spinner" ></ div >
< span class = "loading-text" > Loading files... </ span >
</ div >
</ div >
{: else if files . length === 0 }
< div class = "empty-state" >
< svg
class = "empty-icon"
fill = "none"
stroke = "currentColor"
viewBox = "0 0 24 24"
aria-hidden = "true"
>
< path
stroke-linecap = "round"
stroke-linejoin = "round"
stroke-width = " 1.5 "
d = "M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
/>
</ svg >
< h3 class = "empty-title" > No files yet </ h3 >
< p class = "empty-description" > Upload your first file to get started! </ p >
</ div >
{: else }
< div style = "overflow-x: auto" >
< table class = "file-table" >
< thead >
< tr >
< th > Name </ th >
< th > Type </ th >
< th > Size </ th >
< th > Actions </ th >
</ tr >
</ thead >
< tbody >
{# each files as file ( file . id )}
< tr >
< td class = "file-name" > { file . name } </ td >
< td class = "file-meta" > { file . mimeType } </ td >
< td class = "file-meta" > { formatFileSize ( file . size || 0 ) } </ td >
< td >
< div class = "file-actions" >
< button
type = "button"
onclick = { () =>
handleViewFile (
file . id || "unknown" ,
file . name || "unknown" ,
file . mimeType || "unknown" ,
) }
disabled = { viewingFile === file . id }
class = "action-btn action-btn-edit"
title = "View File"
>
{ viewingFile === file . id ? "⏳" : "👁️" }
</ button >
< button
type = "button"
onclick = { () => handleDeleteFile ( file . id || "unknown" ) }
disabled = { deleting === file . id }
class = "action-btn action-btn-delete"
title = "Delete File"
>
{ deleting === file . id ? "⏳" : "🗑️" }
</ button >
</ div >
</ td >
</ tr >
{/ each }
</ tbody >
</ table >
</ div >
{/ if }
</ div >
</ div >
{/ if }
Update Navigation Links Add a link to the files page in the navigation layout. Update your src/routes/+layout.svelte
file to include the files link: src/routes/+layout.svelte
< script lang = "ts" >
import { onMount } from "svelte" ;
import { goto } from "$app/navigation" ;
import { page } from "$app/stores" ;
import { auth , initializeAuth , nhost } from "$lib/nhost/auth" ;
import "../app.css" ;
let { children } : { children ?: import ( "svelte" ). Snippet } = $ props ();
// Initialize auth when component mounts
onMount (() => {
return initializeAuth ();
});
// Helper function to determine if a link is active
function isActive ( path : string ) : string {
return $ page . url . pathname === path ? "nav-link active" : "nav-link" ;
}
async function handleSignOut () {
if ($ auth . session ) {
await nhost . auth . signOut ({
refreshToken: $ auth . session . refreshToken ,
});
void goto ( "/" );
}
}
</ script >
< div id = "root" >
< nav class = "navigation" >
< div class = "nav-container" >
< a href = "/" class = "nav-logo" > Nhost SvelteKit Demo </ a >
< div class = "nav-links" >
< a href = "/" class = "nav-link" > Home </ a >
{# if $ auth . isAuthenticated }
< a href = "/todos" class = { isActive ( '/todos' ) } > Todos </ a >
< a href = "/files" class = { isActive ( '/files' ) } > Files </ a >
< a href = "/profile" class = { isActive ( '/profile' ) } > Profile </ a >
< button
onclick = { handleSignOut }
class = "nav-link nav-button"
>
Sign Out
</ button >
{: else }
< a href = "/signin" class = "nav-link { isActive ( '/signin' ) } " >
Sign In
</ a >
< a href = "/signup" class = "nav-link { isActive ( '/signup' ) } " >
Sign Up
</ a >
{/ if }
</ div >
</ div >
</ nav >
< div class = "app-content" >
{# if children }
{@ render children ()}
{/ if }
</ div >
</ div >
Test Your File Upload System Run your SvelteKit application and test all the functionality: Things to try out:
Try signing in and out and see how the file upload page is only accessible when signed in.
Upload different types of files (images, documents, etc.)
View and delete files
Sign in with another account and verify you cannot see files from the first account
Key Features Implemented
Dedicated personal storage bucket with proper configuration for user file isolation.
User-friendly upload interface with file selection, preview, and progress feedback using SvelteKit’s reactive patterns.
Complete file listing with metadata, viewing capabilities, and deletion functionality.
Intelligent handling of different file types with appropriate viewing/download behavior.
Comprehensive error handling with user-friendly messages for upload and management operations using Svelte stores.