# Versioning

Versioning keeps every write of a key instead of overwriting in place. With versioning enabled, an overwrite creates a
new version (the previous one stays recoverable), and a delete writes a delete marker (the object reads as gone, but the
prior versions are still there). This is how you protect against accidental overwrites and deletes, and how object lock
holds an immutable history.

A bucket is in one of three versioning states:

| State              | Behavior                                                               |
| ------------------ | ---------------------------------------------------------------------- |
| Disabled (default) | One version per key; overwrites and deletes are permanent.             |
| Enabled            | Every overwrite is a new version; every delete writes a delete marker. |
| Suspended          | New writes get the null version id; existing versions are preserved.   |

Versioning can be enabled, then suspended, then enabled again. Suspending never deletes existing versions.

## Set and read the versioning state

Enable versioning before you need it: a version history only exists for writes made while versioning was enabled. The
native client sets the state with `setBucketVersioning` and reads it with `getBucketVersioning`.

:::: native

::: code-group

```ts [Node]
await native.setBucketVersioning("reports", "Enabled");
const status = await native.getBucketVersioning("reports"); // 'Enabled' | 'Suspended' | ''
```

```go [Go]
// The Go native client uses the lowercase "enabled" / "suspended" spelling:
_, err := native.SetBucketVersioning(ctx, "reports", "enabled")
state, err := native.GetBucketVersioning(ctx, "reports")
fmt.Println(state.Status) // "enabled" / "suspended" / "disabled"
```

```java [Java]
nativeClient.setBucketVersioning("reports", "enabled");
var state = nativeClient.getBucketVersioning("reports");
System.out.println(state.status()); // "enabled" / "suspended" / "disabled"
```

:::

::::

:::: s3

The S3 client takes `Enabled` or `Suspended` (there is no `Disabled` value to set; a bucket that was never enabled reads
back as an empty string):

::: code-group

```ts [Node]
await s3.putBucketVersioning("reports", "Enabled");

const status = await s3.getBucketVersioning("reports");
console.log(status); // "Enabled" (or "" when never enabled)
```

```go [Go]
err := s3.PutBucketVersioning(ctx, "reports", lockwellsdk.VersioningEnabled)

status, err := s3.GetBucketVersioning(ctx, "reports")
fmt.Println(status) // "Enabled" (or "" when never enabled)
```

```java [Java]
s3.putBucketVersioning("reports", "Enabled");

String status = s3.getBucketVersioning("reports"); // "Enabled" or ""
```

:::

::::

Note the spelling difference: the native client takes `enabled` / `suspended` (the lowercase wire form) while the S3
client takes the capitalized `Enabled` / `Suspended`.

::: warning The two clients spell the versioning state differently. The native client uses lowercase `enabled` /
`suspended`; the S3 client uses capitalized `Enabled` / `Suspended`. :::

## Versions and delete markers

Once versioning is enabled, each PUT to the same key stacks a new version. The latest version is what a plain GET
returns.

:::: native

::: code-group

```ts [Node]
await native.setBucketVersioning("docs", "Enabled");

const v1 = await native.putObject("docs", "readme.txt", "first");
const v2 = await native.putObject("docs", "readme.txt", "second");
console.log(v1.versionId, v2.versionId); // two distinct ids

const latest = await native.getObject("docs", "readme.txt");
console.log(latest.body.toString()); // "second"
```

:::

::::

:::: s3

```ts [Node]
await s3.putBucketVersioning("docs", "Enabled");

const v1 = await s3.putObject("docs", "readme.txt", "first");
const v2 = await s3.putObject("docs", "readme.txt", "second");
console.log(v1.versionId, v2.versionId); // two distinct ids

const latest = await s3.getObject("docs", "readme.txt");
console.log(latest.body.toString()); // "second"
```

::::

A delete in a versioned bucket does not remove the data. It writes a delete marker as the new latest version, so a plain
GET returns a not-found while the prior versions remain.

:::: native

::: code-group

```ts [Node]
await native.deleteObject("docs", "readme.txt"); // writes a delete marker

// A plain read now reports not-found:
try {
  await native.getObject("docs", "readme.txt");
} catch (err) {
  // isNativeNotFound(err) === true
}

// The prior versions are still readable by version id (see below).
```

:::

::::

:::: s3

```ts [Node]
await s3.deleteObject("docs", "readme.txt"); // writes a delete marker

try {
  await s3.getObject("docs", "readme.txt");
} catch (err) {
  // isNotFound(err) === true
}
```

::::

To restore, delete the delete marker by its version id, which makes the previous version the latest again.

## List versions and delete markers

`listObjectVersions` returns both real versions and delete markers in one listing, each carrying its `versionId`,
`isLatest` flag, and (for versions) size and ETag. It is paginated by a `(keyMarker, versionIdMarker)` pair. On the
native path, delete markers are folded into the `versions` list and flagged by a `deleteMarker` field on each entry
(Node also surfaces a separate `deleteMarkers` array).

