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. PassforcePathStyle: falsefor 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 });