Docs
Skip to content

Storage

S3 API_

Connect any S3-compatible client, SDK, or tool to Appwrite Storage. Configure credentials once, then manage buckets, objects, and multipart uploads over the S3 API.

15 min read

Raw

Appwrite Storage exposes an S3-compatible API, so you can point the AWS CLI, the AWS SDKs, and third-party tools like rclone or s3cmd at your buckets and files. The API uses AWS Signature Version 4 and maps standard S3 operations onto Appwrite Storage, so most existing S3 code works after you change three settings: the endpoint, the credentials, and the region. Requests can be signed with an Authorization header or handed to a client without Appwrite credentials as a presigned URL, including from a browser.

The S3 API is available on Appwrite Cloud. It needs a project ID and an API key with the storage scopes described below. It also works alongside your existing data: buckets and files are addressable over both the native Storage API and the S3 API at the same time, with no migration required.

Connect an S3 client

Configure your client with the following values. S3 clients authenticate with an access key ID and a secret access key, which Appwrite maps to your project ID and an API key secret.

SettingValue
Endpointhttps://<REGION>.cloud.appwrite.io/v1/s3
Access key IDYour Appwrite project ID
Secret access keyAn Appwrite API key secret
Regionauto or your project's Appwrite region code
Signature versionAWS Signature Version 4 (SigV4)
Addressing stylePath-style only

The <REGION> in the endpoint is your Appwrite Cloud region (for example, fra or nyc), the same region you use for the rest of the Appwrite API. The S3 region setting accepts auto or that same region code; some clients require a region to sign requests, and either value works.

The examples below apply these settings in the AWS CLI and the AWS SDKs.

API key scopes

Create an API key in the Appwrite Console under Overview > Integrations > API keys, then grant the storage scopes your workload needs. The project ID identifies your project, while the API key secret both authenticates the request and determines what it is allowed to do.

Access levelRequired scopes
Read-only (list and download)buckets.read, files.read
Write (create buckets, upload, delete)buckets.write, files.write
Copy (CopyObject, UploadPartCopy)files.read, files.write
Full read and writebuckets.read, files.read, buckets.write, files.write

Copying an object reads the source and writes the destination, so CopyObject and UploadPartCopy need both files.read and files.write. A request signed with a secret that does not match a project API key, or a key missing the required scope, is rejected with AccessDenied (HTTP 403).

Buckets and objects

S3 buckets map directly to Appwrite Storage buckets, and S3 objects map to files. Creating a bucket over S3 creates an Appwrite bucket that uses the S3 bucket name as both its ID and its name, with Appwrite's default settings. Any bucket you already have in your project is usable over S3 immediately.

Object keys are file names, used directly. Uploading to the key reports/january.pdf stores a file named reports/january.pdf, and read, copy, and delete operations address that object by the same key. Nothing has to be looked up first, and files created through the native Storage API or the Console are addressable over S3 under the names they already have.

Because the key is the name, file names have to be unique within a bucket. Uploading to a key that already exists replaces that object in place: its bytes, size, content type, and user metadata are overwritten, while the underlying Appwrite file keeps its ID and permissions, so anything referencing it through the native Storage API stays valid. Concurrent uploads to the same key are serialized, and the last write wins.

Folders

Appwrite Storage organizes the files in a bucket with virtual folders, derived from the paths of your files and never created or deleted on their own. Over the S3 API, folders appear as /-separated prefixes in object keys: uploading to reports/2024/january.pdf stores a single object whose name contains those prefixes, and does not create reports/ or reports/2024/ as separate entities.

To browse a bucket like a directory tree, list objects with the / delimiter and, optionally, a prefix. Appwrite groups object names at each / boundary: names sharing a prefix up to the next / collapse into a single entry under CommonPrefixes (the "folders"), while objects at the current level are returned under Contents (the "files").

Pass a prefix such as reports/ to open that folder and list one level deeper, or list without a delimiter to enumerate a bucket recursively.

