# File Storage on Demand | Volcano

URL: https://volcano.dev/solutions/file-storage

> Store and serve files on Volcano: per-project buckets, policies written in SQL against auth.uid() and path helpers, per-file public visibility, and multipart upload sessions that resume from the SDK or the REST API.

## Files that authorize themselves

Buckets whose access rules are SQL policies, private by default, with resumable uploads for the files that matter.

```console
$ volcano cloud storage bucket create user-files
✓ bucket user-files · private
$ volcano cloud storage policy create user-files --name own-folder \
$   --operation INSERT --definition "(storage.foldername(name))[1] = auth.uid()::text"
✓ policy own-folder · INSERT
$ volcano cloud storage object upload user-files ./report.pdf 8f1c/report.pdf
✓ uploaded 4.2 MB
# every other object in the bucket is still private
```

## Uploads are easy. Deciding who may read them is not

Storing a file takes an afternoon. The rest takes longer: authorizing the upload, keeping one user out of another's folder, surviving a dropped connection halfway through a large file, and producing public URLs that cannot be guessed into something private.

Volcano gives each project buckets whose access rules are written in the same style as your table policies, referencing the caller through auth.uid() plus helpers for paths and file extensions. Objects are private until you make a specific one public, and large uploads resume instead of restarting.

## Access control in the language you already use

*Rules*

A policy is a SQL-style expression over the caller and the object path, so keeping every user inside their own folder takes one line. The rules go in volcano-config.yaml and get reviewed with the code, rather than living in a console someone changed last quarter.

- SELECT, INSERT, UPDATE and DELETE per bucket
- (storage.foldername(name))\[1\] = auth.uid()::text
- storage.extension(name) to restrict file types

```console
own-folder-read    SELECT   foldername[1] = auth.uid()
own-folder-write   INSERT   foldername[1] = auth.uid()
own-folder-delete  DELETE   foldername[1] = auth.uid()
# nothing else in this bucket is reachable
```

## Public is a decision per object

*Visibility*

A bucket does not become public wholesale. You flip visibility on the specific file that should be shared, and it gets a stable URL that needs no SDK, no signature and no token.

- Private by default, for every object
- Per-file visibility, not per-bucket
- Public URLs serve without a client library

Image: Objects in a bucket, each with its own visibility.

## Uploads that survive a bad network

*Large files*

Large files go up in parts through an upload session that stays valid for seven days. When the connection drops, the session reports which parts it already holds and the upload sends only the rest. A video on hotel wifi finishes instead of starting over.

- Upload sessions with progress per part
- getUploadSession lists the parts already stored
- Up to 100 MB per object on Hobby or 10 GB on Superagent

```console
uploading  ████████░░░░░░░░  52%  468 MB
network    connection lost
resuming   from part 47 of 90
✓ complete · nothing re-sent
```

## How it works

1. **Create the bucket**: One command, private from the start, scoped to the project.
2. **Write the policies**: One per operation, saying who may read, write and delete, using the path helpers to scope by user.
3. **Upload from wherever**: The browser with the user's session, or a function with the service key.
4. **Share the public ones**: Flip visibility on the individual objects that belong on the public internet.

## Bucket, policies, upload, serve

### Bucket

```bash
volcano use my-project

volcano cloud storage bucket create user-files
volcano cloud storage bucket list

# Upload, list and inspect from the CLI as well as the SDK
volcano cloud storage object upload user-files ./report.pdf 8f1c/report.pdf
volcano cloud storage object list user-files --prefix 8f1c/

# Share one specific object
volcano cloud storage object visibility user-files 8f1c/report.pdf --public

# And take it back
volcano cloud storage object visibility user-files 8f1c/report.pdf --private
```

### Policies

```bash
# Files are stored under <user-id>/..., so the first path segment is the
# owner. These three policies are the whole access model for the bucket.
# A bucket with no policy for an operation refuses it outright.

volcano cloud storage policy create user-files --name own-folder-read \
  --operation SELECT \
  --definition "(storage.foldername(name))[1] = auth.uid()::text"

volcano cloud storage policy create user-files --name own-folder-write \
  --operation INSERT \
  --definition "(storage.foldername(name))[1] = auth.uid()::text"

volcano cloud storage policy create user-files --name own-folder-delete \
  --operation DELETE \
  --definition "(storage.foldername(name))[1] = auth.uid()::text"

# Avatars are images. Enforce that on the way in rather than discovering
# it when something tries to render a 40 MB PNG. Policies on one operation
# are OR'd, so the folder check lives in the same definition.
volcano cloud storage policy create avatars --name own-images \
  --operation INSERT \
  --definition "(storage.foldername(name))[1] = auth.uid()::text AND storage.extension(name) IN ('png', 'jpg', 'jpeg', 'webp')"

volcano cloud storage policy list user-files
```

