DEV Community

Cover image for Buckets and Object CRUD in Lioran S3 with TypeScript
Swaraj Puppalwar
Swaraj Puppalwar

Posted on

Buckets and Object CRUD in Lioran S3 with TypeScript

Buckets and Object CRUD in Lioran S3

Object storage becomes useful when the abstractions stay boring: buckets contain objects, object keys are identifiers, metadata is inspectable, large transfers stream, and listing does not explode memory.

That is the model used by Lioran S3 V1 Pre-Alpha, built by Lioran Developer Solutions under Lioran Group.

I’m Swaraj Puppalwar, Founder & CTO, and this article documents the current TypeScript bucket/object API.

Create a client

import { BastionClient } from "@liorans3/driver";

const client = new BastionClient(
  process.env.LIORAN_S3_URI!
);
Enter fullscreen mode Exit fullscreen mode

Bucket lifecycle

Create:

await client.buckets.create("media", {
  versioning: false,
  quotaBytes: 50 * 1024 * 1024 * 1024,
});
Enter fullscreen mode Exit fullscreen mode

List:

const buckets = await client.buckets.list();

for (const bucket of buckets) {
  console.log(bucket);
}
Enter fullscreen mode Exit fullscreen mode

Inspect:

const info = await client.buckets.get("media");

console.log(info);
Enter fullscreen mode Exit fullscreen mode

Update:

await client.buckets.update("media", {
  quotaBytes: 100 * 1024 * 1024 * 1024,
  tags: {
    environment: "development",
    owner: "backend",
  },
});
Enter fullscreen mode Exit fullscreen mode

Delete an empty bucket:

await client.buckets.delete("media");
Enter fullscreen mode Exit fullscreen mode

Force deletion:

await client.buckets.delete("media", {
  force: true,
});
Enter fullscreen mode Exit fullscreen mode

Use forced deletion carefully because it removes objects.

Scoped bucket handle

const bucket = client.bucket("media");
Enter fullscreen mode Exit fullscreen mode

The handle owns the bucket-specific object API.

PUT

From text:

await bucket.put(
  "readme.txt",
  "hello",
  { contentType: "text/plain" }
);
Enter fullscreen mode Exit fullscreen mode

From a buffer:

const bytes = Buffer.from("binary-data");

await bucket.put(
  "data/blob.bin",
  bytes,
  { contentType: "application/octet-stream" }
);
Enter fullscreen mode Exit fullscreen mode

From a file stream:

import fs from "node:fs";

await bucket.put(
  "images/avatar.png",
  fs.createReadStream("./avatar.png"),
  { contentType: "image/png" }
);
Enter fullscreen mode Exit fullscreen mode

Streaming is important because Lioran S3's storage design is built around bounded-memory I/O.

GET

const object = await bucket.get(
  "images/avatar.png"
);
Enter fullscreen mode Exit fullscreen mode

Write to disk:

await object.writeToFile(
  "./downloads/avatar.png"
);
Enter fullscreen mode Exit fullscreen mode

Read into memory only when appropriate:

const buffer = await object.buffer();
Enter fullscreen mode Exit fullscreen mode

Text:

const text = await object.text();
Enter fullscreen mode Exit fullscreen mode

JSON:

const config = await object.json<{
  enabled: boolean;
}>();
Enter fullscreen mode Exit fullscreen mode

HEAD

Use HEAD when you need metadata without object content.

const metadata = await bucket.head(
  "images/avatar.png"
);

console.log(metadata.contentType);
console.log(metadata.contentLength);
console.log(metadata.etag);
console.log(metadata.sha256);
Enter fullscreen mode Exit fullscreen mode

DELETE

await bucket.delete(
  "images/avatar.png"
);
Enter fullscreen mode Exit fullscreen mode

Byte-range reads

Range requests are useful for media playback, resumable downloads, large archives, and any client that needs only part of an object.

First MiB:

const part = await bucket.getRange(
  "videos/demo.mp4",
  {
    start: 0,
    end: 1024 * 1024 - 1,
  }
);
Enter fullscreen mode Exit fullscreen mode

Suffix:

const tail = await bucket.getRange(
  "logs/server.log",
  {
    suffix: 4096,
  }
);
Enter fullscreen mode Exit fullscreen mode

Cursor pagination

Avoid APIs that return millions of object records in one response.

Fetch a page:

let cursor: string | undefined;

do {
  const page = await bucket.listPage({
    prefix: "uploads/2026/",
    limit: 100,
    cursor,
  });

  for (const object of page.objects) {
    console.log(object.key);
  }

  cursor = page.next_cursor;
} while (cursor);
Enter fullscreen mode Exit fullscreen mode

The cursor remains opaque to the application.

Prefixes are logical

A key such as:

uploads/2026/10/avatar.png
Enter fullscreen mode Exit fullscreen mode

looks hierarchical, but treat it as an object-storage key, not as permission to assume the raw physical filesystem contains the same path.

Lioran S3 internally separates user-controlled keys from physical storage placement.

That reduces path traversal risk and gives the storage engine freedom to choose safer layouts.

Server-side image optimization

The current driver also exposes an experimental optimization API.

const result = await bucket.optimize(
  "photos/hero.png",
  {
    width: 1200,
    height: 800,
    fit: "cover",
    format: "webp",
    quality: 80,
    targetKey:
      "photos/hero-1200x800.webp",
  }
);

console.log(result.variant.target_key);
Enter fullscreen mode Exit fullscreen mode

Quotas

Bucket quotas provide a hard storage boundary.

Example:

await client.buckets.update("media", {
  quotaBytes: 25 * 1024 * 1024 * 1024,
});
Enter fullscreen mode Exit fullscreen mode

Applications should still handle quota errors explicitly.

import {
  QuotaExceededError,
} from "@liorans3/driver";

try {
  await bucket.put("large.bin", hugeStream);
} catch (error) {
  if (error instanceof QuotaExceededError) {
    console.error("Storage quota exceeded");
  }
}
Enter fullscreen mode Exit fullscreen mode

Design rule

Your application should generally:

  • stream large inputs
  • stream large outputs
  • use HEAD instead of GET for metadata
  • paginate listings
  • keep keys deterministic
  • avoid embedding credentials into source code
  • use quotas intentionally

The API surface is straightforward by design. The interesting engineering belongs underneath it.

Docs: https://docs.liorans3.sbs

Top comments (0)