# Object Storage with Policy-Based Access | Volcano

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

> Object storage on Volcano. Objects are private by default, access is decided by per-bucket policies written against auth.uid(), and the SDK uploads straight from the browser with resumable multipart for large files.

## Object storage that enforces your rules

Buckets per project, objects private until you say otherwise, and access decided by policies written against the signed-in caller.

```console
$ volcano cloud storage bucket create user-files --file-size-limit 26214400
✓ user-files created · objects private by default
$ volcano cloud storage policy create user-files --name own-read \
$   --operation SELECT --definition "auth.uid() = owner_id"
✓ policy own-read · SELECT
$ volcano cloud storage object upload user-files ./report.pdf 8f1c/report.pdf
✓ uploaded 4.2 MB · private
$ volcano cloud storage object visibility user-files 8f1c/report.pdf --public
is_public true · served from its public URL
# every other object in the bucket still needs an authorized request
```

## Most of the work is not the bucket

Getting a bucket takes a minute. The rest takes a week: an endpoint to sign uploads, a rule that keeps each user under their own prefix, a size and type check that actually runs on the server, and a way to publish one file without exposing the twelve next to it.

Volcano gives each project buckets whose objects are private until you say otherwise. Access is decided by per-bucket policies, written in the same style as Postgres row-level security and referencing the caller through auth.uid(). The browser uploads directly with the user's session, so there is no signing service in the path.

## Rules that read like your table policies

*Access*

Each bucket carries a policy per operation: SELECT, INSERT, UPDATE, DELETE. A definition is a SQL-style expression over the caller and the object, so auth.uid() = owner\_id is the whole rule for owner-only reads. Uploads match on the path instead, because an object has no owner until it exists. A bucket with no policy for an operation refuses it.

- One policy per operation: SELECT, INSERT, UPDATE, DELETE
- Match on the owner, the path, or the file extension
- Enforced in the storage layer, not your form

```console
# owner-only reads, written as one SQL expression
$ volcano cloud storage policy create user-files --name own-read \
$   --operation SELECT --definition "auth.uid() = owner_id"
✓ policy own-read
$ volcano cloud storage policy list user-files
own-read SELECT · own-write INSERT · own-delete DELETE
```

## Public is a flag on the file

*Visibility*

Every object carries an is\_public flag that starts false. Set it on one file and that file is served from a stable public URL, while the objects beside it in the same bucket still require an authorized request.

- is\_public is false on every new object
- updateVisibility flips one object either way
- The response carries the object's public\_url

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

## Uploads that survive a dropped connection

*Large files*

Large files go up in parts through an upload session that stays open for seven days. When the connection drops, getUploadSession lists the parts already stored and you send only the rest. uploadResumable drives the same session with progress reporting when you do not need to resume.

- Parts of 5 to 25 MB, uploaded in any order
- getUploadSession lists the parts already stored
- Up to 100 MB per object on Hobby or 10 GB on Superagent

```console
$ createUploadSession('8f1c/keynote.mp4', { totalSize: file.size })
session s_7f2 · 74 parts of 25 MB
part 38/74   ← connection dropped
$ getUploadSession('8f1c/keynote.mp4', 's_7f2')
parts_uploaded: 37 of 74
✓ 37 parts sent · 1.8 GB object ready
```

## What storage covers

- **Per-project buckets**: Buckets are scoped to one project, each with its own policies and constraints.
- **Private by default**: An object is unreadable without an authorized request until you set is\_public on it.
- **Per-bucket policies**: Per-operation rules written against auth.uid(), the same helper your table policies use.
- **Direct browser uploads**: The SDK sends a File, Blob or ArrayBuffer from the page using the user's session.
- **Resumable multipart**: Create, resume and abort upload sessions, or use uploadResumable and let the SDK drive them.
- **Bucket constraints**: Give a bucket an allowed MIME type list and a file size limit of its own.

## Upload it, restrict it, manage it

### 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!,
});

const bucket = volcano.storage.from('user-files');

export async function uploadForUser(userId: string, file: File) {
  const path = userId + '/' + file.name;

  // Anything sizeable goes up in parts, with progress. uploadResumable gives
  // up on the first failed part; to resume later, keep the session id and use
  // createUploadSession, getUploadSession and uploadPart instead.
  if (file.size > 100 * 1024 * 1024) {
    return bucket.uploadResumable(path, file, {
      partSize: 8 * 1024 * 1024,
      onProgress: (sent, total) => setPercent(Math.round((sent / total) * 100)),
    });
  }

  // File, Blob or ArrayBuffer. Not a Node Buffer and not a stream.
  return bucket.upload(path, file, { contentType: file.type });
}
```

### Policies

```bash
# The bucket rejects oversized or wrong-typed uploads before a policy runs.
volcano cloud storage bucket create avatars --allowed-mime-type image/png --file-size-limit 5242880

