Supasheet.

Storage

Manage files with Supabase Storage — buckets, policies, the FILE data type, and the built-in file browser

Overview

Supasheet integrates Supabase Storage at three levels:

  1. The FILE / AVATAR data types automatically upload to the uploads bucket and store metadata on your rows.
  2. Three pre-configured buckets (public, personal, uploads) cover the common access patterns.
  3. A built-in file browser at /storage/$bucketId lets users browse, upload, rename, move, and preview files in any bucket they can access.

All buckets and policies are defined in the migrations under supabase/migrations/ — primarily 20251005041214_general_storage.sql and 20251005051303_uploads.sql.

Pre-Configured Buckets

BucketPublic URL?ReadWrite
publicYes (anyone with the URL)Public + authenticatedAuthenticated; updates/deletes by owner
personalYesOwner only (authenticated)Owner only (authenticated)
uploadsYesGated by schema.table:select (or owner-only under auth/)Gated by schema.table:insert/update/delete (or owner-only under auth/)

public

Use for assets that anyone with the URL should be able to read — product images, blog covers, marketing media.

OperationWho
SELECTpublic (anyone, no auth required)
INSERTAny authenticated user
UPDATEOwner (owner_id = auth.uid())
DELETEOwner

personal

Per-user private storage — documents, drafts, sensitive uploads.

OperationWho
SELECTOwner
INSERTOwner
UPDATEOwner
DELETEOwner

uploads (used by the FILE type)

This is the bucket Supasheet writes to when a user adds a file through a FILE or AVATAR column. Access is governed by your per-table permissions rather than ownership:

  • Path layout: uploads/<schema>/<table>/<column>/<filename>
  • SELECT requires <schema>.<table>:select
  • INSERT requires <schema>.<table>:insert
  • UPDATE / DELETE require the matching permission

This means anyone who can read a row can read its attached files, anyone who can edit a row can replace them, and so on — without you writing custom storage policies.

There's one exception carved out inside the same bucket: the uploads/auth/<uid>/... subpath, used for account profile pictures. Objects under auth/ skip the schema.table:action permission check entirely — a signed-in user has full SELECT/INSERT/UPDATE/DELETE on auth/<their own uid>/... and nothing outside it. There is no separate account_image bucket; avatars are just another prefix inside uploads.

This auth/ carve-out is the one place in uploads where access is owner-based instead of permission-based — every other path in the bucket follows the <schema>.<table>:<action> rule above.

Using FILE Columns

The FILE data type stores an array of file objects ({ name, type, size, url, last_modified }). Combine it with a column comment to constrain what the UI accepts:

CREATE TABLE store.products (
  id    UUID PRIMARY KEY DEFAULT extensions.uuid_generate_v4(),
  name  TEXT NOT NULL,
  image FILE,
  manuals FILE
);

COMMENT ON COLUMN store.products.image   IS '{"accept": "image/*", "maxSize": 5242880, "maxFiles": 1}';
COMMENT ON COLUMN store.products.manuals IS '{"accept": ".pdf",   "maxSize": 10485760, "maxFiles": 5}';

See Data Types for the full type reference and Metadata → File columns for the column metadata options.

FILE_OBJECT (single file)

If a column should accept exactly one file (e.g. a cover image), use the FILE_OBJECT composite type:

CREATE TABLE desk.tasks (
  id    UUID PRIMARY KEY DEFAULT extensions.uuid_generate_v4(),
  cover FILE_OBJECT
);
COMMENT ON COLUMN desk.tasks.cover IS '{"accept": "image/*"}';

AVATAR (single image, for profiles)

AVATAR is FILE_OBJECT with semantic meaning — Supasheet renders it as a circular avatar.

CREATE TABLE blog.authors (
  id     UUID PRIMARY KEY DEFAULT extensions.uuid_generate_v4(),
  name   TEXT NOT NULL,
  avatar AVATAR
);
COMMENT ON COLUMN blog.authors.avatar IS '{"maxSize": 2097152}';

The File Browser

Authenticated users see a Storage entry in the sidebar that lists buckets they can access. Clicking a bucket opens /storage/$bucketId, which provides:

  • Folder navigation with breadcrumbs
  • Multi-file upload (drag-and-drop or the upload button)
  • Rename, move, and delete actions
  • Image / PDF / text previews

Each operation goes through the standard Supabase Storage API — RLS policies on storage.objects are the source of truth.

Creating Custom Buckets

If you need stricter or different access patterns, define your own bucket alongside its policies:

INSERT INTO storage.buckets (id, name, public)
VALUES ('invoices', 'invoices', false);

CREATE POLICY "users_read_own_invoices"
  ON storage.objects FOR SELECT TO authenticated
  USING (
    bucket_id = 'invoices'
    AND owner_id = auth.uid()
  );

CREATE POLICY "users_upload_own_invoices"
  ON storage.objects FOR INSERT TO authenticated
  WITH CHECK (
    bucket_id = 'invoices'
    AND owner_id = auth.uid()
  );

The bucket will appear automatically in the storage sidebar for users whose policies grant them SELECT on it.

Storage Limit Cheat-Sheet

Useful constants when setting maxSize:

SizeBytes
1 MB1048576
2 MB2097152
5 MB5242880
10 MB10485760
25 MB26214400
50 MB52428800
100 MB104857600

Supabase Storage also enforces a global file_size_limit per project. Make sure your maxSize is below the project limit, otherwise uploads will fail at the storage layer.

Next Steps

  • Data TypesFILE, FILE_OBJECT, AVATAR, RICH_TEXT
  • Metadata — Column-level configuration for file fields
  • Authorization — How schema.table:* permissions gate the uploads bucket

On this page