The three surfaces
One lockwelld server exposes one multi-tenant, encrypted object store through three interfaces. The S3 and native interfaces are object data planes with different wire protocols and auth. LNW/1 is an alternate binary transport for the native data plane, not a fourth domain surface. The Admin API is a control plane for the tenant, key, quota, and audit configuration that those data planes enforce.
S3 and native object operations, plus authenticated object workflows in the Admin Web UI, flow through the shared object coordinator and storage pipeline. The JSON Admin API changes authoritative configuration through the same domain services; it does not expose a third object-I/O route. None of the surfaces enables public or anonymous access.
| 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/ (HTTP JSON) or configured LNW/1 listener | public | bearer token (HTTP) or access-key proof (LNW), scoped to the tenant | Your app's object I/O; LNW is server-only and explicit |
| 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 object presigning The S3 client presigns GET, PUT, HEAD, and DELETE. The native client separately mints
native signed GET/PUT URLs (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);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-surface capabilities:
- Native signed URLs for GET and PUT. These use native HMAC tokens instead of the S3 client's query-SigV4 GET/PUT/HEAD/DELETE URLs. 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.
Native Wire is an explicit transport choice
LNW/1 carries the same authorized native operations in deterministic binary frames over a separate configured TLS listener (commonly 127.0.0.1:9444 or :9444). It is experimental, opt-in, and disabled by default. The server requires TLS 1.3 and hostname verification off loopback; optional/required mTLS is listener policy. The binary client negotiates capabilities, flow-control windows, stream limits, upload deadlines, and a fresh access-key proof. It does not use the /api/v1/ HTTP path and it never falls back to JSON, XML, or S3.
Use the LNW architecture guide and wire reference for the frame layout, auth/replay retry contract, error codes, Object Lock/metadata boundary, and rollback. The merged server consumers are @kelphect/sdk-native, SolidStart v2, and the [Spring Boot starter] (/sdks/java-spring-wire). Existing Go/Node/Java core pages continue to describe S3 and HTTP-native JSON until their standalone LNW transports land and pass their source-owner gates.
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 binary data plane | NativeClient | lockwellwire.Client | LockwellNativeWireClient |
| Legacy native JSON | LegacyNativeClient | 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, no IAM/STS/bucket-policy management API, and webhook-only event notifications (SNS/SQS/Lambda targets return 501). The S3 wire API does support SSE-C with a customer-provided AES-256 key on every applicable request; the native and admin surfaces, including LNW/1, do not. The first-party Go, Node, and Java S3 clients expose typed SSE-C helpers and query-SigV4 GET/PUT/HEAD/DELETE presigners. These boundaries keep unsupported behavior explicit and fail-closed. See Tenancy and auth.