:::: native

::: code-group

```ts [Node]
const page = await native.listObjectVersions("docs", { prefix: "readme", maxKeys: 100 });
for (const v of page.versions) {
  console.log("version", v.key, v.versionId, v.isLatest, v.deleteMarker, v.size);
}
```

```go [Go]
page, err := native.ListObjectVersions(ctx, lockwellnative.ListObjectVersionsInput{
    Bucket: "docs", Prefix: "readme", MaxKeys: 100,
})
for _, v := range page.Versions {
    fmt.Println(v.Key, v.VersionID, v.IsLatest, v.DeleteMarker, v.Size)
}
```

```java [Java]
import com.lockwell.sdk.nativeapi.NativeTypes.ListVersionsOptions;

var page = nativeClient.listObjectVersions("docs",
    new ListVersionsOptions("readme", null, null, 100));
page.versions().forEach(v ->
    System.out.println(v.key() + " " + v.versionId() + " latest=" + v.isLatest()
        + " marker=" + v.deleteMarker()));
```

:::

::::

:::: s3

The S3 `ListObjectVersions` splits the two into a `versions` list and a `deleteMarkers` list:

::: code-group

```ts [Node]
const page = await s3.listObjectVersions("docs", { prefix: "readme", maxKeys: 100 });
for (const v of page.versions) {
  console.log("version", v.key, v.versionId, v.isLatest, v.size);
}
for (const m of page.deleteMarkers) {
  console.log("delete-marker", m.key, m.versionId, m.isLatest);
}
```

```go [Go]
page, err := s3.ListObjectVersions(ctx, "docs",
    lockwellsdk.WithVersionsPrefix("readme"),
    lockwellsdk.WithVersionsMaxKeys(100))
for _, v := range page.Versions {
    fmt.Println("version", v.Key, v.VersionID, v.IsLatest, v.Size)
}
for _, m := range page.DeleteMarkers {
    fmt.Println("delete-marker", m.Key, m.VersionID, m.IsLatest)
}
```

```java [Java]
import com.lockwell.sdk.LockwellClient.ListVersionsOptions;

var page = s3.listObjectVersions("docs",
    new ListVersionsOptions().prefix("readme").maxKeys(100));
page.versions().forEach(v ->
    System.out.println("version " + v.key() + " " + v.versionId() + " latest=" + v.isLatest()));
page.deleteMarkers().forEach(m ->
    System.out.println("delete-marker " + m.key() + " " + m.versionId()));
```

:::

::::

### Paginate every version

A version-id marker requires a key marker (both clients reject one without the other). On the native path, the Node
client exposes a `paginateObjectVersions` async iterator that threads both markers for you; in Go and Java, follow
`IsTruncated` with `NextKeyMarker` and `NextVersionIDMarker`.

:::: native

::: code-group

```ts [Node]
for await (const page of native.paginateObjectVersions("docs", { prefix: "readme" })) {
  for (const v of page.versions) console.log(v.key, v.versionId);
}
```

```go [Go]
var keyMarker, versionMarker string
for {
    page, err := native.ListObjectVersions(ctx, lockwellnative.ListObjectVersionsInput{
        Bucket: "docs", Prefix: "readme",
        KeyMarker: keyMarker, VersionIDMarker: versionMarker,
    })
    if err != nil {
        return err
    }
    for _, v := range page.Versions {
        fmt.Println(v.Key, v.VersionID)
    }
    if !page.IsTruncated {
        break
    }
    keyMarker, versionMarker = page.NextKeyMarker, page.NextVersionIDMarker
}
```

```java [Java]
String keyMarker = null, versionMarker = null;
for (;;) {
    var page = nativeClient.listObjectVersions("docs",
        new ListVersionsOptions("readme", keyMarker, versionMarker, null));
    page.versions().forEach(v -> System.out.println(v.key() + " " + v.versionId()));
    if (!page.isTruncated()) break;
    keyMarker = page.nextKeyMarker();
    versionMarker = page.nextVersionIdMarker();
}
```

:::

::::

:::: s3

The S3 client offers the same manual paging plus a built-in paginator:

::: code-group

```ts [Node]
// Async iterator that threads both markers for you:
for await (const page of s3.paginateObjectVersions("docs", { prefix: "readme" })) {
  for (const v of page.versions) console.log(v.key, v.versionId);
}
```

