Skip to main content

Storage API

File storage in an Instroc app is handled by four hooks that live in @instroc/data. There is no separate storage package, so if you see an import from @instroc/storage anywhere, it is wrong. This page covers uploading, listing, deleting, and resolving file URLs, plus how public and private buckets behave when a file is downloaded.

useFileUpload

useFileUpload() returns an upload function and its state:

const { upload, uploading, error, reset } = useFileUpload();

upload(file, {bucket, path, metadata}) takes a browser File and an options object. bucket is "public" or "private", path places the file within the bucket, and metadata is an optional object stored with the file. It resolves to a StoredFile. Files are capped at 50 MB each.

const stored = await upload(file, {
bucket: "public",
path: `avatars/${user.id}`,
});
// stored.url is ready to use in an <img>

useFileList

useFileList({bucket, prefix, limit, offset}) fetches a page of files:

const { files, count, hasMore, loading, error, refetch } = useFileList({
bucket: "public",
prefix: "gallery/",
limit: 24,
});

files is an array of StoredFile, count is the total number of matches, and hasMore tells you whether another page exists beyond the current offset.

useFileDelete

useFileDelete() returns {remove, removing, error}. Call remove(fileId) with the file's id to delete it.

useFileUrl

useFileUrl(fileId) returns the file's URL as a string, or null when the id is unknown. It is a pure lookup and makes no network request, so it is safe to call in render for every item in a list.

The StoredFile shape

Every hook that returns files uses the same shape:

{
id: string;
url: string;
path: string;
filename: string;
mimeType: string;
sizeBytes: number;
bucket: "public" | "private";
metadata?: object;
uploadedBy?: string;
createdAt: string;
}

Public vs private buckets

The bucket you choose decides who can download a file. Files in the public bucket are viewable by anyone with the link, which makes them right for avatars, product images, and anything you render in an <img> tag for all visitors. Files in the private bucket can only be downloaded by signed-in users of your app, so use it for receipts, user documents, and anything that should not leak through a shared URL.

tip

Choose the bucket at upload time based on the least access the file needs. Moving a sensitive file out of the public bucket later does not un-share a link someone already copied.

Lists refresh automatically

After a successful upload or delete, active useFileList results for the affected bucket refresh on their own, the same way data mutations refresh queries on their table. You do not need to call refetch after upload or remove; keep refetch for explicit user actions like a refresh button.

Example: avatar upload with preview

import { useState } from "react";
import { useFileUpload } from "@instroc/data";
import { useAuth } from "@instroc/auth";

function AvatarUploader() {
const { user, updateProfile } = useAuth();
const { upload, uploading, error } = useFileUpload();
const [preview, setPreview] = useState<string | null>(null);

async function onPick(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
setPreview(URL.createObjectURL(file));
const stored = await upload(file, {
bucket: "public",
path: `avatars/${user?.id}`,
});
await updateProfile({ avatar_url: stored.url });
}

return (
<div>
{preview && <img src={preview} alt="Avatar preview" width={96} />}
<input type="file" accept="image/*" onChange={onPick} />
{uploading && <p>Uploading...</p>}
{error && <p>Upload failed. Try a smaller image.</p>}
</div>
);
}
import { useState } from "react";
import { useFileList } from "@instroc/data";

function Gallery() {
const [offset, setOffset] = useState(0);
const { files, hasMore, loading } = useFileList({
bucket: "public",
prefix: "gallery/",
limit: 12,
offset,
});

if (loading && files.length === 0) return <p>Loading photos...</p>;

return (
<div>
<div className="grid">
{files.map((f) => (
<img key={f.id} src={f.url} alt={f.filename} />
))}
</div>
{hasMore && (
<button onClick={() => setOffset(offset + 12)}>Load more</button>
)}
</div>
);
}

For managing buckets and browsing files in the editor, see the Storage panel described in Backend storage. For a product-level view of what file storage is good for, see File storage.