Skip to content

The three surfaces

One lockwelld server exposes one multi-tenant, encrypted object store through three interfaces. They differ only in wire protocol and auth.

Server-side they all flow through the same domain pipeline: the same scope enforcement, bucket policy, encryption, dedup, quota, retention, and audit. None of them relaxes a control the others enforce, and none enables public or anonymous access. Pick the one that fits your stack, or mix them freely.

SurfaceMountListenerAuthUse it for
S3 data plane/ (S3 routes)publicSigV4 (access key)Drop-in for existing S3 clients and tools
Native data plane/api/v1/publicbearer token (lwtk_), minted from an access keyYour own app's object I/O; edge; signed writes
Admin API/admin/api/v1/adminadmin token (lwadm_) + RBACProvisioning tenants, keys, quotas, audit

S3 data plane (SigV4)

The S3-compatible API: SigV4 auth, path-style and virtual-host addressing, versioning, multipart, conditional writes, Object Lock, tagging, and lifecycle. Point any S3 client at the public listener: the AWS SDKs, aws s3, or your existing S3 tooling.

Lockwell also ships a first-party S3 client per language that is a native-first drop-in (idempotent writes, CRC32/CRC32C/CRC64NVME checksums, SSE-S3, streaming).

S3 presign is GET-only The S3 client presigns GET only. There is no presigned PUT, HEAD, or DELETE. For

a browser-direct upload, use a native signed PUT URL (below). :::

ts
import { Client } from "@kelphect/sdk";

const s3 = new Client({
  endpoint: "http://localhost:9000",
  accessKeyId: process.env.LOCKWELL_ACCESS_KEY_ID,
  secretKey: process.env.LOCKWELL_SECRET_KEY,
});
await s3.putObject("my-bucket", "report.txt", Buffer.from("hello"));
const url = s3.presignGetObject("my-bucket", "report.txt", 900); // GET only

The full S3 operation matrix is the S3 operations reference. Use the S3 surface when you already have S3 code or tools, or want strict S3 semantics. For a new app, prefer the native surface.

Native JSON data plane (/api/v1/)

A first-class JSON/HTTP object surface on the same public listener as S3, under a different path prefix, so your app talks to Lockwell natively. No SigV4 signing, no XML. JSON in, JSON out.

Auth is a short-lived native bearer token (lwtk_...) minted from an existing access key. The client auto-manages it for you:

  • mints on first use (POST /api/v1/auth/token),
  • caches until shortly before expiry,
  • refreshes transparently, and
  • re-mints once on a 401.

Token acquisition is concurrency-safe (single-flight), so a burst of requests never mints per request. The Go and Java native clients manage the token the same way. See the Go and Java SDK pages.

Two native-only capabilities the S3 client cannot offer:

  • Native signed URLs for GET and PUT. The S3 presigner is GET-only; the native API signs writes, which is the browser-direct upload path. See Signed URLs.
  • Edge-safe in Node. The native client imports nothing from node:*. It runs unchanged on Cloudflare Workers, Vercel Edge, Bun, and Deno. See Edge runtimes.
ts
import { NativeClient } from "@kelphect/sdk";

const native = new NativeClient({
  endpoint: "http://localhost:9000", // public listener (same host:port as S3)
  accessKeyId: process.env.LOCKWELL_ACCESS_KEY_ID,
  secretKey: process.env.LOCKWELL_SECRET_KEY,
});

await native.putObject("my-bucket", "report.txt", "hello", { contentType: "text/plain" });
const up = await native.signUrl({ method: "PUT", bucket: "my-bucket", key: "photo.jpg", ttlSeconds: 300 });

See Data operations for the full object API and the Native API reference for the route set.

JSON Admin API (/admin/api/v1/)

The control plane: tenants, service accounts, access keys, quotas, and audit, as versioned JSON on a separate admin listener (never the public S3 port). It authenticates with an admin API token (lwadm_..., minted offline with lockwell admin-token create) sent as Authorization: Bearer <token>, not SigV4.

Authorization composes an RBAC role (owner / operator / viewer) with an optional single-tenant scope, so a tenant-scoped token cannot cross to another tenant. Every request, success and denial, is audited. Mutations accept a dryRun flag to preview the plan without applying it.

ts
import { AdminClient } from "@kelphect/sdk";

const admin = new AdminClient({
  endpoint: "http://localhost:9001", // admin listener, not the S3 port
  token: process.env.LOCKWELL_ADMIN_TOKEN, // Authorization: Bearer lwadm_...
});

await admin.createTenant({ id: "acme", name: "Acme Inc" });
const key = await admin.createKey("acme", { scopes: "read,write,delete" });
console.log(key.secretKey); // shown exactly once. Store it now.

See the Admin API reference and Tenancy and auth.

One pipeline, no security bypass

The three surfaces are different transports over one set of in-process domain services. The native API and the Admin API do not reach around the S3 path's guarantees. They call the same tenant lifecycle, metadata repo, scope/policy engine, object coordinator, secret cipher, and auditor:

  • Tenant isolation comes from the credential, never the request path. A native token carries its tenant, and another tenant's bucket simply does not exist for it.
  • Scope enforcement (read/write/delete/admin plus bucket/prefix scopes) is identical across surfaces. A read-only key cannot write on any of them.
  • Encryption, dedup, quota, object-lock, retention apply identically. An object written via the native API is encrypted-at-rest and quota-checked exactly like an S3 write.

Which client, per language

SurfaceNodeGoJava
S3 data planeClientlockwellsdk.ClientLockwellClient
Native data planeNativeClientlockwellnative.ClientLockwellNativeClient
Admin APIAdminClientlockwelladmin.ClientLockwellAdminClient
App kit (native + admin)LockwellKitlockwellkit.KitLockwellKit

The app kit composes the native and admin clients into a near-zero-glue multi-tenant app surface: tenant-to-key provisioning, per-tenant clients, browser-direct signed URLs, and webhook verification. It is the recommended starting point for a new multi-tenant app. See App kit.

Deliberate non-goals

Across every surface, by design: no public or anonymous access without a token or signed URL, no SSE-KMS/SSE-C, no IAM/STS/bucket-policy management API, and webhook-only event notifications (SNS/SQS/Lambda targets return 501). The S3 client additionally stays presign-GET-only. These keep the security surface small and fail-closed. See Tenancy and auth.

Released under the Apache-2.0 License. License