Skip to main content
MinIO speaks the S3 API with AWS Signature V4 against your own server endpoint (e.g. http://localhost:9000). MinIO is self-hosted, so the endpoint is required and path-style addressing is used by default. Credentials (access key + secret key from the MinIO Console) are created the same way in both runtimes, see MinIO Credentials.

Node (server-side)

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

const minio = new MinIOResource({
  bucket: process.env.MINIO_BUCKET!,
  endpoint: process.env.MINIO_ENDPOINT!,
  accessKeyId: process.env.MINIO_ACCESS_KEY!,
  secretAccessKey: process.env.MINIO_SECRET_KEY!,
})

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

Browser (presigned URLs)

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

1. Server: sign URLs with the MinIO endpoint

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

const client = new S3Client({
  region: 'us-east-1',
  endpoint: process.env.MINIO_ENDPOINT,
  forcePathStyle: true,
  credentials: {
    accessKeyId: process.env.MINIO_ACCESS_KEY!,
    secretAccessKey: process.env.MINIO_SECRET_KEY!,
  },
})
const BUCKET = process.env.MINIO_BUCKET!

app.post('/presign/minio', 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 }) })
})
MinIO uses path-style URLs (forcePathStyle: true), virtual-hosted style requires extra DNS setup on a self-hosted server.

2. Browser: wire it up

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

const minio = new MinIOResource({
  bucket: 'my-bucket',
  endpoint: 'http://localhost:9000',
  presignedUrlProvider: async (path, op, opts) => {
    const r = await fetch('/presign/minio', {
      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/': minio }, { mode: MountMode.READ })
endpoint on the browser config is only used for display/logging; the actual endpoint is baked into the presigned URLs your backend returns.

3. CORS

MinIO answers CORS preflights for all origins by default, so local dev typically works without configuration. To restrict origins, set the server environment variable and restart MinIO:
MINIO_API_CORS_ALLOW_ORIGIN=http://localhost:5173,https://app.example.com
See the MinIO resource docs for the equivalent Python wiring.