Objects under Contents are returned with their full keys; use those to read, copy, or delete a file. CommonPrefixes are folder paths only, with nothing to download at the prefix itself.

/ is the only supported delimiter. Setting delimiter to any other value returns NotImplemented (HTTP 501).

Bash
# List the top level of a bucket: immediate folders (CommonPrefixes) and files
aws s3api list-objects-v2 --bucket my-bucket --delimiter / \
--endpoint-url https://<REGION>.cloud.appwrite.io/v1/s3
# Open the "reports" folder and list one level down
aws s3api list-objects-v2 --bucket my-bucket --prefix reports/ --delimiter / \
--endpoint-url https://<REGION>.cloud.appwrite.io/v1/s3
# aws s3 ls uses the / delimiter automatically; omit --recursive to browse by folder
aws s3 ls s3://my-bucket/reports/ \
--endpoint-url https://<REGION>.cloud.appwrite.io/v1/s3

Example commands

Once your client is configured, everyday S3 commands work as usual. These AWS CLI examples assume you have run aws configure as shown above; each command passes the endpoint explicitly with --endpoint-url.

Bash
# Create a bucket
aws s3api create-bucket --bucket my-bucket \
--endpoint-url https://<REGION>.cloud.appwrite.io/v1/s3
# Upload a file (uses multipart automatically for large files)
aws s3 cp ./january.pdf s3://my-bucket/reports/january.pdf \
--endpoint-url https://<REGION>.cloud.appwrite.io/v1/s3
# List objects
aws s3 ls s3://my-bucket --recursive \
--endpoint-url https://<REGION>.cloud.appwrite.io/v1/s3
# Download an object by its key
aws s3 cp s3://my-bucket/reports/january.pdf ./january.pdf \
--endpoint-url https://<REGION>.cloud.appwrite.io/v1/s3

Bucket operations

All operations are addressed with path-style URLs under /v1/s3. The following bucket operations are supported.

OperationDescription
ListBucketsList the project's buckets.
CreateBucketCreate a bucket. The S3 bucket name becomes the Appwrite bucket's ID and name, with Appwrite's default settings.
HeadBucketCheck that a bucket exists and is accessible.
DeleteBucketDelete an empty bucket. Deleting a bucket that still contains objects returns BucketNotEmpty (HTTP 409).
GetBucketLocationReturns the added region.
GetBucketAcl, PutBucketAclCompatibility responses only. See Limitations.

Object operations

The following object operations are supported.

OperationDescription
PutObjectUpload an object, replacing anything already stored under that key. Returns an ETag. Honors Content-Type, x-amz-meta-* user metadata, and x-amz-server-side-encryption.
GetObjectDownload an object. Supports Range requests (HTTP 206), and the If-None-Match (HTTP 304) and If-Match (HTTP 412) conditional headers.
HeadObjectRetrieve an object's metadata (size, content type, ETag, user metadata) without the body.
CopyObjectCopy an object using the x-amz-copy-source header. Supports x-amz-metadata-directive: REPLACE to replace metadata.
DeleteObjectDelete a single object.
DeleteObjectsDelete multiple objects in one request, including quiet mode.
ListObjectsList objects in a bucket, with prefix filtering.
ListObjectsV2List objects with prefix, delimiter (only /), max-keys, continuation-token, and start-after. Use delimiter=/ to browse folders.
GetObjectAcl, PutObjectAclCompatibility responses only. See Limitations.

Uploads sent with the AWS SDKs' default integrity protections, which frame the body as Content-Encoding: aws-chunked with a streaming payload signature, are decoded transparently, and a CRC32 trailer checksum is verified against the stored bytes when the client sends one.

Content type is taken from the Content-Type header you send. When that is missing or generic (application/octet-stream), Appwrite infers a type from the file extension. Buckets keep their constraints under the S3 API: uploads that exceed the bucket's maximum file size or use a disallowed extension are rejected, and disabled buckets are treated as not found.

Multipart uploads

Large files are uploaded in parts. Standard SDK multipart helpers, such as the AWS CLI's aws s3 cp and the SDK transfer managers, use these operations automatically.