### Upload

```typescript
import { VolcanoAuth } from '@volcano.dev/sdk';

const volcano = new VolcanoAuth({
  apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL!,
  anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
});

export async function uploadReport(file: File, onProgress: (pct: number) => void) {
  const { user } = await volcano.auth.getUser();
  if (!user) return { error: 'sign in required' };

  // The path decides authorization, so it has to start with the user's id.
  const path = `${user.id}/${crypto.randomUUID()}-${file.name}`;

  // uploadResumable sends the file in parts and reports progress. It gives up
  // on the first failed part; to resume later, keep the session id and send
  // the missing parts with getUploadSession and uploadPart.
  const { data, error } = await volcano.storage
    .from('user-files')
    .uploadResumable(path, file, {
      onProgress: (uploaded, total) => onProgress(Math.round((uploaded / total) * 100)),
    });

  if (error) {
    // A policy rejection surfaces here rather than as a partial upload.
    return { error: error.message };
  }

  return { path: data.object.name };
}
```

### Serve

```typescript
// Private: the policy decides, so this returns nothing for someone else's file.
export async function downloadReport(path: string) {
  const { data, error } = await volcano.storage.from('user-files').download(path);

  if (error) {
    return { error: error.message };
  }

  return { blob: data };
}

// Listing is scoped the same way — a prefix outside the user's folder is empty.
export async function myFiles(userId: string) {
  const { data, error } = await volcano.storage
    .from('user-files')
    .list(`${userId}/`, { limit: 100 });

  return error ? { files: [] } : { files: data };
}

// Public: flip visibility on the one object. The response already carries the
// URL, so there is no second call to make.
export async function publish(path: string) {
  const { data, error } = await volcano.storage
    .from('user-files')
    .updateVisibility(path, true);

  if (error) {
    return { error: error.message };
  }

  // Stable, and needs no client library to fetch.
  return { url: data.public_url };
}
```

## What you stop maintaining

- **0 upload endpoints to write**: The browser uploads directly and the policy decides whether it is allowed.
- **1 language for every rule**: Storage policies are SQL, using the same auth.uid() as your table policies.
- **Per object visibility**: Sharing one file never means opening a bucket.

## Outcomes

- **Private by default**: Exposure is something you opt into, one file at a time.
- **Rules under review**: Policies live in the project manifest, not in a console's history.
- **Uploads that finish**: Resumable multipart survives the network your users actually have.

## Built on Volcano

- [Buckets and objects](https://volcano.dev/features/file-storage): Per-project buckets, SQL policies with path and extension helpers, per-file visibility, resumable uploads.
- [Who is uploading](https://volcano.dev/features/authentication): Verified sessions, so auth.uid() in a storage policy means a real user.
- [Processing after upload](https://volcano.dev/features/durable-functions): A durable function per upload scans, transcodes and thumbnails as steps, and a failed stage retries on its own.
- [Metadata about the file](https://volcano.dev/features/postgres): Rows describing each object, protected by policies that match the bucket's.

## Frequently asked questions

### Are files public by default?

No. Every object is private when created. You make a specific file public by changing its visibility, which gives it a stable URL that needs no SDK or signing.

### How do I stop one user reading another's files?

Store objects under a path that begins with the user's id and write the policy against it: (storage.foldername(name))\[1\] = auth.uid()::text. Uploads and reads outside that folder are rejected.

### Can I restrict which file types are accepted?

Yes. storage.extension(name) is available in policies, so an INSERT policy can accept png, jpg and webp for an avatars bucket and refuse everything else.

### What happens if a large upload is interrupted?

Nothing is lost. An upload session stays valid for seven days, getUploadSession reports which parts are already stored, and the upload sends only the rest instead of starting over.

### Can a function write files?

Yes. Functions can upload, move, copy and delete with the service key, which is how thumbnailing and other post-upload processing usually works.

### Can I use the storage API without the SDK?

Yes. Everything is available over REST and from the CLI, so scripts, CI jobs and one-off migrations do not need a client library.

## Get started

Ready to store your first file? Create a free account at https://volcano.dev/signup, or have your coding agent follow the setup playbook at https://volcano.dev/setup.md.
