Your storage, your rules.
DevSafe does not run a server. Your encrypted git bundles go straight to an S3-compatible bucket that you own. No middleman, no vendor lock-in, no surprise egress bills.
Supported providers
Any storage provider that speaks the S3 API works with DevSafe. These four are tested and documented.
| Provider | Egress fees | Notes |
|---|---|---|
| Cloudflare R2 | None | Recommended. Zero egress means restores cost nothing. |
| AWS S3 | $0.09/GB | Widely supported. Use S3 Intelligent-Tiering to reduce storage costs. |
| MinIO | None (self-hosted) | Fully self-hosted. Runs on your own hardware or VM. |
| Backblaze B2 | $0.01/GB | Low cost. S3-compatible API available on all buckets. |
Restoring from a backup should never cost money. Cloudflare R2 charges $0 for egress, so you can restore as often as you need without worrying about bandwidth bills. Storage is $0.015/GB/month.
Setup with Cloudflare R2
Create an R2 bucket
Log in to the Cloudflare dashboard. Go to R2 Object Storage and create a new bucket. The name can be anything (for example, devsafe-backups). Pick the region closest to you.
Create API credentials
In the R2 section, go to Manage R2 API Tokens. Create a token with Object Read & Write permission scoped to your bucket. Save the Access Key ID and Secret Access Key.
Configure DevSafe
Run the init-storage command. DevSafe stores your credentials in the system keychain, not in a config file.
$ devsafe init-storage --provider r2 \ --bucket devsafe-backups \ --account-id your-cloudflare-account-id \ --access-key-id your-access-key \ --secret-access-key your-secret-key ✓ credentials saved to system keychain ✓ bucket reachable (us-east-1) ✓ write test passed ✓ storage ready
That is it. DevSafe will now upload encrypted git bundles to your R2 bucket. Run devsafe backup to create your first snapshot.
Setup with AWS S3
Create an S3 bucket
In the AWS console, create a new S3 bucket. Enable versioning if you want AWS-level history on top of DevSafe's own versioning. Block all public access (DevSafe never needs public URLs).
Create an IAM user with minimal permissions
Create a dedicated IAM user for DevSafe. Attach this inline policy, which grants only the permissions DevSafe needs.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::your-bucket-name",
"arn:aws:s3:::your-bucket-name/*"
]
}]
}
Configure DevSafe
Point DevSafe at your S3 bucket.
$ devsafe init-storage --provider s3 \ --bucket your-bucket-name \ --region us-west-2 \ --access-key-id AKIA... \ --secret-access-key your-secret-key ✓ credentials saved to system keychain ✓ bucket reachable (us-west-2) ✓ storage ready
Setup with MinIO
MinIO gives you a fully self-hosted S3-compatible storage server. Everything stays on your own hardware. One Docker command gets you running.
$ docker run -d --name minio \ -p 9000:9000 -p 9001:9001 \ -v ~/minio-data:/data \ -e MINIO_ROOT_USER=minioadmin \ -e MINIO_ROOT_PASSWORD=minioadmin \ minio/minio server /data --console-address ":9001"
Open http://localhost:9001 to access the MinIO console. Create a bucket (for example, devsafe), then configure DevSafe with the custom endpoint.
$ devsafe init-storage --provider s3 \ --endpoint http://localhost:9000 \ --bucket devsafe \ --access-key-id minioadmin \ --secret-access-key minioadmin ✓ credentials saved to system keychain ✓ bucket reachable (custom endpoint) ✓ storage ready
The example above uses MinIO's default username and password. For production use, set strong values for MINIO_ROOT_USER and MINIO_ROOT_PASSWORD and create a dedicated access key through the MinIO console.
Storage key format
Every encrypted git bundle is stored with a structured key name (the "path" inside your bucket). This format is not arbitrary. It is what makes stateless reconstruction possible.
# Pattern {repo-id}/{type}/{sequence-zero-padded}.enc # Examples a1b2c3d4/bundle/000001.enc a1b2c3d4/bundle/000002.enc a1b2c3d4/working/000001.enc e5f6g7h8/bundle/000001.enc
Each part of the key has a purpose:
- repo-id is a stable identifier derived from the repository. It groups all backups for one repo together.
- type separates committed data (
bundle) from uncommitted work (working). - sequence is a zero-padded counter that increases with each backup. Sorting by key name gives you chronological order.
- .enc indicates the file is encrypted with nonce-unique AEAD (AES-256-GCM).
Why this matters: DevSafe can reconstruct your complete backup timeline from key names alone, using a single ListObjectsV2 API call. No database, no sidecar files, no server. If your DevSafe config is lost, pointing a fresh install at the same bucket gives you everything back (assuming you still have your encryption key).
Lifecycle rules
Over time, old backups accumulate. You can use your storage provider's built-in lifecycle rules to automatically delete or archive old objects. DevSafe's devsafe gc command also handles cleanup, but provider-native rules give you a safety net that works even if the CLI is not running.
Cloudflare R2
In the R2 dashboard, go to your bucket settings and add a lifecycle rule. For example, delete objects older than 90 days.
$ npx wrangler r2 bucket lifecycle set devsafe-backups \ --id cleanup-old \ --expire-days 90 ✓ lifecycle rule created
AWS S3
Use a lifecycle configuration to transition old objects to cheaper storage classes or delete them entirely.
{
"Rules": [{
"ID": "archive-old-backups",
"Status": "Enabled",
"Transitions": [{
"Days": 30,
"StorageClass": "GLACIER_IR"
}],
"Expiration": {
"Days": 365
}
}]
}
$ aws s3api put-bucket-lifecycle-configuration \ --bucket your-bucket-name \ --lifecycle-configuration file://lifecycle.json
Both work independently. Provider lifecycle rules act on object age. devsafe gc understands backup semantics and keeps the minimum set needed for full reconstruction. Using both together is safe and recommended.