```go [Go]
// Manual paging:
var keyMarker, versionMarker string
for {
    page, err := s3.ListObjectVersions(ctx, "docs",
        lockwellsdk.WithKeyMarker(keyMarker),
        lockwellsdk.WithVersionIDMarker(versionMarker))
    if err != nil {
        return err
    }
    for _, v := range page.Versions {
        fmt.Println(v.Key, v.VersionID)
    }
    if !page.IsTruncated {
        break
    }
    keyMarker, versionMarker = page.NextKeyMarker, page.NextVersionIDMarker
}
```

```java [Java]
var pager = s3.listObjectVersionsPaginator("docs",
    new ListVersionsOptions().prefix("readme"));
while (pager.hasMorePages()) {
    var page = pager.nextPage();
    page.versions().forEach(v -> System.out.println(v.key() + " " + v.versionId()));
}
```

:::

::::

## Read and delete a specific version

Pass a version id to read or delete one exact version instead of the latest. The native client reads with the
`versionId` option on `getObject` and deletes with `deleteObject(bucket, key, { versionId })`.

:::: native

::: code-group

```ts [Node]
// Read an older version:
const old = await native.getObject("docs", "readme.txt", { versionId: v1.versionId });
console.log(old.body.toString()); // "first"

// Permanently delete one version (not a delete marker; the bytes are gone):
await native.deleteObject("docs", "readme.txt", { versionId: v1.versionId });
```

```go [Go]
// Read an older version:
old, err := native.GetObject(ctx, lockwellnative.GetObjectInput{
    Bucket: "docs", Key: "readme.txt", VersionID: v1ID,
})
defer old.Close()

// Permanently delete one version:
_, err = native.DeleteObject(ctx, "docs", "readme.txt", v1ID)
```

```java [Java]
// Read an older version (range null, versionId set):
try (var old = nativeClient.getObject("docs", "readme.txt", null, v1Id)) { /* ... */ }

// Permanently delete one version:
nativeClient.deleteObject("docs", "readme.txt", v1Id);
```

:::

::::

:::: s3

::: code-group

```ts [Node]
// Read an older version:
const old = await s3.getObject("docs", "readme.txt", { versionId: v1.versionId });
console.log(old.body.toString()); // "first"

// Permanently delete one version (not a delete marker; the bytes are gone):
await s3.deleteObject("docs", "readme.txt", { versionId: v1.versionId });
```

```go [Go]
// Read an older version:
old, err := s3.GetObject(ctx, "docs", "readme.txt", lockwellsdk.WithVersionID(v1ID))
defer old.Body.Close()

// Permanently delete one version:
err = s3.DeleteObject(ctx, "docs", "readme.txt", lockwellsdk.WithVersionID(v1ID))
```

```java [Java]
import java.util.Map;

// Read an older version (Range/versionId go through the query map):
var old = s3.getObject("docs", "readme.txt", Map.of("versionId", v1Id));
```

:::

::::

Deleting a specific version id removes that version's data for good. Deleting without a version id (in a versioned
bucket) writes a delete marker instead.

## Suspended versioning and the null version

Suspending versioning stops creating new versions. Writes made while suspended take the special null version id
(`"null"`). A later write to the same key while still suspended overwrites that null version in place, so at most one
null version exists per key. Existing non-null versions from the enabled period are untouched and stay readable by their
version ids.

:::: native

::: code-group

```ts [Node]
await native.setBucketVersioning("docs", "Enabled");
const enabled = await native.putObject("docs", "config.json", '{"v":1}'); // real version id

await native.setBucketVersioning("docs", "Suspended");
await native.putObject("docs", "config.json", '{"v":2}'); // takes the "null" version id
await native.putObject("docs", "config.json", '{"v":3}'); // overwrites the same "null" version

// The enabled-period version is still there:
const v1 = await native.getObject("docs", "config.json", { versionId: enabled.versionId });
```

:::

::::

:::: s3

```ts [Node]
await s3.putBucketVersioning("docs", "Enabled");
const enabled = await s3.putObject("docs", "config.json", '{"v":1}'); // real version id

await s3.putBucketVersioning("docs", "Suspended");
await s3.putObject("docs", "config.json", '{"v":2}'); // takes the "null" version id
await s3.putObject("docs", "config.json", '{"v":3}'); // overwrites the same "null" version

const v1 = await s3.getObject("docs", "config.json", { versionId: enabled.versionId });
```

::::

Re-enabling versioning resumes stacking new versions; it does not retroactively version the writes made while suspended.

## Versioning and object lock

Object lock requires versioning, so a bucket created with object lock has versioning enabled and cannot return to a
non-versioned state. Each version can carry its own retention deadline and legal hold. See
[object lock](/guide/object-lock).

## Related

- [Object lock](/guide/object-lock) for per-version WORM retention and legal holds.
- [Deleting objects](/guide/deleting-objects) for delete markers and batch deletes.
- [Object tags](/guide/object-tags) for tagging a specific version.
- [S3 operations reference](/reference/s3-operations) for the full operation matrix.