OperationDescription
CreateMultipartUploadBegin a multipart upload and receive an UploadId.
UploadPartUpload a single part. Returns the part's ETag.
UploadPartCopyUpload a part by copying a byte range from another object, using x-amz-copy-source and x-amz-copy-source-range.
CompleteMultipartUploadAssemble the uploaded parts into the final object.
AbortMultipartUploadDiscard an in-progress multipart upload and its parts.
ListMultipartUploadsList in-progress multipart uploads in a bucket.
ListPartsList the parts already uploaded for an UploadId.

Presigned URLs

A presigned URL carries its AWS Signature Version 4 authentication in the query string instead of an Authorization header, so it can be handed to a client that has no Appwrite credentials of its own: a browser, a mobile app, or anyone you send the link to. Appwrite accepts presigned URLs for the supported S3 operations, generated by the standard helpers in the AWS SDKs, the AWS CLI, rclone, and s5cmd.

The signature covers the HTTP method, the object key, and every other query parameter in the URL, so a presigned URL authorizes exactly one request. A URL signed for GetObject cannot be replayed as a PutObject, and changing the key or any signed parameter invalidates the signature.

The request body is not signed. A presigned upload URL is generated before the bytes exist, so Appwrite verifies it as UNSIGNED-PAYLOAD and the client supplies the body afterwards. Anything else the client has to send, such as Content-Type, must be signed into the URL when you generate it and then sent unchanged with the request.

Expiry

Set the lifetime when you generate the URL. Appwrite accepts 1 second to 604800 seconds (7 days), the same ceiling AWS applies to SigV4 presigned URLs, and rejects anything outside that range. The URL is valid from the timestamp it was signed with until that timestamp plus the expiry; a request that arrives after the window closes returns AccessDenied (HTTP 403).

Presigned URLs are stateless, so an individual URL cannot be revoked once issued. To cut one short, delete or rotate the API key that signed it, which invalidates every URL signed with that secret at once. Keep expiry windows as short as your workload allows.

What a presigned URL grants

A presigned URL is signed with an API key secret and inherits that key's scopes for the single operation it encodes. The secret is never part of the URL, and the URL cannot be used to recover it or to perform any other operation. Anyone holding the URL can still make that one request until it expires, so treat it as a credential: send it over HTTPS, keep it out of logs, and share it only with the client that needs it.

Required parameters

SDK and CLI helpers add these for you. If you sign URLs yourself, all six are required, and the request must not also carry an Authorization header.

ParameterValue
X-Amz-AlgorithmAWS4-HMAC-SHA256. Any other algorithm is rejected.
X-Amz-Credential<PROJECT_ID>/<YYYYMMDD>/<REGION>/s3/aws4_request. The date must match X-Amz-Date.
X-Amz-DateThe request timestamp, in YYYYMMDDTHHMMSSZ format.
X-Amz-ExpiresThe lifetime in seconds, from 1 to 604800.
X-Amz-SignedHeadersLowercase and semicolon-separated, sorted, and including host.
X-Amz-SignatureThe 64-character hexadecimal SigV4 signature.

A URL that omits one of these parameters, repeats one, or combines query authentication with an Authorization header is rejected with AccessDenied (HTTP 403).

Browser access

The S3 endpoint answers cross-origin requests, so a presigned URL can be used directly from a web page. Downloading an object, uploading to a presigned PutObject URL, and streaming a Range request all work from browser JavaScript with no proxy in front.

Requests are allowed from any origin, and preflight OPTIONS requests are answered by the endpoint without reaching your buckets. Because every request authenticates from its own signature, cross-origin responses are credential-free: cookies are neither sent nor accepted (Access-Control-Allow-Credentials is false), and you do not need to register the origin as a platform in your project. Preflight responses are cacheable for 24 hours.

These response headers are exposed to browser JavaScript, so client code can read them off a fetch response:

