# Versioned buckets

Buckets with versioning enabled list their versions and delete markers, and a delete there adds a marker over the current object.

A versioned bucket keeps what a key held before, and the simulator lists those rows and handles a delete as S3 does. Load the archive scenario, list the history of a key whose latest version is a delete marker and delete a second key:

```bash
s3curl() { curl -s --aws-sigv4 'aws:amz:us-east-1:s3' --user 'AKIAIOSFODNN7EXAMPLE:wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' "$@"; }
curl -s -X POST "$RYSTIC_URL/_rystic/scenario" -d '{"name": "s3_versioned_archive"}' -w '%{http_code}\n'
# -> 204
s3curl "$RYSTIC_URL/acme-demo-archive?versions&prefix=notes/" | grep -oE '<DeleteMarker>|<Version>|<IsLatest>[a-z]+'
# -> <DeleteMarker>
# -> <IsLatest>true
# -> <Version>
# -> <IsLatest>false
# -> <Version>
# -> <IsLatest>false
s3curl "$RYSTIC_URL/acme-demo-archive/notes/draft.md" | grep -oE '<Code>[^<]*'
# -> <Code>NoSuchKey
s3curl -X DELETE "$RYSTIC_URL/acme-demo-archive/reports/2025-q4.csv" -D - -o /dev/null | grep -i '^x-amz-delete-marker'
# -> x-amz-delete-marker: true
```

### What is versioned

`s3_versioned_archive` holds `acme-demo-archive`, versioning enabled, with six version rows across three keys:

- `reports/2025-q3.csv`, overwritten once
- `reports/2025-q4.csv`, with one version
- `notes/draft.md`, whose latest version is a delete marker over two older ones, so it has no current object

`s3_encrypted_vault` holds a bucket with versioning and MFA delete enabled. `s3_bucket_farm` holds buckets in every versioning state.

`ListObjectVersions` returns `IsLatest`, the older versions and each `DeleteMarker`. `GetBucketVersioning` and `PutBucketVersioning` read and set the bucket's status.

### Deletes and delete markers

A `DeleteObject` in a versioned bucket makes a delete marker the current version and returns `x-amz-delete-marker: true`. The object stays listed as an older version, and a later `GetObject` returns `404 NoSuchKey`, as on S3.

A `DeleteObject` with `?versionId=` removes that version for good and adds no delete marker. The current object stays when the version named is an older one.

**Differs from S3:** when the version deleted is the current one, the older versions stay listed with `IsLatest` false and `GetObject` returns 404. S3 makes the newest remaining version current.

**Differs from S3:** `DeleteObjects` (`POST /{Bucket}?delete`) ignores each `VersionId` and deletes the key's current object. It adds no delete marker. The key's latest row stays `IsLatest` true, and `GetObject` returns 404.

### Writes and older versions

Only the latest version of a key has a body. A scenario's older versions are rows in the version listing.

**Differs from S3:** `GetObject`, `GetObjectAttributes`, `GetObjectAcl` and the object tagging calls ignore `?versionId=` and act on the current object, whatever version it names.

**Differs from S3:** `HeadObject` with `?versionId=` returns 404 for any version but the current one.

**Differs from S3:** a `CopyObject` whose `x-amz-copy-source` names an older version returns `404 NoSuchKey`.

**Differs from S3:** an `UploadPartCopy` whose `x-amz-copy-source` names an older version copies the current object.

**Differs from S3:** a `PutObject` into a versioned bucket writes a `null` version, not a new one ([Limitations](https://www.rystic.ai/docs/simulators/s3.md#limitations)). So test version history against the scenarios' rows, not against your own overwrites.

---

Source: https://www.rystic.ai/docs/simulators/s3/features/versioned-buckets · Markdown: https://www.rystic.ai/docs/simulators/s3/features/versioned-buckets.md
