/storage/v1/* on your project URL. It manages files in buckets with metadata mirrored into the storage.buckets and storage.objects tables in your project’s Postgres so you can apply Row Level Security to file access just like any other table. On managed cloud the backend is S3; on a self-hosted Docker deployment the backend is local-disk by default (set STORAGE_BACKEND=s3 plus the matching credentials to use S3 instead).
This page covers the conceptual model — buckets vs objects, public vs private, how the per-project S3 prefix works, and the size/MIME constraints. For the API surface, see Storage Reference. For uploads, see Storage uploads. For RLS on storage.objects, see Storage policies.
Buckets and objects
Every file lives inside a bucket — a top-level container with its own name, public/private flag, and optional MIME allowlist. Within a bucket, files are addressed by an arbitrary slash-separated path (Storage doesn’t model “folders” — they’re just substrings of the path).storage.buckets— one row per bucket: name, public flag, allowed MIME types, file size limit, owner. You manage buckets viaPOST /storage/v1/bucketor directly via PostgREST onstorage.buckets.storage.objects— one row per uploaded file: bucket_id, name (path), size, mimetype, metadata, owner. Storage API populates this on every upload; you read it via PostgREST to list files or apply RLS.
storage.objects and your own public.* tables. A common pattern: a public.documents table with a storage_path column, joined to storage.objects for size/last-modified.
Public vs private buckets
Buckets have apublic boolean that gates one specific URL shape:
GET /storage/v1/object/public/{bucket}/{path}— no auth required, returns the file directly. Only works for buckets wherepublic = true.GET /storage/v1/object/authenticated/{bucket}/{path}— requiresAuthorization: Bearer <user access token>. RLS onstorage.objectsdecides whether the request succeeds.GET /storage/v1/object/sign/{bucket}/{path}— returns a pre-signed URL valid for a configurable TTL. Useful for sharing private files via email/links without making the whole bucket public.
storage.objects is a regular Postgres table with policies you can apply just like your own tables. See Storage policies for patterns.
The per-project S3 prefix
All Powabase projects share a single S3 bucket on the backend, but each project’s files are namespaced unders3://<global-bucket>/projects/<ref>/. The Storage API enforces this prefix at the application layer — your project’s reads and writes are constrained to its own subtree, even though the underlying S3 bucket is shared.
You don’t need to think about this in normal API use. It does matter if:
- You want to give an external system direct S3 access. You can’t issue an AWS IAM role scoped to “this project’s prefix” without coordinating with the platform team. For external integrations, use signed URLs (TTL-bounded) or stream through your own backend.
- You’re auditing storage usage. Backups, S3 logs, and CloudWatch metrics are at the global-bucket level. To attribute usage to a project, filter by the
projects/{ref}/prefix.
File size limit
The default file size limit is 50 MB (52428800 bytes), enforced by the Storage API before forwarding to S3. Larger uploads return 413 Payload Too Large.
For files between 50 MB and several GB, use resumable uploads (TUS protocol at /storage/v1/upload/resumable). TUS chunks the upload and persists progress, so a dropped connection mid-upload can be resumed rather than restarted. The per-chunk size, not the total file size, is what hits the 50 MB limit — so set chunk size to under 50 MB and you can upload arbitrarily large files. See Storage uploads for the TUS client setup.
For files much larger than a few GB (video archives, datasets), TUS works but client-side handling gets heavyweight. Consider whether the file belongs in object storage at all — versioned datasets often want dataset-specific tooling, not a generic file API.
MIME allowlist
Each bucket can optionally restrict which content types are accepted. Set it at bucket creation:Content-Type header return 400 invalid_mime_type. When allowed_mime_types is null (the default), the bucket accepts anything within the global file size limit.
Bucket-level file_size_limit overrides the project default for files in that bucket. Useful when you want a low limit on user-uploaded avatars but a higher limit on internal-only document buckets.
The MIME check is client-trust-based. The Storage API checks the
Content-Type header your client sends, not the actual file contents. A malicious client could upload an executable with Content-Type: image/png and bypass the allowlist. For untrusted clients, validate file contents server-side (e.g., by running magic-byte detection) before treating the upload as the claimed type. The MIME allowlist is a UX safeguard, not a security boundary.Image transformations
Powabase ships an imgproxy service in the shared infrastructure, fronted by the Storage API. When you fetch an image, you can pass transformation parameters as a query string and the response is the transformed image:width, height, resize (cover/contain/fill), quality, format (webp, png, jpeg), and a few others. The transformed images are cached by imgproxy, so the second request for the same parameters is fast.
This is the right tool for serving multiple sizes of user-uploaded avatars, generating thumbnails for a media gallery, or converting between formats on the fly. It’s not the right tool for batch processing — for that, do the transformation server-side and upload the results as separate objects.
The render endpoint mirrors the object endpoint shape: /render/image/public/, /render/image/authenticated/, and /render/image/sign/ for public, RLS-gated, and signed-URL access respectively.
Owner column and auth.uid()
storage.objects includes an owner column (uuid) set at upload time. By default it’s the sub claim from the uploading user’s JWT — i.e., auth.uid() at upload time. RLS policies can use this for “users can only access files they uploaded” patterns:
owner is null. Service-role uploads can explicitly set owner to any user uuid via the x-owner header.
Storage and the AI schema
Powabase’s Sources pipeline (/api/sources/upload) ingests files into Storage internally — the platform creates the bucket, manages the path, and tracks the lifecycle. Those files are not the same as files you upload via /storage/v1/*.
- User Storage — buckets you create, files your app uploads. Lives in
storage.objectsunder buckets you own. - Platform Sources — files uploaded via
/api/sources/upload. Lives in a platform-managed bucket; the rows you see are inai.sources, notstorage.objects.
Next steps
Storage uploads
Browser-direct uploads, signed URLs, and TUS resumable uploads.
Storage policies
RLS patterns on storage.objects — own-files-only, public-read, role-based.
Storage Reference
Full /storage/v1/* endpoint catalog.
RLS Model
The role-and-policy model that gates storage.objects.