Upload Files to DigitalOcean Spaces from the Browser
DigitalOcean Spaces is S3-compatible object storage. It implements a subset of
the S3 API that includes presigned URLs and multipart uploads, which covers
every operation upup uses. The datacenter region (nyc3, sfo3, ams3,
fra1, sgp1 and the others) is part of the endpoint hostname, and the
endpoint decides which datacenter receives a request.
This page covers server mode, where @useupup/server
holds the credentials. See Client Mode vs Server Mode if
you would rather sign URLs yourself and have the browser upload directly.
The config
Only the storage block differs from the
AWS S3 example. Keep uploadTokenSecret and the
rest of the handler as they are there.
const storage = {
type: 'digitalocean',
bucket: process.env.SPACES_BUCKET!, // the Space name
region: process.env.SPACES_REGION!, // e.g. 'nyc3'
endpoint: `https://${process.env.SPACES_REGION}.digitaloceanspaces.com`,
accessKeyId: process.env.SPACES_KEY,
secretAccessKey: process.env.SPACES_SECRET,
}bucket is the Space name. DigitalOcean's UI says "Space" or "bucket", the S3
API says "bucket", and they are the same thing. The endpoint host is the
region, not the Space: https://nyc3.digitaloceanspaces.com, never
https://my-space.nyc3.digitaloceanspaces.com. upup turns on path-style
addressing whenever endpoint is set, so the Space name is added to the path
for you; putting it in the hostname as well produces requests aimed at a Space
inside a Space.
Console setup
- Create the Space. DigitalOcean control panel → Spaces Object Storage → Create Bucket. Pick the datacenter region, which cannot be changed later, and Standard Storage: Cold Storage buckets do not support CORS, so a browser cannot upload to them. File listing is private by default; leave it that way unless you want the contents publicly enumerable.
- Create an access key. Spaces Object Storage → Access Keys → Create Access Key. Choose Limited access and give the key Read/Write/Delete on this one bucket, which covers the object and multipart operations upup performs. The secret key is shown once.
- Add a CORS configuration from the bucket's Settings tab, as described below. The browser uploads straight to Spaces by default in both client and server mode.
A DigitalOcean API token is not a Spaces key
Spaces access keys and DigitalOcean personal access tokens are different
credentials issued from different pages. The API token that works with
doctl and the DigitalOcean API will not authenticate an S3 request. If
uploads come back as 403 with credentials you are sure are correct, check
that they came from the Access Keys tab under Spaces Object Storage.
CORS
In the bucket's Settings tab, under CORS Configurations, click Add. Enter your
app's full origin (for example https://app.example.com; one wildcard is
allowed in the hostname), allow PUT (plus GET and HEAD if the browser
reads files back), and add Content-Type under Allowed Headers, because upup
sends it with every upload. That is enough for single-request uploads.
The control panel has no field for exposed headers. Multipart uploads need the
ETag header exposed, so if you enable them, upload the rule as XML instead;
see Multipart uploads fail at the last step.
CORS applies in both client and server mode: by default the browser uploads
straight to Spaces either way, since server mode hands it a presigned URL
instead of proxying the bytes. The exceptions are cloud-drive transfers
(server to Spaces) and a deployment using onPresignResponse to rewrite the
URL to a same-origin proxy. See
Credentials And CORS.
Large files and multipart uploads
A single PUT to Spaces can be up to 5 GB. Past that, or when you want uploads
to survive a dropped connection, use multipart: Spaces accepts parts of at
least 5 MiB (except the last) and up to 5 GB, up to 10,000 parts, and objects
up to 5 TB.
In upup, multipart is part of server mode. Turn it on in the uploader:
<UpupUploader
mode="server"
serverUrl="/api/upup"
provider="digitalocean"
resumable={{ protocol: 'multipart' }}
/>Files of 5 MiB or more (thresholdBytes) are split into parts of at least
5 MiB (chunkSizeBytes), and the part size grows for very large files so that
no upload needs more than 10,000 parts. The /multipart/* routes of
@useupup/server start, sign and complete the upload, each part goes to Spaces
through its own presigned URL, and a failed part is retried without restarting
the file. See Resumable uploads.
Interrupted uploads keep their parts so a later attempt can continue (cross-reload resume is on by default). Spaces deletes incomplete multipart uploads older than 30 days on its own, and a bucket lifecycle rule can remove them sooner.
Serving files back through the CDN
Each Space can enable a CDN endpoint on a hostname of the form
<space>.<region>.cdn.digitaloceanspaces.com. Use it for reads, and keep
uploading to the origin endpoint in the config above: requests with presigned
URLs are never cached by the CDN, and DigitalOcean caps PUT and multipart
requests sent to the CDN with presigned URLs at 8100 KiB.
The CDN serves public files from its cache. A private file needs a presigned URL, which bypasses the cache and works on the CDN hostname only when it was generated in virtual-hosted style, so for private files the origin URL that upup returns is the simpler choice.
Troubleshooting
CORS preflight fails
The browser console shows the PUT blocked by CORS policy. Check, in order:
- The Space has a CORS rule, and it is a Standard Storage bucket. Cold Storage buckets do not support CORS.
- The origin matches exactly: scheme, host and port.
PUTis an allowed method, andContent-Typeis an allowed header, along with any other header your endpoint returns inuploadHeaders.- The browser is not reusing a cached preflight. DigitalOcean recommends an Access Control Max Age of 5 seconds while you test.
- If you read files through the CDN, purge its cache after changing CORS. Responses cached before the change carry no CORS headers.
Multipart uploads fail at the last step
Every part uploads, then completion fails. upup reads each part's ETag
response header in the browser and sends the list to your server, which
completes the upload with it. If the CORS rule does not expose ETag, the
browser cannot read the header and Spaces rejects the part list.
The control panel cannot set exposed headers, so upload the rule as XML with s3cmd, configured with the Space's region endpoint:
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>https://app.example.com</AllowedOrigin>
<AllowedMethod>PUT</AllowedMethod>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>HEAD</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
<ExposeHeader>ETag</ExposeHeader>
<MaxAgeSeconds>3000</MaxAgeSeconds>
</CORSRule>
</CORSConfiguration>s3cmd setcors cors.xml s3://my-spaceChanging a bucket's CORS configuration through the API needs a Full access key. The Limited access key upup uses cannot do it, so run this with a separate key.
403 SignatureDoesNotMatch
Spaces computed a different signature from the one in the URL. The usual causes:
- The credentials are not a Spaces access key. A DigitalOcean API token, or a mistyped secret, produces a signature Spaces cannot match.
- The browser sent a different
Content-Typefrom the one that was signed.@useupup/serversigns the content type and length and returns theContent-Typeheader inuploadHeaders, which upup sends unchanged. A client-mode endpoint that signs a content type must return it the same way; see S3 Presign Responses. - The URL changed after signing. A presigned URL is bound to its host and path, so moving a path-style URL onto the CDN hostname, or rewriting it into virtual-hosted style, breaks the signature.
Other 403 errors on the upload
- The key lacks write access. A Limited access key with only Read permission on the bucket cannot upload; it needs Read/Write/Delete.
- The presigned URL expired before the upload started. Keep
expiresInlonger than the time between signing and uploading, and keep the server's clock synced: presigned URLs are time-scoped.
Uploaded files return 403 when opened
Files in a Space are private by default, and @useupup/server uploads without
an ACL, so a plain https://my-space.nyc3.digitaloceanspaces.com/<key> link
returns 403. Either:
- Share them through presigned download URLs.
@useupup/serverreturns a signeddownloadUrlwith each upload, valid fordownloadUrlExpiresInseconds (three days by default). - Make them public: in the control panel, open the Files tab, then the file's
… menu → Manage Permissions → Public, or run
s3cmd setacl s3://my-space/<key> --acl-public.
The bucket's File Listing setting is separate. It controls whether anyone can list the Space's keys, not whether a file can be read.
The CDN serves an old file
The CDN keeps each file for the edge cache TTL, one hour by default.
@useupup/server's default key layout puts a random UUID in every key, so a
new upload never replaces a cached file. Stale content comes from objects you
overwrite at the same key, or from responses cached before a CORS change. Purge them in the
control panel (the bucket's Settings tab → CDN → Purge CDN Cache, or a file's
… menu → Purge from CDN cache) or with doctl:
doctl compute cdn flush <cdn-id> --files /path/to/file.jpgdoctl authenticates with a DigitalOcean API token, not a Spaces key.
FAQ
Can I upload files to DigitalOcean Spaces directly from the browser?
Yes. Your server signs a short-lived presigned URL with a Spaces access key, and
the browser sends the file straight to that URL. The key never reaches the
browser. The Space needs a CORS rule that allows your origin, the PUT method
and the Content-Type header.
Does DigitalOcean Spaces support multipart uploads?
Yes. Spaces implements the S3 multipart API: parts of at least 5 MiB (except
the last) and up to 5 GB, up to 10,000 parts, and up to 5 TB per object. A
single PUT is limited to 5 GB. In upup, multipart runs in server mode with
resumable: { protocol: 'multipart' }, and the CORS rule must expose the
ETag header.
How large a file can I upload in the Spaces control panel?
The control panel uploads files smaller than 2 GB, in batches of fewer than 100
files. For larger files or batches, use an S3-compatible tool such as s3cmd
or the AWS CLI, or the Spaces API with multipart uploads.
Are files uploaded to a Space public?
No. Files in a Space are private by default, and @useupup/server uploads them
without an ACL, so they stay private. Share them through presigned download
URLs, or make individual files public in the control panel or with
s3cmd setacl.
Can I upload through the Spaces CDN endpoint?
Upload to the origin endpoint, such as nyc3.digitaloceanspaces.com, and use
the CDN hostname for reads. Requests with presigned URLs are never cached by the
CDN. DigitalOcean also caps PUT and multipart requests sent to the CDN with
presigned URLs at 8100 KiB.
Next steps
- Storage Providers: the provider matrix and
the shared
storageconfig reference. - Server Mode Setup:
getUserId,providers,tokenStore, and tuning. - Client Mode vs Server Mode: which one you want and what changes on the wire.
- DigitalOcean Spaces documentation: limits, CORS and CDN reference.