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.
| Surface | Mount | Listener | Auth | Use it for |
|---|---|---|---|---|
| S3 data plane | / (S3 routes) | public | SigV4 (access key) | Drop-in for existing S3 clients and tools |
| Native data plane | /api/v1/ | public | bearer token (lwtk_), minted from an access key | Your own app's object I/O; edge; signed writes |
| Admin API | /admin/api/v1/ | admin | admin token (lwadm_) + RBAC | Provisioning 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). :::
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 onlyThe 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.
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.
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/adminplus 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
| Surface | Node | Go | Java |
|---|---|---|---|
| S3 data plane | Client | lockwellsdk.Client | LockwellClient |
| Native data plane | NativeClient | lockwellnative.Client | LockwellNativeClient |
| Admin API | AdminClient | lockwelladmin.Client | LockwellAdminClient |
| App kit (native + admin) | LockwellKit | lockwellkit.Kit | LockwellKit |
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.