# 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.

```mermaid
flowchart TB
  s3c["S3 clients and tools"] -->|SigV4| S3["S3 API"]
  app["Your app or browser"] -->|Bearer token| NAT["Native API /api/v1/ or LNW/1"]
  adm["Admin tooling"] -->|Admin token| ADM["Admin API /admin/api/v1/"]
  S3 --> PIPE
  NAT --> PIPE
  ADM --> CTRL["Control plane: tenants, keys, quota, audit"]
  CTRL --> PIPE
  PIPE["Object pipeline: scope, policy, encryption, dedup, quota, retention, audit"] --> STORE[("Encrypted object store")]
```

| 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).

::: tip S3 object presigning The S3 client presigns **GET, PUT, HEAD, and DELETE**. The native client separately mints
native signed GET/PUT URLs (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);
```

The full S3 operation matrix is the [S3 operations reference](/reference/s3-operations). 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](/sdks/go) and [Java](/sdks/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](/guide/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](/guide/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](/guide/data-operations) for the full object API and the
[Native API reference](/reference/native-api) 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](/guide/native-wire) and [wire reference](/reference/native-wire) for the frame layout,
auth/replay retry contract, error codes, Object Lock/metadata boundary, and rollback. The merged server consumers are
[`@kelphect/sdk-native`](/sdks/bun-native), Go's [`lockwellwire`](/sdks/go), and the
[Spring Boot starter](/sdks/java-spring-wire); the per-language SDK pages document each one.

## 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](/reference/admin-api) and [Tenancy and auth](/guide/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

| 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](/guide/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](/guide/tenancy-and-auth).