Exposed headerUse
Content-LengthThe size of the object body.
Accept-Ranges, Content-RangeRange request support, and the byte range returned.
ETagThe object's entity tag, for conditional requests and integrity checks.
Last-ModifiedWhen the object was last written.
x-amz-server-side-encryptionThe server-side encryption applied to the object.

Browser requests may send the standard content and caching headers, including Content-Type, Range, and Cache-Control. Custom x-amz-* request headers are not permitted cross-origin, so set user metadata such as x-amz-meta-* from a server-side client rather than from the browser.

Events and usage

Writes over the S3 API are ordinary Storage writes, so they take part in the rest of your project. Uploading, replacing, or deleting an object fires the matching buckets.[bucketId].files.[fileId].create, .update, or .delete event, and creating or deleting a bucket fires the corresponding bucket event. Those events reach webhooks, realtime subscribers, and event-triggered functions exactly as native Storage writes do.

S3 traffic is metered like the rest of your project's API traffic: requests and inbound and outbound bandwidth accrue to your project's network usage.

Limitations

The S3 API targets the operations most clients depend on. Keep the following in mind.

  • Path-style addressing only. Virtual-hosted-style URLs (https://<bucket>.host/...) are not supported. Enable path-style addressing in your client, as shown in Connect an S3 client.
  • Signature Version 4 only. Requests are authenticated with SigV4, either with an Authorization header or as a presigned URL that carries the signature in the query string, but never both at once. A header-signed request must carry a timestamp within 15 minutes of the server's clock, while a presigned URL is valid for up to 7 days from the timestamp it was signed with.
  • File names must be unique within a bucket. Object keys are file names, so a bucket cannot hold two objects under one key, and uploading to an existing key replaces the object stored there. A bucket that already contains files sharing a name and folder is not served over the S3 API: requests whose key resolves ambiguously return InvalidRequest (HTTP 400). See Buckets and objects.
  • ACLs are compatibility responses. GetObjectAcl, PutObjectAcl, GetBucketAcl, and PutBucketAcl return a canned private ACL and are accepted for tooling compatibility. They do not change access. Manage access with Appwrite permissions and API key scopes instead.
  • No folder resource or folder markers. Folders are virtual: they are /-separated prefixes in object keys, browsed by listing with delimiter=/. See Folders. Using any other delimiter value or uploading a folder-marker object (a key ending in /) returns NotImplemented (HTTP 501).
  • Prefer ListObjectsV2 for large buckets. ListObjects returns every matching object in a single response, while ListObjectsV2 honors max-keys (capped at 1000) and paginates with IsTruncated and a NextContinuationToken. No objects are silently dropped.
  • No bucket CORS configuration. The cors bucket sub-resource is not implemented. It is not needed to call the endpoint from a browser: cross-origin requests are allowed from any origin by default. See Browser access.
  • Unsupported S3 features. Object tagging, lifecycle, bucket policies and policy status, encryption configuration, ownership controls, notifications, versioning, object locking, and restores are not supported. Any request carrying a sub-resource or query parameter the API does not implement returns NotImplemented (HTTP 501) rather than being partially applied.

Error responses

Errors are returned as standard S3 XML error documents with an S3 error code and HTTP status.

S3 error codeHTTP statusMeaning
NoSuchBucket404The bucket does not exist, is disabled, or is not accessible.
NoSuchKey404The object key does not exist.
BucketNotEmpty409The bucket still contains objects and cannot be deleted.
BucketAlreadyOwnedByYou409A bucket with that ID already exists in your project.
AccessDenied403The signature is invalid, the API key is expired or missing a required scope, or a presigned URL has expired or is malformed.
InvalidRange416The requested Range cannot be satisfied.
PreconditionFailed412An If-Match precondition did not hold.
NotImplemented501The requested S3 feature is not supported.
InvalidRequest400The request was malformed, violated a bucket constraint, or its key matched more than one file.
InternalError500An unexpected server error occurred.

Was this page helpful?

Share what worked or what we should fix. Once approved, our agents automatically apply suggested updates to the docs.