Skip to main content
DigitalOcean Spaces uses the S3 API with AWS Signature V4 against https://<region>.digitaloceanspaces.com. MIRAGE derives this endpoint from your region automatically. Credentials (Spaces access key pair) are created the same way in both runtimes, see DigitalOcean Credentials.

Node (server-side)

pnpm add @struktoai/mirage-node
import { DigitalOceanResource, MountMode, Workspace } from '@struktoai/mirage-node'

const spaces = new DigitalOceanResource({
  bucket: process.env.DO_SPACE!,
  region: process.env.DO_REGION!,
  accessKeyId: process.env.DO_ACCESS_KEY_ID!,
  secretAccessKey: process.env.DO_SECRET_ACCESS_KEY!,
})

const ws = new Workspace({ '/bucket/': spaces }, { mode: MountMode.READ })
const res = await ws.execute('ls /bucket/')
console.log(res.stdoutText)

Browser (presigned URLs)

pnpm add @struktoai/mirage-browser
The browser DigitalOceanResource is secret-free, your backend signs each operation using your Spaces keys and returns a URL. Spaces accepts AWS Signature V4, so @aws-sdk/s3-request-presigner works, pointed at the Spaces endpoint.

1. Server: sign URLs with the Spaces endpoint

import {
  CopyObjectCommand,
  DeleteObjectCommand,
  GetObjectCommand,
  HeadObjectCommand,
  ListObjectsV2Command,
  PutObjectCommand,
  S3Client,
} from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'

const REGION = process.env.DO_REGION!

const client = new S3Client({
  region: REGION,
  endpoint: `https://${REGION}.digitaloceanspaces.com`,
  credentials: {
    accessKeyId: process.env.DO_ACCESS_KEY_ID!,
    secretAccessKey: process.env.DO_SECRET_ACCESS_KEY!,
  },
})
const BUCKET = process.env.DO_SPACE!

app.post('/presign/spaces', async (req, res) => {
  const { path, op, opts } = req.body
  const key = path.replace(/^\/+/, '')
  const ttl = typeof opts?.ttlSec === 'number' ? opts.ttlSec : 300
  let cmd
  switch (op) {
    case 'GET':    cmd = new GetObjectCommand({ Bucket: BUCKET, Key: key }); break
    case 'PUT':    cmd = new PutObjectCommand({ Bucket: BUCKET, Key: key, ContentType: opts?.contentType }); break
    case 'HEAD':   cmd = new HeadObjectCommand({ Bucket: BUCKET, Key: key }); break
    case 'DELETE': cmd = new DeleteObjectCommand({ Bucket: BUCKET, Key: key }); break
    case 'LIST':   cmd = new ListObjectsV2Command({
      Bucket: BUCKET,
      Prefix: opts?.listPrefix,
      Delimiter: opts?.listDelimiter,
      ContinuationToken: opts?.listContinuationToken,
    }); break
    case 'COPY':   cmd = new CopyObjectCommand({
      Bucket: BUCKET,
      Key: key,
      CopySource: `${BUCKET}/${opts?.copySource}`,
    }); break
  }
  res.json({ url: await getSignedUrl(client, cmd, { expiresIn: ttl }) })
})

2. Browser: wire it up

import { DigitalOceanResource, MountMode, Workspace } from '@struktoai/mirage-browser'

const spaces = new DigitalOceanResource({
  bucket: 'my-space',
  region: 'nyc3',
  presignedUrlProvider: async (path, op, opts) => {
    const r = await fetch('/presign/spaces', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ path, op, opts }),
    })
    const { url } = await r.json()
    return url
  },
})

const ws = new Workspace({ '/bucket/': spaces }, { mode: MountMode.READ })
region on the browser config is only used for display/logging; the actual endpoint is baked into the presigned URLs your backend returns.

3. CORS

Spaces supports the S3 PutBucketCors call, so the AWS CLI works against the Spaces endpoint (you can also use the DigitalOcean control panel, Space -> Settings -> CORS Configurations):
aws s3api put-bucket-cors --bucket "$DO_SPACE" \
  --endpoint-url "https://$DO_REGION.digitaloceanspaces.com" \
  --cors-configuration '{"CORSRules":[{"AllowedOrigins":["http://localhost:5173","https://app.example.com"],"AllowedMethods":["GET","PUT","HEAD","DELETE","POST"],"AllowedHeaders":["*"],"ExposeHeaders":["ETag","Content-Length","Content-Type","Last-Modified"]}]}'
See the DigitalOcean resource docs for the equivalent Python wiring.