---
title: "@unikvs/s3.bun"
description: "Learn how to use @unikvs/s3.bun to store data in S3-compatible object storage from Bun."
---

## Overview [#overview]

`@unikvs/s3.bun` is a Bun-only storage plugin that saves byte arrays to S3-compatible object storage using Bun's built-in S3 client. Bun only It does not depend on AWS SDK; it dynamically imports `S3Client` from the `bun` module for object operations. It handles byte arrays only. `write` accepts `Uint8Array<ArrayBuffer>` and `read` returns `Uint8Array<ArrayBuffer>`.

Install it as follows.

```package-install
npm install @unikvs/s3.bun
```

It depends on `@unikvs/core`. No AWS SDK is required.

## Usage [#usage]

The storage class is `S3`.

```ts
import { S3 } from "@unikvs/s3.bun";
```

```ts
new S3(bucket: string, options: S3StorageOptions = {})
```

| Argument  | Type               | Description                                                      |
| --------- | ------------------ | ---------------------------------------------------------------- |
| `bucket`  | `string`           | The destination bucket name. All operations use it.              |
| `options` | `S3StorageOptions` | Bun's `S3Options` without `bucket`, plus `allowRepair`. Defaults to an empty object. |
| `options.allowRepair` | `boolean` | Whether to allow repair writes. Defaults to `false`. When `false`, `write` and `getWritable` for repairs (where `vars["unikvs:repair"]` is `true`) throw `RepairNotAllowedError`. |

`S3StorageOptions` accepts `accessKeyId`, `secretAccessKey`, `sessionToken`, `region`, `endpoint`, `virtualHostedStyle`, `partSize`, `queueSize`, `retry`, and more. Omitted fields are resolved by Bun from environment variables such as `S3_ACCESS_KEY_ID` and `AWS_ACCESS_KEY_ID`. Values in `options` take precedence over environment variables.

:::note
Credential resolution is delegated to Bun's standard mechanisms. When connecting to S3-compatible services, specify `endpoint` and `region`.
:::

The lifecycle is as follows. Right after instantiation, `isOpen` is `false`. Calling `open()` creates an internal `S3Client` and sets `true`. `open()` is asynchronous because it uses a dynamic import. Calling `close()` drops the reference and resets to `false`. Bun's `S3Client` has no dispose API, so there is no release step.

```ts
import { UniKvs } from "unikvs";
import { S3 } from "@unikvs/s3.bun";

const storage = new S3("my-bucket", {
  region: "ap-northeast-1",
});

await storage.open();

const kvs = UniKvs.config().appendStorage(storage).create();
```

:::warning
Creating the bucket is not this package's responsibility. Create it beforehand via the console or similar tooling.
:::

## Object Layout [#layout]

Keys map directly to object keys. No prefixing or escaping is applied.

| UniKVS key               | S3 object key            |
| ------------------------ | ------------------------ |
| `test.txt`               | `test.txt`               |
| `folder/テスト #123.dat` | `folder/テスト #123.dat` |

Single writes use `S3Client.write`, reads use `S3File.arrayBuffer`, existence checks use `S3Client.exists`, and deletions use `S3Client.delete`. `clear` lists all objects in the bucket with `S3Client.list`, advances pages via `continuationToken`, and deletes them 25 at a time in parallel. No prefix filtering is applied.

:::warning
`clear` deletes all objects in the bucket. Use a dedicated bucket for UniKVS. Shared buckets lose other apps' data too.
:::

## Streams and Multipart [#multipart]

A single `write` saves with `S3Client.write`, and a single `read` converts the `S3File.arrayBuffer` result into `Uint8Array`.

Writable streams use the `NetworkSink` returned by `S3File.writer`, so large payloads benefit from multipart upload.

```ts
const writable = storage.getWritable({ key, vars, signal });
```

The arguments are `vars`, `key`, and `signal`. The part size is read from `vars`. The `@unikvs/s3.bun:partSize` key takes precedence, falling back to `@unikvs/s3:partSize`. Specify a positive integer in bytes. When omitted, Bun's default of 5 MiB applies.

:::tip
For large data, use a writable stream instead of a single `write`. It supports multipart upload.
:::

If `signal` is already aborted, it throws `StorageAbortedError` without starting. Bun's `NetworkSink` has no abort API, so an in-flight request cannot be canceled mid-write. Calling `close()` after an abort rejects with the `signal` reason and never completes the object. Incomplete multipart uploads are not exposed as objects, so partial contents can never be read.

Obtain a readable stream as follows.

```ts
const readable = storage.getReadable({ key, signal });
```

Internally it calls `S3File.stream` and relays chunks while checking `signal`. Failures for missing keys surface as `S3Error` when the stream is read.

## Errors [#errors]

| Error | When thrown |
| --- | --- |
| `UnsupportedRuntimeError` | Thrown by `open()` when the runtime has no `Bun` global. A shared error from `@unikvs/core`. |
| `StorageNotOpenError` | Thrown by `close()` when the storage is not open. |
| `InvalidPartSizeError` | Thrown by `getWritable` when the part size in `vars` is not a positive integer. A shared error from `@unikvs/core`. |
| `StorageAbortedError` | Thrown when the `signal` for `getWritable` is already aborted. A shared error from `@unikvs/core`. |

- `read` on a missing key fails with an error. Use `exists` beforehand when needed.
- `delete` on a missing key does not fail.
- Failures such as auth errors reject the stream's `close()` with the failure reason.

## Examples [#examples]

A minimal example against S3-compatible storage. The endpoint and credentials are dummy values for common S3-compatible storage (such as MinIO). Create the bucket beforehand.

```ts
import { S3 } from "@unikvs/s3.bun";

const storage = new S3("my-bucket", {
  endpoint: "http://127.0.0.1:9000",
  region: "ap-northeast-1",
  accessKeyId: "minioadmin",
  secretAccessKey: "minioadmin",
});

await storage.open();

const signal = AbortSignal.timeout(30_000);
const key = "hello.txt";
const data = new TextEncoder().encode("Hello S3");

await storage.write({ key, data, signal });
const loaded = await storage.read({ key, signal });
console.log(new TextDecoder().decode(loaded));

storage.close();
```
