Search selfstore
v1.8.21

S3-compatible home

An S3-compatible bucket is the durable home for teams and power users who already run object storage: Amazon S3, Cloudflare R2, Backblaze B2, a self-hosted MinIO. The user brings a bucket and a key pair; the store writes one encrypted backup object there. There is no AWS SDK and no serverless signer in the way: the browser builds and signs every request itself.

One call

const outcome = await store.connectS3({
  endpoint: 'https://s3.eu-west-3.amazonaws.com', // or your R2 / B2 / MinIO origin
  region: 'eu-west-3',
  bucket: 'my-app-backups',
  key: 'ada/my-app.zip', // the object key the backup lives at
  accessKeyId,
  secretAccessKey,
});
// 'started' | 'merged' | 'manual' | 'cancelled'

As with every home, two passwords can be in play: the bucket credential (the access key pair, how the store reaches S3) and the backup password that encrypts the object itself (store.protect(...), or connectS3(config, { password }) for an already-encrypted object).

Signed in the browser, no SDK

selfstore signs each request with AWS Signature V4 using WebCrypto (HMAC-SHA256). The secret key derives a per-request signature locally and never leaves the device - only the signature, the access-key id and the signed headers travel. That is stronger than a bearer token on the wire, and it is why S3 works with no broker: the signing that a serverless function usually does happens in the page.

  • https is enforced (loopback aside for a local MinIO), so object bytes and the signature are never sent in the clear.
  • The secret key is sealed at rest under the same non-extractable per-device key as the WebDAV credential - it survives a reload but resists casual inspection of IndexedDB (not code in your origin; see the security guide).
  • Path-style by default (endpoint/bucket/key), which every provider and a plain MinIO accept. Pass forcePathStyle: false for virtual-hosted-style (bucket.endpoint/key) where you prefer it.

The bucket needs CORS

The request comes from your app’s browser origin, so the bucket must allow it. A minimal rule (adjust the origin and methods to your setup):

[
  {
    "AllowedOrigins": ["https://your-app.example"],
    "AllowedMethods": ["GET", "PUT", "HEAD"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["ETag"]
  }
]

Your app’s Content-Security-Policy must also allow the endpoint host in connect-src. A missing CORS rule is the usual first-run stumble; a connectS3 that never resolves is almost always this.

Reading the outcomes

Same contract as every home. started wrote this device’s data to an empty key; merged folded an existing backup in; cancelled means the credentials were rejected (wrong key, or the object is encrypted and you passed no password - PASSWORD_REQUIRED is thrown before anything changes). A later 403 from S3 surfaces as a genuine access loss (re-enter the keys); a 404 just means the object does not exist yet and the key is writable.

On the advanced store

Driving createLocalStore yourself, or attaching S3 as a resilience replica:

import { s3Target } from 'selfstore/advanced';

const target = await s3Target.connect({
  kv: store.flowHost.kv,
  config: { endpoint, region, bucket, key, accessKeyId, secretAccessKey },
});
await store.advanced.attachTarget(target, { password: backupPassword });