# Then one policy per operation. The definition is a SQL expression over
# the caller and the object, evaluated on every request.
volcano cloud storage policy create avatars --name own-read \
  --operation SELECT --definition "auth.uid() = owner_id"

# An object has no owner until it exists, so uploads match on the path.
volcano cloud storage policy create avatars --name own-write \
  --operation INSERT --definition "(storage.foldername(name))[1] = auth.uid()::text"

volcano cloud storage policy create avatars --name own-delete \
  --operation DELETE --definition "auth.uid() = owner_id"

volcano cloud storage policy list avatars
```

### Manage

```typescript
const bucket = volcano.storage.from('user-files');

// Listing under a prefix. SELECT policies decide what comes back, and the
// next page is the same call with a cursor.
const { data: page } = await bucket.list(userId + '/', { limit: 50 });

// Whole object as a Blob; pass a range to read part of it instead.
const { data: blob, error } = await bucket.download(userId + '/2026/report.pdf');

// Rename and duplicate without moving bytes through the browser.
await bucket.move(userId + '/draft.pdf', userId + '/2026/report.pdf');
await bucket.copy(userId + '/2026/report.pdf', userId + '/2026/report.bak.pdf');
await bucket.remove([userId + '/2026/report.bak.pdf']);

// Publish one file. The response already carries the URL, so there is no
// second call to make.
const { data: object } = await bucket.updateVisibility(userId + '/2026/report.pdf', true);
const shareable = object?.public_url;

// If you only have the path, getPublicUrl builds it without a round trip.
const { data } = bucket.getPublicUrl(userId + '/2026/report.pdf');
console.log(data?.publicUrl);
```

### Operate

```bash
# Buckets
volcano cloud storage bucket list
volcano cloud storage bucket get avatars
volcano cloud storage bucket update avatars --file-size-limit 10485760

# Objects
volcano cloud storage object upload avatars ./me.png 8f1c/me.png --content-type image/png
volcano cloud storage object list avatars
volcano cloud storage object move avatars 8f1c/me.png 8f1c/avatar.png
volcano cloud storage object copy avatars 8f1c/avatar.png 8f1c/avatar.bak.png
volcano cloud storage object download avatars 8f1c/avatar.png ./avatar.png
volcano cloud storage object delete avatars 8f1c/avatar.bak.png

# Visibility is per object, not per bucket.
volcano cloud storage object visibility avatars 8f1c/avatar.png --public

volcano cloud storage stats
```

## What people store in it

### Files users upload

The browser writes straight into the user's own prefix with their session. A request that aims somewhere else gets the same answer whether it came from your form or from curl.

### Artifacts your functions write

A function builds an export or a report and writes it back to a bucket. The user reads it through a policy, or you publish that one object and send the link.

### Per-tenant files

Give each tenant a bucket, or keep one bucket and put the tenant in the policy definition. Either way the rule lives next to the objects rather than in an application check.

## Works with the rest of Volcano

- [The caller in your rules](https://volcano.dev/features/authentication): auth.uid() in a storage policy is the user Volcano signed in, the same value your table policies see.
- [File metadata in Postgres](https://volcano.dev/features/postgres): Keep the row that describes a file in Postgres, protected by a policy that matches the one on the object.
- [Processing after upload](https://volcano.dev/features/edge-functions): Download an object inside a function and transform it there, using /tmp for the intermediate files.
- [Serving from your app](https://volcano.dev/features/nextjs-hosting): Render public objects directly, and read private ones through a route that has the caller's session.

## Frequently asked questions

### Are files private by default?

Yes. Every object lands with is\_public set to false and stays unreadable until a policy allows the caller or you publish that specific object.

### Can the browser upload without a signing service?

Yes. The SDK uploads directly with the user's session and the storage layer applies your policies, so there is no signing endpoint to run and no pre-signed URL to mint.

### How large can a file be?

Up to 100 MB per object on Hobby or 10 GB on Superagent. Large files go up in parts through an upload session, which lasts seven days, so an upload that loses its connection can send just the missing parts. How much each account can store is on the pricing page.

### What can a bucket restrict on its own?

A bucket can carry an allowed MIME type list and its own file size limit, both checked on the server. Those run before your policies, so a wrong-typed upload never reaches them.

### How do I share one file publicly?

Call updateVisibility(path, true) from the SDK, or volcano cloud storage object visibility \<bucket\> \<path\> --public from the CLI. The response carries the object's public\_url, a stable link that needs no token.

### Can I process a file after it is uploaded?

Yes. Invoke a function with the bucket and path, download the object there, and do the work with a writable /tmp for the intermediate files. For a pipeline such as transcode, thumbnail and index, use a durable function: each stage is a step, and a failed stage retries without redoing the others.

## 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.
