# S3-compatible home Source: https://selfstore.dev/docs/s3 Back up to any S3-compatible bucket the user controls - Amazon S3, Cloudflare R2, Backblaze B2, MinIO - with one call, store.connectS3. The browser signs each request itself; no serverless function, no SDK. 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 ```ts 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](https://selfstore.dev/docs/security)). - **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): ```json [ { "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`](https://selfstore.dev/docs/advanced) yourself, or attaching S3 as a [resilience replica](https://selfstore.dev/docs/resilience): ```ts 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 }); ``` Map of this site for a model: https://selfstore.dev/llms.txt Every page in one file: https://selfstore.dev/llms-full.txt