objects, boolean quiet)` |
| `copyObject` | `CopyResult copyObject(srcBucket, srcKey, srcVersionId, dstBucket, dstKey, ...)` |
`PutOptions` is a fluent builder: `.contentType(v)`, `.metadata(k, v)`, `.idempotencyKey(v)`, `.serverSideEncryption()`,
`.checksum(alg)`, `.objectLock(mode, retainUntilRfc3339)`, `.legalHold(boolean)`. The `queryAndRange` map on
`getObject`/`getObjectStream` carries `range`, `partNumber`, `versionId`, and the `response-*` overrides; pass
`Map.of()` for a plain read.
`StreamingGetResult` is `AutoCloseable` and exposes the `InputStream body()` plus a `readAllBytes()` convenience; close
it (try-with-resources) to release the connection.
### Listing and paginators
| Method | Signature |
| ---------------------- | -------------------------------------------------------------------------------------------- |
| `listObjectsV2` | `ListResult listObjectsV2(bucket, String prefix, Integer maxKeys, String continuationToken)` |
| `listObjects` | `ListV1Result listObjects(bucket, prefix, marker, Integer maxKeys, delimiter)` |
| `listObjectVersions` | `ListVersionsResult listObjectVersions(bucket, ListVersionsOptions opts)` |
| `listMultipartUploads` | `ListMultipartUploadsResult listMultipartUploads(bucket, ListUploadsOptions opts)` |
| `listParts` | `ListPartsResult listParts(bucket, key, uploadId, ListPartsOptions opts)` |
The marker-paged lists each have a `Paginator` that threads markers for you. A paginator is an `Iterable
` of
pages: drive it with `hasMorePages()` / `nextPage()`, a for-each, or `toList()`.
```java
var p = client.listObjectVersionsPaginator("reports",
new LockwellClient.ListVersionsOptions().prefix("logs/"));
while (p.hasMorePages()) {
var page = p.nextPage();
page.versions().forEach(v -> System.out.println(v.key() + " " + v.versionId()));
}
```
The constructors are `listObjectVersionsPaginator`, `listMultipartUploadsPaginator`, and `listPartsPaginator`. The
`ListVersionsOptions` / `ListUploadsOptions` / `ListPartsOptions` builders carry `prefix`, `delimiter`, the relevant
markers, and the page cap.
### Multipart
`createMultipartUpload(bucket, key, contentType)` returns a `CreateMpuResult`; the checksum-aware overload
`createMultipartUpload(bucket, key, contentType, checksumAlgorithm)` returns a `CreateMpuChecksumResult`.
Then `uploadPart(...)`, `uploadPartCopy(...)`, `completeMultipartUpload(bucket, key, uploadId, parts)`, and
`abortMultipartUpload(bucket, key, uploadId)`. The checksum-aware `uploadPart` overload sends a verified per-part digest
and folds it into the composite checksum on complete.
### Tagging, Object Lock, presign
`putObjectTagging(bucket, key, Map tags)`, `getObjectTagging(bucket, key)`,
`deleteObjectTagging(bucket, key)`, typed `getTypedObjectRetention` / `putObjectRetention`, typed
`getObjectLegalHoldStatus` / `putObjectLegalHold`, and the legacy `getObjectRetention` / `getObjectLegalHold` readers.
Each Object Lock mutation accepts a version id overload. The four presign methods are `presignGetObject`,
`presignPutObject`, `presignHeadObject`, and `presignDeleteObject`.
Retention and legal hold can be set on the write through `PutOptions.objectLock(...)` and `.legalHold(...)` or changed
afterward with the typed methods, subject to Object Lock enforcement. The native client separately exposes `signUrl`
for constrained native GET/PUT URLs.
### Errors and the async client
Server errors throw `ApiException` with `code()`, `statusCode()`, `requestId()`, and `isNotFound()`. A
`LockwellAsyncClient` wraps an existing synchronous client with `CompletableFuture` results for bucket, object,
listing/paginator, copy, multipart/checksum, tagging, typed Object Lock, and all four presign operations. Closing the
wrapper only closes an executor it created; a supplied executor remains application-owned. Cancelling a future marks
that future cancelled, while interruption of an in-flight HTTP exchange depends on the supplied executor and call state.
`NativeException` and `AdminException` preserve RFC 9457 `type()`, `title()`, `detail()`, `instance()`, and
`extensions()` alongside the stable code/status/request id. Branch on the exact machine code or documented predicate,
not the human detail string.
## `LockwellNativeClient` (the native client)
The native JSON data plane at `/api/v1/`. No SigV4, no XML.
Configured with an access-key id plus secret, it auto-manages the bearer token: mints a `lwtk_…` token on first use,
caches it until shortly before expiry, refreshes transparently, and re-mints once on a 401. Token management is
thread-safe (single-flight refresh under a lock), so concurrent callers share one in-flight mint.
The Java native client retries safe/idempotent requests on transient transport errors, `429`, and `5xx` by default,
honoring `Retry-After`. `GET`, `HEAD`, and `DELETE` are replayed automatically; `PUT` and `POST` require an
`Idempotency-Key`, such as `PutOptions.idempotencyKey(...)`. Pass `retryPolicy(RetryPolicy.disabled())` to attempt each
request once.
```java
import com.lockwell.sdk.RetryPolicy;
import com.lockwell.sdk.nativeapi.*;
import com.lockwell.sdk.nativeapi.NativeTypes.*;
import java.time.Duration;
LockwellNativeClient client = LockwellNativeClient.builder()
.endpoint("https://objects.example.com") // public S3 port; native API at /api/v1
.accessKeyId(System.getenv("LOCKWELL_ACCESS_KEY_ID"))
.secretKey(System.getenv("LOCKWELL_SECRET_KEY"))
.requestTimeout(Duration.ofSeconds(30))
.retryPolicy(RetryPolicy.defaults())
.build();
client.createBucket("reports");
// An idempotent PUT needs a body-integrity signal: pass an expected checksum.
PutResult put = client.putObject("reports", "q1.txt", "hello".getBytes(),
new PutOptions().contentType("text/plain").idempotencyKey("q1-2026").checksum("sha256", sha));
// Streaming GET (InputStream body, no whole-object buffering). Caller closes it.
try (GetResult got = client.getObject("reports", "q1.txt")) {
got.body().transferTo(System.out);
}
// Streaming PUT from an InputStream supplier.
client.putObject("reports", "big.bin", () -> Files.newInputStream(path),
new PutOptions().contentType("application/octet-stream"));
```
### Buckets and objects
| Method | Signature |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `listBuckets` | `List listBuckets()` |
| `createBucket` | `createBucket(bucket)` / `createBucket(bucket, CreateBucketOptions opts)` |
| `getBucket` / `deleteBucket` | `Bucket getBucket(bucket)` / `void deleteBucket(bucket)` |
| `getBucketVersioning` / `setBucketVersioning` | `VersioningState` (`setBucketVersioning(bucket, status)`) |
| `setBucketCORS` / `getBucketCORS` / `deleteBucketCORS` | browser CORS rules |
| `putObject` | `putObject(bucket, key, byte[] body[, PutOptions])` or `putObject(bucket, key, Supplier body, PutOptions)` (streaming) |
| `getObject` | `getObject(bucket, key)` / `getObject(bucket, key, String range, String versionId)` |
| `headObject` | `headObject(bucket, key)` / `headObject(bucket, key, String versionId)` |
| `deleteObject` | `deleteObject(bucket, key)` / `deleteObject(bucket, key, String versionId)` |
| `listObjects` | `ListObjectsResult listObjects(bucket, ListObjectsOptions opts)` |
| `batchDeleteObjects` | `BatchDeleteResult batchDeleteObjects(bucket, List objects)` |
| `copyObject` | `CopyResult copyObject(destBucket, destKey, sourceBucket, sourceKey, CopyOptions opts)` |
`PutOptions` here is a native fluent builder: `.contentType(v)`, `.idempotencyKey(v)`, `.ifMatch(v)` / `.ifAbsent()`
for conditional writes, `.checksum(alg, value)`, and metadata. The streaming overload takes a
`Supplier extends InputStream>` so the body is opened lazily.
S3 streaming uploads/downloads and multipart parts have additive
`ProgressListener` overloads. `TransferProgress` reports cumulative bytes,
known/unknown total, direction, and part number; listener execution applies
backpressure and listener failure cancels/closes the transfer. Configure
`responseMetadataListener(Consumer)` on the S3, native, or
admin builder to observe successful request-id/trace headers without changing
result records. `listObjectsV2` is the explicit page API; `paginateObjectsV2`
and `iterateObjectsV2` provide lazy continuation-token iteration.
For production retry and timeout settings, see the [Java native client guide](/sdks/java-native).
### ERP security review
For fiscal and GDPR artifacts, the Java native client and `StorageProfiles` write through the same server-side object
pipeline as every Lockwell data-plane write. Production configs set `encryption.enabled = true`, so native writes are
stored as encrypted chunks under per-tenant data keys. Verify finalized objects with `HeadResult.encrypted()` or
`GetResult.encrypted()`, both derived from the `X-Lockwell-Encrypted` response header, and do not run tenant-handling
ERP data on an encryption-disabled deployment.
Key management is deliberately explicit: `encryption.key_provider = "local"` is the only accepted runtime provider
today. External KMS and customer-managed key support is deferred, so unsupported provider values fail config validation
instead of creating fake compliance evidence or hidden storage network calls. `lockwell keys rotate` affects future
per-tenant object writes; `lockwell keys rewrap ...` is the tracked workflow for historical chunks. Access-key
master-key rewrap is a separate credential-maintenance workflow.
Residency is decided by deployment placement, not by the SDK. SaaS deployments keep TangibleShift artifacts in
EU/Portugal-approved infrastructure by placing the Lockwell node, `storage.data_dir`, metadata store, backups, and key
escrow there and exposing only that endpoint to the ERP. On-prem/server and desktop-local deployments keep objects local
by running `lockwelld` against a customer-controlled `storage.data_dir` and local backup/key custody.
Keep bucket and key names non-sensitive. Object bytes are encrypted, but bucket names, object keys, object tags, and
audit resource strings remain operational metadata. A `metadata-backup` stream is decrypted by design except for
application-sealed fields such as access-key secrets, so treat it as a secret. Complete fiscal backups must include the
master key, `metadata-backup` stream, data-encryption key directory, and blob store; backup/restore evidence must prove
retention policies, legal holds, checksums, and audit trail survive restore.
### Tags, retention, legal hold, versions, multipart
`getObjectTags` / `setObjectTags`, `getObjectRetention` / `setObjectRetention(bucket, key, mode, retainUntil)`,
`getObjectLegalHold` / `setObjectLegalHold(bucket, key, status)`, `listObjectVersions(bucket, ListVersionsOptions)`, and
the multipart set (`createMultipartUpload`, `uploadPart`, `listParts`, `completeMultipartUpload`,
`abortMultipartUpload`, `listMultipartUploads`).
### Bucket CORS
Browser CORS is available on the native client as `CORSConfiguration` / `CORSRule`:
```java
import com.lockwell.sdk.nativeapi.NativeTypes.CORSConfiguration;
import com.lockwell.sdk.nativeapi.NativeTypes.CORSRule;
import java.util.List;
CORSConfiguration cfg = new CORSConfiguration(List.of(new CORSRule(
"browser-direct",
List.of("https://app.example.com"),
List.of("GET", "HEAD", "PUT"),
List.of("content-type"),
List.of("ETag"),
600)));
CORSConfiguration stored = client.setBucketCORS("reports", cfg);
CORSConfiguration got = client.getBucketCORS("reports");
client.deleteBucketCORS("reports");
```
Changing CORS is an admin-scoped bucket operation. For app onboarding, prefer `kit.configureBucketCORS(...)` or
`new ProvisionOptions().bucketCORS(...)`, which use a transient admin key and revoke it after the update.
### Signed URLs (GET and PUT)
The native API supports signed write URLs (unlike the S3 presigner). `signUrl` returns a `String` usable without a
bearer token:
```java
String download = client.signUrl("GET", "reports", "q1.txt", 900);
String upload = client.signUrl("PUT", "reports", "incoming.bin", 600);
```
### Signed-URL constraints (browser-direct upload/download)
Pass `SignedUrlOptions` to pin properties the browser cannot be trusted to send correctly. The constraints are
HMAC-covered in the signed token and enforced by the server at dispatch time. See [signed URLs](/guide/signed-urls) for
the full set and the security shape.
```java
import com.lockwell.sdk.nativeapi.NativeTypes.SignedUrlOptions;
// PUT: pin content-type, cap size, verify the body, and make a retry idempotent.
String upload = client.signUrl("PUT", "fiscal", "2026/0001.pdf", 300,
new SignedUrlOptions()
.contentType("application/pdf")
.contentLengthMax(10L * 1024 * 1024)
.checksum("SHA256", sha256B64)
.idempotencyKey("invoice-2026-0001"));
// PUT prefix-scoped: browser chooses the suffix under "imports/".
String prefixUpload = client.signUrl("PUT", "imports", null, 300,
new SignedUrlOptions().keyPrefix("imports/"));
// GET: predictable download headers.
String download = client.signUrl("GET", "fiscal", "2026/0001.pdf", 300,
new SignedUrlOptions()
.responseContentType("application/pdf")
.responseContentDisposition("attachment; filename=\"invoice-2026-0001.pdf\""));
```
For `keyPrefix`, insert the full object key under the prefix into the returned URL's path before the `?token=` query.
See [signed URLs](/guide/signed-urls#prefix-scoped-uploads).
`signUrlResult(...)` accepts the same options and returns the full `SignedUrl` record.
See [signed URLs](/guide/signed-urls).
### Bucket notifications
Webhook-only delivery; a new config ID returns its signing secret exactly once. GET and same-ID updates report only
`hasSecret()`.
```java
import java.util.List;
NotificationConfig cfg = new NotificationConfig("https://app.example.com/hook",
List.of("s3:ObjectCreated:*", "s3:ObjectRemoved:*"))
.id("reports-events")
.filter("prefix", "incoming/");
NotificationConfiguration created = client.setBucketNotification("reports", cfg);
String signingSecret = created.configs().get(0).signingSecret(); // store securely
NotificationConfiguration current = client.getBucketNotification("reports");
boolean signed = current.configs().get(0).hasSecret(); // signingSecret() is null on GET
client.deleteBucketNotification("reports"); // clear
```
`setBucketNotification` also takes a `List` overload for multiple targets.
### Errors
Native errors throw `NativeException` with `code()`, `statusCode()`, `requestId()` and the predicates `isUnauthorized()`
(401), `isForbidden()` (403), `isNotFound()` (404), `isConflict()` (409), `isPreconditionFailed()` (412),
`isQuotaExceeded()` (507).
### ERP error taxonomy and retry classification
Use `ErpErrors.classify(Throwable)` when mapping Lockwell failures to TangibleShift RFC 9457 problem details. It accepts
`AdminException` and `NativeException` and returns a stable `Classification` with `category`, `statusCode`, `code`,
`requestId`, `problemType`, `auditReason`, `retryDecision`, and write-retry proof flags. It intentionally does not copy
the raw exception message, so secrets, signed URLs, object keys, tenant names, and customer names do not leak into ERP
problem bodies.
The classifier keys on exact Lockwell JSON problem codes first. ERP-specific codes include `tenant_disabled`,
`key_revoked`, `key_expired`, `quota_exceeded`, `retention_blocked`, `legal_hold_blocked`, `idempotency_conflict`, and
`idempotency_in_progress`; status-only fallback is used only when a non-Lockwell response has no machine code.
```java
import com.lockwell.sdk.kit.ErpErrors;
import com.lockwell.sdk.kit.ErpErrors.Category;
try {
nativeClient.putObject("imports", key, body, opts);
} catch (RuntimeException e) {
var c = ErpErrors.classify(e);
if (c.category() == Category.RATE_LIMITED || c.category() == Category.TRANSIENT_UPSTREAM) {
// Retry writes only when the request has an idempotency key and checksum.
if (c.writeRetryRequiresIdempotencyKey()) scheduleReplayWithProof(c.requestId());
}
throw toProblemDetail(c.problemType(), c.statusCode(), c.code(), c.requestId());
}
```
* `NOT_FOUND`, `ALREADY_EXISTS`, `VALIDATION_ERROR`, `FORBIDDEN`: do not retry blindly; fix ERP state, scope, or request
shape.
* `UNAUTHORIZED`, `KEY_EXPIRED`: refresh the admin token or native bearer/credential path, then retry once.
* `KEY_REVOKED`, `TENANT_DISABLED`: operator action required; rotate/select a new purpose key or stop tenant work.
* `QUOTA_EXCEEDED`: operator action required; raise quota or stop the import/export job.
* `RATE_LIMITED`, `TRANSIENT_UPSTREAM`, `IDEMPOTENCY_IN_PROGRESS`: back off; retry writes only with an idempotency key
and body-binding checksum.
* `RETENTION_BLOCKED`, `LEGAL_HOLD_BLOCKED`: do not retry; surface fiscal/legal evidence and keep the object.
* `PRECONDITION_FAILED`, `IDEMPOTENCY_CONFLICT`: do not retry with changed bytes; reconcile the ERP row and object
version.
For multi-GB SAF-T/import/export artifacts, prefer streaming APIs, set `contentLength` when known so quota is checked up
front, and choose multipart for resumable large writes. Any automated replay of a write must carry the same idempotency
key plus a checksum over the same bytes.
## `LockwellAdminClient` (the admin client)
Targets `/admin/api/v1/` on the admin listener (never the public S3 port). It authenticates by an admin API bearer token
(`lockwell admin-token create`).
```java
import com.lockwell.sdk.admin.*;
import com.lockwell.sdk.admin.AdminTypes.*;
LockwellAdminClient admin = LockwellAdminClient.builder()
.endpoint("https://admin.example.com") // admin listener, NOT the S3 port
.token(System.getenv("LOCKWELL_ADMIN_TOKEN")) // Authorization: Bearer
.build();
for (Tenant t : admin.listTenants()) System.out.println(t.id());
Tenant acme = admin.createTenant("acme", "Acme Inc");
// Every mutation has a *DryRun twin that sends ?dryRun=true and returns the plan.
DryRunPlan plan = admin.deleteTenantDryRun("acme", "offboarding", "acme");
// The secret is returned exactly once on create/rotate. Store it immediately.
NewKey key = admin.createKey("acme",
new CreateKeyOptions("sa-1", "read,write,delete", null,
"ERP bootstrap", "ts-install-123:reports"));
System.out.println(key.secretKey());
```
| Method | Dry-run twin |
| --------------------------------------------------------- | ----------------------------------------- |
| `listTenants()` / `getTenant(id)` | (read-only) |
| `createTenant(id, name)` | `createTenantDryRun(id, name)` |
| `disableTenant(id, reason)` | `disableTenantDryRun(id, reason)` |
| `deleteTenant(id, reason, confirm)` | `deleteTenantDryRun(id, reason, confirm)` |
| `getQuota(id)` / `setQuota(id, bytes)` / `clearQuota(id)` | `setQuotaDryRun`, `clearQuotaDryRun` |
| `getUsage(id)` | (read-only) |
| `listAccounts(id)` / `createAccount(id, name)` | `createAccountDryRun(id, name)` |
| `listKeys(id)` (never returns secrets) | (read-only) |
| `createKey(id, CreateKeyOptions)` | `createKeyDryRun(id, opts)` |
| `rotateKey(id, keyId, RotateKeyOptions)` | `rotateKeyDryRun(id, keyId, opts)` |
| `revokeKey(id, keyId, reason)` | `revokeKeyDryRun(id, keyId, reason)` |
| `queryAudit(AuditQuery q)` | (read-only) |
`CreateKeyOptions(accountId, scopes, expiresAt[, reason[, externalRef]])` follows the scope grammar (verb list
`read,write,delete,admin`, or resource form `op=read:bucket=reports:prefix=in/,op=write:bucket=reports:prefix=in/`).
`accountId` is a real service-account id; use `externalRef` for caller-owned ERP installation/purpose metadata. The
secret on a created or rotated key is on `NewKey.secretKey()` and is shown exactly once.
Errors throw `AdminException` with `isUnauthorized()`, `isForbidden()`, `isNotFound()`, `isRetentionBlocked()` (the 412
retention/legal-hold gate). See the [Admin API reference](/reference/admin-api).
::: warning `NewKey.secretKey()` is readable exactly once, on create or rotate. Persist it immediately; it is never
recoverable afterward. :::
## `LockwellKit` (the app kit)
Composes the admin and native clients with near-zero glue. The per-tenant native-client cache is a `ConcurrentHashMap`,
so it is thread-safe.
```java
import com.lockwell.sdk.kit.*;
import com.lockwell.sdk.kit.KitTypes.*;
import com.lockwell.sdk.nativeapi.LockwellNativeClient;
import java.time.Duration;
LockwellKit kit = LockwellKit.builder()
.adminEndpoint("https://admin.example.com") // admin listener
.adminToken(System.getenv("LOCKWELL_ADMIN_TOKEN"))
.nativeEndpoint("https://objects.example.com") // public S3 port; native API at /api/v1
.build();
// Provision: ensure the tenant exists, mint a fresh read/write/delete key
// (optionally bucket-scoped), optionally create a default bucket. Secret returned ONCE.
ProvisionResult p = kit.provisionTenant("acme",
new LockwellKit.ProvisionOptions().defaultBucket("uploads")
.bucketCORS(new CORSConfiguration(List.of(new CORSRule(
List.of("https://app.example.com"), List.of("GET", "HEAD", "PUT"))))));
store(p.credentials()); // {accessKeyId, secretKey}
// ERP-safe bootstrap retry: keyExternalRef is the stable external reference.
EnsureProvisionResult ensured = kit.ensureTenantProvisioning("acme",
new LockwellKit.ProvisionOptions()
.keyExternalRef("ts-install-123:uploads")
.bucketScope("uploads")
.reason("ERP bootstrap"));
if (ensured.key().created()) {
store(ensured.key().credentials()); // secret shown ONCE
} else {
rememberAccessKeyId(ensured.key().key().accessKeyId());
}
// A per-tenant native client (cached per (tenant, creds); auto-manages the bearer token).
LockwellNativeClient acme = kit.clientForTenant("acme", p.credentials());
acme.putObject("uploads", "hello.txt", "hi".getBytes());
// Browser direct upload/download. Signed URLs the browser uses with NO bearer token.
BrowserSignedUrl up = kit.signedUploadUrl(p.credentials(), "uploads", "in.bin",
Duration.ofMinutes(10), "application/octet-stream");
BrowserSignedUrl dl = kit.signedDownloadUrl(p.credentials(), "uploads", "hello.txt",
Duration.ofMinutes(15));
// Or update CORS later with another transient admin-scoped key.
kit.configureBucketCORS("acme", "uploads", new CORSConfiguration(List.of(
new CORSRule(List.of("https://app.example.com"), List.of("GET", "HEAD", "PUT")))));
// Verify an incoming webhook (constant-time HMAC-SHA256).
boolean ok = LockwellKit.verifyWebhook(requestBodyBytes,
request.getHeader("X-Lockwell-Signature"), mySecret);
```
`ProvisionOptions` is a fluent builder: `.tenantName(name)`, `.bucketScope(bucket)`, `.defaultBucket(bucket)`,
`.bucketCORS(cfg)`, `.keyExternalRef(externalRef)`, `.keyExpiresAt(rfc3339)`, `.reason(reason)`. The default bucket and
optional CORS rules are applied with a transient admin-on-that-bucket key that is revoked immediately, so the long-lived
tenant key stays admin-free.
Use `provisionTenant` when the app intentionally wants a fresh one-time secret every call. Use
`ensureTenantProvisioning` for idempotent ERP/on-prem bootstrap: it requires `.keyExternalRef(...)`, lists active key
metadata, and reuses a matching `externalRef + scopes + expiry` key without creating another live key. Existing key
metadata never carries the secret. If the ERP lost the one-time secret before storing it, rotate the returned key
through `kit.admin().rotateKey(...)` with a `RotateKeyOptions` audit reason and persist the new secret from that
rotation response; omitted rotate scopes/expiry preserve the existing key's values. `.reason(...)` on
`ProvisionOptions`, `CreateKeyOptions`, and `RotateKeyOptions` is written into Lockwell's audit row; the Admin API also
emits `X-Request-Id` and stores it as audit correlation. Concurrent first bootstrap attempts can still both observe "no
key yet" before either create reaches the server; serialize that first call with the ERP's DB lock/outbox until Lockwell
exposes a server-side idempotency-key primitive for key create.
### ERP transaction, outbox, and reconciliation
Lockwell calls are not part of the ERP database transaction. During the ERP tenant/company transaction, only derive the
opaque refs (`tenantPublicRef`, `installationRef`, `lockwellTenantId`, company ref, purpose) and persist an ERP outbox
record. Do not call `ensureTenantProvisioning`, create buckets, or mint keys until the ERP row has committed.
The outbox worker is the only place that talks to Lockwell for onboarding. It should run these steps with one ERP
database lock per tenant/company: call `ensureTenantProvisioning` with a stable `keyExternalRef`, ensure the default
bucket/profile, create purpose keys with `ErpScopes`, store each access-key id, and store one-time secrets only on the
nested key result's `created=true`/credentials-present path. Retries are safe when every key uses stable
`externalRef + scopes + expiry`: `ensureTenant` accepts an existing tenant, `ensureBucket` accepts an existing bucket,
and `ensureKey` reuses matching active keys instead of duplicating live credentials. If the ERP lost a one-time secret,
rotate the returned key with an audited reason and store the replacement secret.
Reconciliation is ERP-owned. Keep mapping rows in states such as `pending_lockwell`, `active`, `failed_lockwell`,
`disabled`, and `delete_ready`; compare them with `listTenants`, `getTenant`, and `listKeys` output. A committed ERP row
with missing Lockwell resources goes back through the outbox. A Lockwell tenant with no active ERP mapping is an orphan:
first disable it with an audited reason, revoke purpose keys, and only then decide whether it can be deleted. Use
`deleteTenantDryRun` to preview affected buckets, keys, versions, retention, legal holds, and bytes before destructive
cleanup.
Offboarding is a sequence, not a helper. Disabling the ERP tenant, disabling the Lockwell tenant, revoking purpose keys,
retaining fiscal artifacts, deleting non-retained data, and final tenant deletion are separate audited steps. Never
delete a Lockwell tenant just because the ERP row was removed; fiscal retention and legal holds can outlive the ERP
account.
Topology is explicit. In SaaS, TangibleShift operates the regional Lockwell deployment and the ERP backend owns the
admin token; browsers and customer code never see it. In customer on-prem server installs, Lockwell runs beside the ERP
server as an external daemon owned by the customer/operator, with a scoped provisioning token stored in the ERP secret
store. In desktop-local installs, the installer may bootstrap a local `lockwelld`, but retention-grade evidence requires
documented backups, key custody, and clock monitoring before fiscal data is stored there.
Retention deadlines are enforced by the Lockwell server clock at write/delete time. The ERP may compute the policy date,
but on-prem and desktop deployments must monitor NTP/time drift and record the clock source used for fiscal evidence. If
clock drift is detected, pause retention-sensitive onboarding, offboarding, and delete workflows until the operator has
reconciled the time source.
### ERP tenant/company/purpose layout
`ErpScopes` keeps the ERP-side mapping table explicit. TangibleShift stores opaque `tenantPublicRef`, `installationRef`,
and `lockwellTenantId` values, then the helper derives purpose paths and key metadata. Bucket names and prefixes must
not contain customer legal names, tax ids, or free-form site labels.
```java
import com.lockwell.sdk.kit.ErpScopes;
import com.lockwell.sdk.kit.ErpScopes.Purpose;
var mapping = ErpScopes.tenantMapping("ts_tenant_7f3a", "install_9b12", "lwtenant_9");
var imports = ErpScopes.purposePath(mapping, "co_a812", Purpose.IMPORTS);
System.out.println(imports.bucket()); // imports
System.out.println(imports.prefix()); // companies/co_a812/imports/
System.out.println(imports.externalRef()); // ts:install_9b12:ts_tenant_7f3a:co_a812:imports
kit.ensureKey(mapping.lockwellTenantId(),
ErpScopes.temporaryBrowserUploadKey(imports, "2026-01-02T03:04:05Z", "ERP import upload"));
```
All company object keys should start with `companies///`. The purpose-scoped access-key
templates cover `fiscalArchiveAppendKey`, `importReadWriteKey`, `temporaryBrowserUploadKey`, `exportReadKey`,
`dataRightsReadWriteKey`, and `supportDiagnosticReadKey`. Store the access key id per company/purpose so ERP support can
rotate/revoke by purpose, restating the same generated scope and audit reason. After rotation, persist the new secret
and drop the ERP credential cache; `clientForTenant` sees new credentials as a fresh cache key, so cache invalidation
creates a new native token manager. Tests should assert `path.prefix()` on every write/read path to catch cross-company
mistakes.
`configureBucketCORS` updates an existing bucket the same way: mint a transient admin-scoped key, call the native CORS
route, revoke the key. `signedUploadUrl` and `signedDownloadUrl` accept either `TenantCredentials` or a
`LockwellNativeClient`. For browser-direct flows with constraints (pinned content-type, size cap, checksum, idempotency
key, prefix scope, or GET response overrides), pass a `SignedUrlOptions`:
```java
import com.lockwell.sdk.nativeapi.NativeTypes.SignedUrlOptions;
BrowserSignedUrl up = kit.signedUploadUrl(creds, "inbox", "photo.jpg",
Duration.ofMinutes(5),
new SignedUrlOptions().contentType("image/jpeg").contentLengthMax(5L * 1024 * 1024));
```
### ERP storage profiles
The Java kit includes `StorageProfiles` for ERP-owned artifact classes. These helpers encode safe Lockwell option
combinations; they do **not** decide TangibleShift's fiscal retention durations, GDPR outcomes, or support retention
policy.
```java
import com.lockwell.sdk.kit.StorageProfiles;
client.createBucket("fiscal-archive", StorageProfiles.fiscalArchiveBucket());
var fiscal = StorageProfiles.fiscalArchiveWrite(
"application/pdf",
"2033-01-01T00:00:00Z", // ERP-owned retain-until date
"SHA256",
pdfSha256Base64,
"invoice-2026-0001");
client.putObject("fiscal-archive", "2026/0001.pdf", pdfBytes, fiscal.putOptions());
client.setObjectRetention("fiscal-archive", "2026/0001.pdf",
fiscal.retention().mode(), fiscal.retention().retainUntil()); // COMPLIANCE
var importUpload = StorageProfiles.importUploadUrl(Duration.ofMinutes(5),
"application/json", 2L * 1024 * 1024, "SHA256", importSha256Base64,
"import-job-123", "imports/");
BrowserSignedUrl up = kit.signedUploadUrl(creds, "imports", null,
importUpload.ttl(), importUpload.options());
var exportDownload = StorageProfiles.exportDownloadUrl(Duration.ofMinutes(5),
"application/pdf", "attachment; filename=\"invoice-2026-0001.pdf\"",
"ERP export invoice-2026-0001");
BrowserSignedUrl dl = kit.signedDownloadUrl(creds, "exports", "2026/0001.pdf",
exportDownload.ttl(), exportDownload.options());
```
Apply retention (and any legal hold) before acknowledging a fiscal archive as finalized. The native API applies
retention after the object exists, so ERP startup/reconciliation should use the ERP archive ledger to find finalized
object keys whose retention was not applied after a crash and repair them before deletion is possible.
Named profiles:
* `fiscal-archive`: versioning + Object Lock bucket, no-overwrite write options, checksum verification, idempotency, and
explicit `COMPLIANCE` retention helpers for finalized PDFs and SAF-T exports.
* `imports`: server-side writes and short-lived browser PUT URLs with prefix scope, size cap, checksum, and idempotency.
* `exports`: signed GET URLs with response content headers and an HMAC-covered audit reason.
* `data-rights`: export artifact writes/downloads where the ERP owns expiry and retention-vs-erasure conflict handling.
* `support-bundles`: `redactedSupportBundleWrite`; redact before upload, because the SDK does not inspect payload bytes.
Governance mode is not the fiscal recipe. Lockwell's native and S3 object-lock tests prove active retention cannot be
shortened or deleted before the retain-until date, and governance bypass headers are rejected.
Signed-URL audit reasons are signed but not encrypted in the URL token; use stable job/document references, not secrets,
raw personal data, or sensitive free text.
For ERP deployments where the backend uses an internal service URL but the browser reaches a different public origin,
set `signedUrlPublicOrigin` on the kit builder; set `signedUrlMaxTtl` for a client-side TTL cap. Both are optional. See
[signed URLs](/guide/signed-urls#separate-internal-and-public-origins).
Reach the underlying admin client via `kit.admin()` for operations the kit does not wrap. See
[the app kit guide](/guide/app-kit).
### ERP live conformance
The production test harness includes a Java ERP live conformance runner that composes `LockwellKit`,
`LockwellAdminClient`, and `LockwellNativeClient` against the packaged Lockwell image. It covers the minimum
TangibleShift cutover flow: idempotent provisioning, object-lock fiscal writes, signed browser GET/PUT URLs, large
streaming artifacts, typed quota and credential errors, tenant-disable denial, and audit request-id correlation.
Backup and restore remain operational CLI drills (`backup-plan`, `backup-verify`, restore dry-run/verify) documented in
`docs/backup-restore.md`, not Java SDK helper APIs.
## Spring Boot starter
The `com.lockwell.sdk.spring` package auto-configures a `LockwellClient` and a `LockwellAsyncClient` bean when the
`lockwell.*` properties are present. Spring is an optional dependency, so non-Spring consumers never pull it in and the
core SDK keeps its zero-runtime-dependency profile.
```yaml
# application.yml
lockwell:
endpoint: https://objects.example.com
access-key-id: ${LOCKWELL_ACCESS_KEY_ID}
secret-key: ${LOCKWELL_SECRET_KEY}
# user-agent: my-service/1.0 # optional override
```
```java
@Service
public class ReportService {
private final LockwellClient lockwell;
public ReportService(LockwellClient lockwell) { this.lockwell = lockwell; }
// ...
}
```
The supported properties are `endpoint`, `access-key-id`, `secret-key`, and the optional `user-agent`. Both beans are
`@ConditionalOnMissingBean`, so an application-defined client always wins.
The S3-style starter above is distinct from the opt-in LNW/1 starter. See \[Spring Boot Native Wire]
(/sdks/java-spring-wire) for `lockwell.native-wire.*` properties, JDK 25/Spring Boot 4.1.1 compatibility, and the
server-only binary streaming contract. LNW does not change this core client's Java 21 bytecode or HTTP JSON behavior.
## Health and readiness
`LockwellNativeClient` and `LockwellAdminClient` expose `healthz()`, `healthzAsync()`, `readyz()`, and `readyzAsync()`.
They return typed `HealthResult` components and do not send credentials to probe endpoints. These are operational
probes, not substitutes for an authenticated request. See [operations and observability](/guide/operations-and-observability).
## Not supported (by design)
No public-bucket or anonymous access without a token or signed URL, no SSE-KMS, no IAM/STS/bucket-policy, no
website/tiering/Select/Object-Lambda, and webhook is the only notification target (SNS/SQS/Lambda are 501 server-side).
The Lockwell server and this first-party client support SSE-C and copy-source SSE-C with raw 32-byte customer-key
options on object, copy, and multipart operations. The S3 `LockwellClient` exposes presigned GET/PUT/HEAD/DELETE object
operations.
---
---
url: /sdks/java-native.md
description: >-
Production notes for the Java LockwellNativeClient, including bearer-token
management, safe native retries, Retry-After handling, request timeouts, and
TangibleShift ERP-style integration defaults.
---
# Java HTTP-native compatibility client
Use `LockwellNativeClient` when a Java or Spring service needs the versioned HTTP/JSON compatibility data plane at
`/api/v1/`. It is intentionally distinct from the primary binary LNW/1 client in
`com.lockwell.sdk.springwire.LockwellNativeWireClient`; it does not silently switch transports. Both surfaces use the
same tenant isolation, encryption, quota, retention, audit, and scope checks as the S3 surface.
This page is the production checklist for JVM services such as TangibleShift ERP. The full method reference remains on
the [Java SDK page](/sdks/java).
## Install
```xml
com.lockwell
lockwell-sdk
0.2.2
```
The package targets JDK 25. For the binary transport and Spring Boot 4.1 autoconfiguration, use the
`com.lockwell:lockwell-spring-boot-starter` dependency and see the [Java SDK wire section](/sdks/java#lnw1-native-wire).
## Configure the client
```java
import com.lockwell.sdk.RetryPolicy;
import com.lockwell.sdk.nativeapi.LockwellNativeClient;
import java.net.http.HttpClient;
import java.time.Duration;
LockwellNativeClient nativeClient = LockwellNativeClient.builder()
.endpoint("https://objects.example.com") // public listener; /api/v1 is added by the SDK
.accessKeyId(System.getenv("LOCKWELL_ACCESS_KEY_ID"))
.secretKey(System.getenv("LOCKWELL_SECRET_KEY"))
.httpClient(HttpClient.newHttpClient())
.requestTimeout(Duration.ofSeconds(30))
.retryPolicy(RetryPolicy.defaults())
.build();
```
`endpoint` is the public object listener, not the admin listener. The client appends `/api/v1` itself, so pass the base
origin such as `https://objects.example.com`.
## Token lifecycle
The native client accepts the same access-key id and secret used by the S3 clients. It mints a short-lived bearer token
with `POST /api/v1/auth/token`, caches it until shortly before expiry, and refreshes it under a single-flight lock so
concurrent callers share one in-flight mint. If a request receives `401`, the client forces one token re-mint and
replays that request once inside the current attempt.
This means application code should not cache bearer tokens separately. Store the access-key id and secret in the service
secret store, build one shared client per tenant credential set, and let the SDK handle token refresh.
## Native retries
The Java native client uses `RetryPolicy.defaults()` by default. It retries transient failures only when the request can
be replayed safely:
| Request shape | Retried by default? | Why |
| ------------------------- | ------------------- | ------------------------------------------------- |
| `GET`, `HEAD`, `DELETE` | Yes | Idempotent HTTP methods. |
| `PUT` and `POST` writes | Only with a key | Requires an `Idempotency-Key` header. |
| Streaming upload supplier | Only when keyed | The supplier must be able to open a fresh stream. |
| Other methods | No | Not known to be replay-safe. |
Transient failures are transport errors, `429`, and `5xx` responses. A server `Retry-After` header is honored when it is
present as delta-seconds or an HTTP date. The SDK never lets `Retry-After` shorten the local backoff, and it caps the
server-requested wait so one bad peer cannot hold the caller forever.
Turn retries off when a caller owns the retry loop:
```java
LockwellNativeClient oneAttempt = LockwellNativeClient.builder()
.endpoint(endpoint)
.accessKeyId(accessKeyId)
.secretKey(secretKey)
.retryPolicy(RetryPolicy.disabled())
.build();
```
## Write idempotency
For `putObject`, pair the idempotency key with a checksum. The key makes a retry replay-safe, and the checksum lets the
server prove the replayed body is the same payload before it collapses the duplicate.
```java
import com.lockwell.sdk.nativeapi.NativeTypes.PutOptions;
import java.security.MessageDigest;
import java.util.Base64;
byte[] body = invoiceJson.getBytes(java.nio.charset.StandardCharsets.UTF_8);
String sha256 = Base64.getEncoder().encodeToString(
MessageDigest.getInstance("SHA-256").digest(body));
nativeClient.putObject("erp-documents", "invoices/2026-0001.json", body,
new PutOptions()
.contentType("application/json")
.idempotencyKey("invoice-2026-0001")
.checksum("sha256", sha256));
```
For `Supplier` uploads, the supplier must be repeatable. A supplier that opens a file path is replayable; a
supplier that returns an already-consumed stream is not.
## Request timeouts
`requestTimeout(Duration)` sets the JDK `HttpRequest.timeout` for each HTTP attempt. If retries are enabled, total wall
time can include more than one request timeout plus retry backoff. For large uploads, size the timeout for the full
upload attempt. For streaming downloads, the timeout covers the request until the response arrives; the caller owns the
pace of reading and closing the returned stream.
Use a shared `HttpClient` if your service has strict TLS, proxy, or pooling requirements:
```java
HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
LockwellNativeClient nativeClient = LockwellNativeClient.builder()
.endpoint(endpoint)
.accessKeyId(accessKeyId)
.secretKey(secretKey)
.httpClient(http)
.requestTimeout(Duration.ofSeconds(30))
.build();
```
Streaming native PUT, GET, and multipart part overloads accept a
`ProgressListener`. `TransferProgress` reports cumulative bytes, known or
unknown totals, direction, and part number; listener execution applies
backpressure and listener failure cancels the source/response. The native
builder also accepts `responseMetadataListener` for successful request-id and
trace headers. One-shot streams are not transparently resumed; use range/ETag
or multipart list/abort plus application-owned atomic files when recovery is
needed.
## ERP defaults
For systems that write business documents, exports, invoices, attachments, or restore artifacts:
* Use the native client for new Lockwell integrations and the S3 client only for S3-porting work.
* Keep one shared client per tenant credential set instead of rebuilding a client per request.
* Set `requestTimeout` explicitly.
* Keep the native retry default unless the service has its own bounded retry framework.
* Add an idempotency key and checksum to every write that might be retried.
* Use `ifAbsent()` for create-only writes and `ifMatch(etag)` for optimistic updates.
* Close every `GetResult` in a try-with-resources block.
## Signed-URL constraints for browser-direct flows
When the ERP mints a signed URL for a browser upload or download, pin the properties the browser cannot be trusted to
send correctly. Pass `SignedUrlOptions` to `signUrl` / `signUrlResult` (or to the kit's `signedUploadUrl` /
`signedDownloadUrl`): `contentType` (pins the stored type), `contentLengthMax` (rejects oversize), `checksum` (verifies
the body), `idempotencyKey` (collapses retries), `keyPrefix` (PUT-only prefix scope), and the GET `responseContentType`
/ `responseContentDisposition` overrides. These are HMAC-covered and enforced by the server at dispatch time. See
[signed URLs](/guide/signed-urls) for the full set.
For ERP deployments with separate internal and browser-reachable origins, set `signedUrlPublicOrigin` on the app kit
builder; set `signedUrlMaxTtl` for a client-side TTL cap.
## ERP live conformance
The production test harness now includes a Java ERP live conformance runner. It composes the Java app kit, admin client,
and native client against the packaged Lockwell image to prove the minimum cutover flow: idempotent tenant provisioning,
object-lock fiscal writes, signed browser GET/PUT URLs, large streaming artifacts, typed denial/error mapping,
rotate/revoke and tenant-disable denial, and audit request-id correlation.
Backup and restore remain operational CLI drills (`backup-plan`, `backup-verify`, restore dry-run/verify) documented in
`docs/backup-restore.md`, not Java native SDK helpers.
## Related pages
* [Java SDK](/sdks/java) for the complete package reference.
* [Errors and retries](/guide/errors-and-retries) for cross-language retry behavior.
* [Conditional writes and idempotency](/guide/conditional-writes) for write safety patterns.
* [Upload and download](/guide/data-operations) for object I/O examples.
---
---
url: /sdks/bun-native.md
description: Server-only @kelphect/sdk-native LNW/1 client for Node 22+ and Bun 1.4+.
---
# `@kelphect/sdk-native` (Node and Bun)
`@kelphect/sdk-native` 0.1.0 is the shared server-only TypeScript client for Lockwell Native Wire v1. It uses raw TLS
1.3, deterministic binary frames, bounded multiplexing, and the credential proof defined by the \[LNW reference]
(/reference/native-wire). It never sends HTTP, JSON, XML, or S3 on this transport and never silently falls back.
The package is source-shipped and test-qualified in this repository. Publication to a customer registry is a separate
release decision; pin the exact package version and server commit together in an application lockfile.
## Runtime and exports
| Runtime | Minimum | Entry |
| --- | --- | --- |
| Node | 22.0.0 | `@kelphect/sdk-native/node` or the root `node` condition |
| Bun | 1.4.0 | `@kelphect/sdk-native/bun` or the root `bun` condition |
| Browser / default bundler condition | unsupported | throws a credential-free denial |
`/protocol` exposes append-only constants and registries. `/testing` exposes the codec and injectable socket boundary for
conformance tests. Keep all client construction, credentials, and imports in server-only modules.
## Install and connect
```sh
npm install @kelphect/sdk-native@0.1.0
# or: bun add @kelphect/sdk-native@0.1.0
```
```ts
import { createLockwellClient } from "@kelphect/sdk-native";
const lockwell = createLockwellClient({
host: process.env.LOCKWELL_NATIVE_HOST ?? "lockwell.internal",
port: Number(process.env.LOCKWELL_NATIVE_PORT ?? "9444"),
tls: {
// The issuing CA is public configuration, not a private key.
ca: process.env.LOCKWELL_NATIVE_CA,
},
credentials: async () => ({
accessKeyId: process.env.LOCKWELL_ACCESS_KEY_ID!,
secretKey: process.env.LOCKWELL_SECRET_KEY!,
}),
pool: { min: 1, max: 4, acquireTimeoutMs: 5_000, idleTimeoutMs: 30_000 },
});
await lockwell.connect({ timeoutMs: 5_000 });
const result = await lockwell.putObject({
bucket: "private",
key: "a.bin",
body: new Uint8Array([1, 2, 3]),
contentLength: 3,
options: { idempotencyKey: crypto.randomUUID(), timeoutMs: 10_000 },
});
console.log(result.etag);
await lockwell.close();
```
Use `tls.ca` for a private PKI. Hostname verification and TLS 1.3 cannot be disabled. mTLS requires `tls.cert` and
`tls.key` together. Credential providers run for every new connection, allowing rotation without rebuilding the client.
## Operations and streaming
The typed client covers capabilities/readiness; bucket CRUD and versioning; object PUT/GET/HEAD/DELETE, ranges,
conditions, ordered metadata, checksums, list/pagination and copy; versions/delete markers; multipart create, upload,
list, completion/resume discovery, abort and upload listing; tags; retention and legal hold; batch delete; bucket CORS
and webhook notifications; and signed GET/PUT capabilities. SSE-C and ADMIN are not advertised or implemented on this
access-key wire.
`getObject` returns a `ReadableStream` and a completion promise. Consume the body to release a stream back to
the pool. `putObject` and `uploadPart` accept `Uint8Array`, `ArrayBuffer`, `Blob`, `ReadableStream`, or an async
iterable; provide the exact `contentLength`. Progress callbacks report transferred bytes. One-shot streams are never
replayed implicitly.
```ts
const download = await lockwell.getObject({
bucket: "private",
key: "a.bin",
range: { start: 0, endInclusive: 1023 },
onProgress: (transferred, total) => console.log({ transferred, total }),
});
for await (const chunk of download.body) consume(chunk);
await download.completed;
```
`RequestOptions` supports `signal`, an absolute `deadline` or `timeoutMs`, an idempotency key, and a validated
`traceparent`. Diagnostics expose state, open/idle connections, active streams, and negotiated capabilities without
returning credentials or TLS material.
## Authentication, errors, and retries
Every connection performs HELLO/WELCOME and an access-key HMAC proof with a fresh timestamp and nonce. Duplicate
`AUTH_REPLAY` is terminal. A bounded replay-capacity `RATE_LIMITED` response may be retried only by discarding the
connection, resolving credentials again, and generating a fresh proof before the caller deadline. Application retries are
limited to read-safe operations or writes with an idempotency key and replayable body; `AUTH_REPLAY`, scope denials,
malformed hints, and streaming bodies are not retried. `AbortSignal` cancellation and deadlines stop the active stream.
The client reports stable `LockwellError` subclasses (`ProtocolError`, `TransportError`, `AuthenticationError`,
`AuthorizationError`, `ServiceError`, `CancelledError`, `DeadlineExceededError`, and `ClientClosedError`). Error text is
bounded and redacted; log sinks receive no secret, token, payload, or filesystem path.
## Framework and browser boundaries
Framework adapters must own the lifecycle at a server/Nitro boundary. Use [SolidStart](/sdks/solidstart) for the tested
SolidStart v2 integration. The browser condition and the package default export intentionally throw, and the package
contains no browser socket or credential code. Do not import it from client components, static builds, Cloudflare
Workers, or an unknown serverless target.
## Verification
The source README, unit tests, byte fixtures, malformed-frame tests, packed consumer, and live Go TLS tests are the
authority. Re-run the package checks from `sdk/typescript-native` before changing a capability claim:
```sh
bun run typecheck
bun test --timeout 15000
bun run build
bun run pack:check
```
The [capability index](/reference/sdk-capabilities) records which framework and base-SDK adapters are merged. Go's
standalone LNW client, Node's primary-LNW transport, Java shared-core LNW client, and Nuxt remain pending source-owner
PRs and are not claimed
by this page. The merged Next.js adapter is documented separately in the [Next.js guide](/sdks/nextjs).
---
---
url: /sdks/solidstart.md
description: >-
SolidStart v2 server integration for Lockwell LNW/1 with Node, Bun, and
supported Nitro presets.
---
# `@kelphect/sdk-solidstart`
`@kelphect/sdk-solidstart` 0.1.0 is the server-only SolidStart v2 integration for the shared
[`@kelphect/sdk-native`](/sdks/bun-native) LNW/1 client. It adds Vite/runtime guards, request and response streaming,
server-function helpers, lifecycle ownership, and redacted diagnostics. It does not encode frames, open sockets, or
provide a JSON/S3 fallback.
## Support matrix
| Build or deployment | Status |
| --- | --- |
| Built server on Node 22+ | supported |
| Built server on Bun 1.4+ | supported |
| Nitro `node_server` / `node_cluster` | supported |
| Nitro `bun` | supported |
| SolidStart v2 build/dev toolchain | Node 24+ currently required |
| Cloudflare, Netlify, edge, static, browser, unknown Nitro preset | refused before application modules load |
The package is source-shipped and test-qualified; pin `@kelphect/sdk-native` 0.1.0 alongside the adapter and verify the
server commit. Edge targets remain refused until an authenticated binary streaming transport is qualified there.
## Install and guard the build
```sh
npm install @kelphect/sdk-solidstart@0.1.0 @kelphect/sdk-native@0.1.0
```
Only the root package is safe in universal Vite configuration. Put the guard next to SolidStart and Nitro, and make the
declared preset match the resolved preset:
```ts
import { solidStart } from "@solidjs/start/config";
import { nitro } from "nitro/vite";
import { defineConfig } from "vite";
import { lockwellSolidStart } from "@kelphect/sdk-solidstart";
export default defineConfig({
plugins: [
solidStart(),
lockwellSolidStart({
solidStartVersion: "2.0.4",
deployment: { target: "nitro", preset: "node_server" },
}),
nitro(),
],
nitro: { preset: "node_server" },
});
```
The guard rejects provider edge plugins, static builds, mismatched Nitro declarations, and unsupported runtime floors
with `LOCKWELL_SOLIDSTART_UNSUPPORTED_RUNTIME`. There is no catch-and-fallback path.
## Server-only client and routes
Import runtime helpers from `@kelphect/sdk-solidstart/server` (or `/node` and `/bun`) in `*.server.ts` modules. That
entry carries SolidStart's `server-only` marker and resolves to a throwing denial module under the browser condition.
```ts
import env from "env:server/runtime";
import { createNodeLockwellSolidStartClient } from "@kelphect/sdk-solidstart/node";
export const lockwell = createNodeLockwellSolidStartClient({
instanceName: "web",
lifecycle: import.meta.env.DEV ? "development" : "production",
host: env.LOCKWELL_NATIVE_HOST ?? "localhost",
port: Number(env.LOCKWELL_NATIVE_PORT ?? "9444"),
tls: { ca: env.LOCKWELL_NATIVE_CA, serverName: "lockwell.internal" },
credentials: () => ({
accessKeyId: env.LOCKWELL_ACCESS_KEY_ID!,
secretKey: env.LOCKWELL_SECRET_KEY!,
}),
pool: { min: 0, max: 16, acquireTimeoutMs: 10_000, idleTimeoutMs: 30_000 },
retry: { maxAttempts: 3, baseDelayMs: 50, maxDelayMs: 2_000 },
requestTimeoutMs: 30_000,
});
```
The public server surface includes `bindLockwellRequest`, `createObjectRouteHandlers`, `getObjectResponse`,
`headObjectResponse`, `putObjectResponse`, `uploadPartResponse`, `runLockwellServerFunction`,
`runCurrentLockwellServerFunction`, and `runLockwellAction`. Object routes preserve `ReadableStream`
backpressure, canonical `Content-Length`, a single explicit byte range, request disconnect cancellation, validated
`traceparent`, and bounded idempotency keys. Keep streamed bodies in API routes; server-function values are serialized.
The process-local lifecycle single-flights setup, survives development HMR while refreshing credential and telemetry
providers, and closes once during production disposal. Changing non-secret configuration under the same `instanceName`
fails with a stable collision error instead of reusing the wrong pool. Metrics, tracing, retry, logger, pool, TLS, host,
port, and `requestTimeoutMs` are forwarded to the shared client rather than implemented a second time.
## Security and errors
TLS hostname validation stays enabled; provide a private CA and set `serverName` to the certificate identity. Browser,
static, edge, Cloudflare, Netlify, and unknown Nitro targets are denied. Error responses are bounded, no-store DTOs with
stable native error codes; stacks, credentials, filesystem paths, and payloads are not reflected. Health and metrics are
aggregate and should be protected by the deployment's operational policy.
The adapter inherits LNW upload admission (default process-wide 128), per-stream idle/duration bounds, flow control,
Object Lock, ordered user metadata, checksums, ranges, multipart, tags, CORS, notifications, and signed capabilities from
the shared client. SSE-C and ADMIN are absent. Choose S3 explicitly for SSE-C; never switch transports in an exception
handler.
## Example and evidence
The runnable external application is [`examples/solidstart-native`](https://github.com/RusticStack/lockwell/tree/main/examples/solidstart-native):
* `src/routes/api/objects/[...path].ts` handles GET, HEAD, range GET, and PUT streams.
* `src/routes/api/multipart/[uploadId]/[partNumber]/[...path].ts` handles streamed parts.
* `src/actions.ts` demonstrates list, copy, version, multipart, retention, and legal-hold server actions.
Adapter tests cover runtime/preset denial, configuration, single-flight/HMR lifecycle, rotation, interrupted streams,
ranges, size bounds, malformed errors, packed NodeNext consumers, browser denial, and secret/native-code scans. Oracle
ARM64 Node/Bun live TLS and external SolidStart builds passed. The representative lifecycle benchmark (Node LNW
54.3 vs S3 77.2 MiB/s; Bun LNW 71.2 vs S3 126.6 MiB/s) is a bounded process-local sample, not a storage, durability,
latency-SLO, or production-throughput claim. Local Windows Bun returned `UNAVAILABLE` for the stalled-handshake stress
shape and is not part of the current qualification evidence.
Run the adapter checks from `sdk/solidstart`:
```sh
npm run test:node
npm run test:bun
npm run test:distribution
```
Read [the complete SolidStart contract](https://github.com/RusticStack/lockwell/blob/main/docs/solidstart-native-sdk.md)
for migration/rollback and the exact evidence ledger. The merged Next.js adapter has its own [framework guide](/sdks/nextjs);
Nuxt remains open source-owner work and is not claimed here. Rust is an explicit first-party SDK non-goal.
---
---
url: /sdks/nextjs.md
description: Server-only Next.js 16.3 adapter for Lockwell LNW/1 on Node 22+ and Bun 1.4+.
---
# `@kelphect/sdk-nextjs`
`@kelphect/sdk-nextjs` is the merged, server-only Next.js adapter for Lockwell's experimental LNW/1 binary data
plane. It composes the shared [`@kelphect/sdk-native`](/sdks/bun-native) client; it does not duplicate the codec and it
never falls back to HTTP, JSON, XML, or S3. Existing S3 applications should keep using [`@kelphect/sdk`](/sdks/node)
and migrate route-by-route.
## Support boundary
| Deployment or surface | Status | Tested contract |
| --- | --- | --- |
| Next.js 16.3.3 through 16.x | supported | App Router Server Components, Server Actions, and Node Route Handlers |
| Node.js 22+ | supported | `next start`, standalone/container output, and representative Node serverless warm processes |
| Bun 1.4+ | supported | Node-compatible Next runtime with the shared Bun socket condition |
| Client Components and browser bundles | refused | build-time `server-only`/browser condition denial; no credentials or native socket code |
| Edge Route Handlers and middleware | refused | socket-free `edge-light` export raises `LOCKWELL_UNSUPPORTED_RUNTIME` |
The adapter is for Node-compatible server execution. Add `export const runtime = "nodejs"` to routes that import it;
Edge cannot open the authenticated raw TLS socket. No compatibility fallback is attempted. The package is a source and
test contract in this repository; no package publication is implied.
## Install and configure
```sh
npm install @kelphect/sdk-nextjs @kelphect/sdk-native server-only
```
Keep all endpoint and credential values server-only. `lockwellConfigFromEnv()` rejects every `NEXT_PUBLIC_LOCKWELL_*`
variable, and `LockwellSecret` redacts string and JSON conversion:
```dotenv
LOCKWELL_NATIVE_HOST=storage.internal
LOCKWELL_NATIVE_PORT=9444
LOCKWELL_ACCESS_KEY_ID=tenant-key
LOCKWELL_SECRET_KEY=replace-me
LOCKWELL_TLS_CA=/run/secrets/lockwell-ca.pem
# Optional mTLS: provide both values, never only one.
LOCKWELL_TLS_CERT=/run/secrets/client.crt
LOCKWELL_TLS_KEY=/run/secrets/client.key
LOCKWELL_TLS_SERVER_NAME=storage.internal
LOCKWELL_LIFECYCLE=auto
LOCKWELL_TIMEOUT_MS=30000
```
The typed alternative is `createNextLockwell(config)` with `NextLockwellCredentials`, `LockwellSecret`, optional
credential provider, TLS CA/server name, and an atomic mTLS certificate/key pair. `getNextLockwell(config)` owns a
Fast Refresh-safe process accessor; `disposeNextLockwell()` drains that owned pool. Use `lifecycle: "long-lived"` for
`next start`/containers or `lifecycle: "serverless"` for a small lazy warm pool. Neither lifecycle takes ownership of
the application's SIGTERM/SIGINT handlers.
## Streaming route handler
The adapter forwards a Web `ReadableStream` to LNW/1 without buffering the object in memory. A bounded upload requires a
valid `Content-Length` header (or explicit `contentLength`), propagates the request abort signal, and accepts validated
`traceparent` and `Idempotency-Key` context:
```ts
import "server-only";
import { lockwellErrorResponse } from "@kelphect/sdk-nextjs";
import { lockwell } from "@/lib/lockwell";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function PUT(request: Request, { params }: { params: Promise<{ key: string[] }> }) {
try {
const storage = await lockwell();
const result = await storage.putRequest(request, {
bucket: "app-private",
key: (await params).key.join("/"),
request: { signal: request.signal, timeoutMs: 120_000 },
cache: { mode: "revalidate", namespace: "tenant-from-auth" },
});
return Response.json({ etag: result.etag, versionId: result.versionId }, { status: 201 });
} catch (error) {
return lockwellErrorResponse(error);
}
}
```
`NextLockwellAdapter.getResponse()` returns the native response stream, supports bounded single ranges, and emits
private/no-store headers by default. `headCached()` caches metadata only under a stable tenant/principal namespace;
object bytes are never put in Next's server cache.
## Cache, lifecycle, and observability
Use `expireLockwellObject()` in a Server Action for immediate read-your-writes, or
`revalidateLockwellObject()` in a Route Handler for stale-while-revalidate behavior. Tags hash the namespace, bucket,
key, and optional version so a request-supplied bucket name cannot cross a tenant boundary.
The package exports `lockwellRequestContext()` for cancellation, traceparent, and idempotency propagation,
`lockwellDeploymentDiagnostics()`/`redactedConfig()` for safe runtime diagnostics, and
`registerLockwellInstrumentation()`/`lockwellTraceSink()` for optional metrics and OpenTelemetry-compatible spans.
`safeLockwellError()` and `lockwellErrorResponse()` return bounded, no-store errors; unknown exceptions become a generic
500\. Diagnostics and errors exclude credentials, tokens, certificates, payloads, and filesystem paths.
## Data and failure semantics
The adapter exposes the shared native operations through `adapter.client`: bucket and object CRUD, ranges and streaming,
copy, pagination, multipart resume/abort, versions/delete markers, tags, retention, legal hold, and signed capabilities.
LNW capability negotiation, checksums, upload bounds, retryability, cancellation, and tenant authorization remain owned
by the shared SDK/server. Application operations are never silently replayed; one-shot streaming bodies require a
caller-owned recovery strategy. SSE-C is not an LNW capability: SSE-C-shaped headers are rejected before I/O, so use the
documented S3 surface when customer-provided keys are required. Governance bypass and the Admin wire surface are absent.
## Verification and example
The runnable [`examples/nextjs-native-wire`](https://github.com/RusticStack/lockwell/tree/main/examples/nextjs-native-wire)
fixture covers App Router actions, streaming PUT/GET/HEAD/DELETE, bounded ranges, multipart, checksums, versioning,
retention/legal-hold denial, interrupted streams, cache invalidation, standalone output, serverless warm reuse, and
Edge/Client Component denial. The package contract is documented in
[`docs/nextjs-native-sdk.md`](https://github.com/RusticStack/lockwell/blob/main/docs/nextjs-native-sdk.md) and its tests
under [`sdk/nextjs/test`](https://github.com/RusticStack/lockwell/tree/main/sdk/nextjs/test).
```sh
cd sdk/nextjs && npm ci && npm run check
cd ../../examples/nextjs-native-wire && npm ci && npm run build && npm run verify && npm run verify:serverless
```
The merged adapter's Oracle ARM64 evidence covers Node and Bun live TLS workflows, standalone/container and
representative serverless builds, packed consumers, browser/Edge denial scans, quality, supply-chain, and bounded
concurrency. Its benchmark is a single-host measurement, not a general performance or hosted-provider claim.
## Migration and rollback
Adopt one Server Component, Action, or Route Handler at a time while keeping the existing S3/HTTP-native path explicit.
There is no automatic fallback. Rollback removes the adapter import, drains its owned pool with
`disposeNextLockwell()`, and restores the prior route; object bytes, versions, retention, and legal holds require no
migration.
---
---
url: /sdks/nuxt.md
description: >-
Use Lockwell's binary LNW/1 data plane safely from Nuxt 4.5+ and supported
Nitro Node or Bun servers.
---
# Nuxt 4.5+ Native Wire
`@kelphect/nuxt-lockwell` is the server-only Nuxt adapter for the shared `@kelphect/sdk-native` LNW/1 client. It owns
Nuxt configuration, Nitro lifecycle, request cancellation, HTTP streaming helpers, health diagnostics, and optional
OpenTelemetry bridging. It does not copy the wire codec and never falls back to JSON, XML, or S3.
::: warning Release boundary
LNW/1 remains opt-in. Use the source-shipped package only with the matching Lockwell server revision until the release
gate and package-publication policy explicitly approve a public version.
:::
## Install
```sh
npm install @kelphect/nuxt-lockwell @kelphect/sdk-native
```
Use Nuxt 4.5 or newer with Node 22+ or Bun 1.4+. Configure public connection settings in `nuxt.config.ts`, but keep
credentials and TLS private keys in server runtime configuration:
```ts
export default defineNuxtConfig({
modules: ["@kelphect/nuxt-lockwell"],
lockwellNative: {
host: "lockwell.internal",
port: 9444,
requestTimeoutMs: 30_000,
tls: {
serverName: "lockwell.internal",
caFile: "/run/secrets/lockwell-ca.pem",
},
pool: {
maxConnections: 8,
idleTimeoutMs: 30_000,
acquireTimeoutMs: 10_000,
},
},
})
```
Supply credentials only through private runtime environment variables:
```text
NUXT_LOCKWELL_NATIVE_ACCESS_KEY_ID=...
NUXT_LOCKWELL_NATIVE_SECRET_ACCESS_KEY=...
```
Putting `lockwellNative` under `runtimeConfig.public` fails the build. TLS certificate verification remains enabled,
and file-backed CA, client-certificate, and private-key material is loaded only by the server process.
## Supported Nitro presets
| Preset | Runtime | Status |
| --- | --- | --- |
| `nitro-dev` | Node | Supported for development; HMR retires the old pool |
| `node-server` | Node 22+ | Supported |
| `node-cluster` | Node 22+ | Supported; one bounded pool per worker |
| `bun` | Bun 1.4+ | Supported |
| `aws-lambda`, `netlify`, `vercel` | Serverless | Rejected until raw-TLS lifecycle is qualified |
| Cloudflare, edge, Deno, service worker | Edge | Rejected; no raw socket transport |
| `static`, `github-pages`, unknown | No supported server | Rejected |
Unsupported presets fail during `nitro:config`. There is no hidden JSON or S3 fallback. Selective prerendering inside a
supported server build is an application decision; static-only generation is not supported.
## Server helpers
Nitro auto-imports server-only helpers:
* `useLockwellNative()` returns the shared typed client.
* `useLockwellStorage(event)` adds request cancellation, deadlines, and valid inbound trace context.
* `defineLockwellEventHandler(handler)` supplies the event-scoped client, service, and cancellation signal.
* `uploadLockwellObjectFromEvent` streams a known-length body.
* `uploadLockwellMultipartFromEvent` handles bounded unknown-length or resumable uploads with abort cleanup.
* `downloadLockwellObjectToEvent` streams full, closed, open, and suffix ranges.
```ts
export default defineLockwellEventHandler(async ({ event }) => {
return uploadLockwellObjectFromEvent(event, {
bucket: "documents",
key: getRouterParam(event, "key")!,
checksum: {
algorithm: "SHA256",
value: getHeader(event, "x-lockwell-checksum-sha256")!,
},
request: { idempotencyKey: getHeader(event, "idempotency-key") },
})
})
```
Known-length uploads stream directly. Unknown-length requests must use the multipart helper, which processes sequential
5–512 MiB parts, retains at most one part plus an inbound chunk, and performs best-effort abort cleanup. Direct
idempotent streams require a caller-supplied checksum; the adapter never buffers a stream merely to manufacture one.
The service exposes the shared core's bucket, object, pagination, multipart, batch-delete, copy, version, tag,
retention, legal-hold, CORS, notification, readiness, and signed-capability operations. SSE-C and Admin operations are
not LNW/1 data-plane capabilities.
## Lifecycle and diagnostics
The native client is lazy and shared per Nitro application. Nitro's `close` hook drains it, and development HMR retires
the previous instance before replacement. H3 request cancellation propagates to LNW/1 `CANCEL`; operation deadlines use
the smaller caller or configured bound.
`GET /api/_lockwell/health` performs native readiness and returns 503 when unavailable. Its fixed response contains
only aggregate state, bounded connection counts, negotiated capabilities, and metrics—never credentials, TLS material,
peer certificates, object keys, tenant IDs, or payloads. When `@opentelemetry/api` is installed and telemetry is
enabled, the adapter uses the application's providers and installs no exporter.
## Migration and rollback
Keep S3 and LNW/1 clients explicit while migrating. Compare reads and metadata first, then move idempotent writes,
multipart recovery, versions, Object Lock denials, cancellation, and bounded-concurrency workloads. Rollback drains the
native pool and routes the application back to its separately configured S3 client; object bytes and metadata require no
migration because both transports use the same server-side storage authority.
See the [Native Wire guide](/guide/native-wire), [capability index](/reference/sdk-capabilities), and the
[source contract](https://github.com/RusticStack/lockwell/blob/main/docs/sdk-nuxt-native.md).
---
---
url: /sdks/java-spring-wire.md
description: >-
Spring Boot 4.1 and JDK 25 Lockwell Native Wire starter with TLS, streaming,
Object Lock, and metrics.
---
# Spring Boot Native Wire starter
`com.lockwell:lockwell-spring-boot-starter` 0.2.2 is the opt-in, JDK 25-first client for LNW/1. It is a separate
artifact compiled with `--release 25`; the existing `com.lockwell:lockwell-sdk` remains Java 21 bytecode and continues
to provide the S3, HTTP-native JSON, Admin, and kit clients. The starter sends deterministic binary frames only—never
HTTP, JSON, or XML—and never falls back to another surface.
## Install
```xml
com.lockwell
lockwell-spring-boot-starter
0.2.2
```
This coordinate is source-shipped and consumer-tested in the repository; use your approved internal or GitHub Packages
mirror and pin the artifact/checksums. The starter is tested with Spring Boot 4.1.1 and an external compatibility
consumer under Spring Boot 4.0.7.
## Configuration
Enable the starter explicitly and keep secrets in environment-backed configuration:
```properties
lockwell.native-wire.enabled=true
lockwell.native-wire.host=lockwell.internal.example
lockwell.native-wire.port=9444
lockwell.native-wire.access-key-id=${LOCKWELL_ACCESS_KEY_ID}
lockwell.native-wire.secret-key=${LOCKWELL_SECRET_KEY}
lockwell.native-wire.tls.ca-certificate=/run/secrets/lockwell-ca.pem
# Base64(SHA-256(SubjectPublicKeyInfo)); supplements PKIX + hostname verification.
lockwell.native-wire.tls.spki-sha256-pins[0]=${LOCKWELL_SPKI_PIN}
# Required together for an optional/required-mTLS listener.
lockwell.native-wire.tls.client-key-store=/run/secrets/lockwell-client.p12
lockwell.native-wire.tls.client-key-store-password=${LOCKWELL_CLIENT_KEYSTORE_PASSWORD}
```
Public properties include `host`, `port`, `accessKeyId`, `secretKey`, `poolSize` (1–128, default 4),
`connectTimeout` (10 seconds), `readTimeout` (2 minutes), `acquireTimeout` (10 seconds), and nested TLS CA, SPKI pin,
PKCS#12 identity, and explicit loopback-plaintext test settings. TLS 1.3 and hostname verification are enforced for
non-loopback use. `toString()` and diagnostics redact secret and keystore password material.
Spring auto-configuration creates lifecycle-managed `LockwellNativeWireClient`, a virtual-thread
`LockwellNativeWireAsyncClient`, and `LockwellNativeWireHealthIndicator` when `lockwell.native-wire.enabled=true`.
The health check performs authenticated readiness, including the server metadata-authority/quorum guard. Disable the
property to roll back without changing stored objects.
## Java API and streaming
The client exposes capabilities/readiness, bucket CRUD/versioning, object PUT/GET/HEAD/DELETE, ranges, ordered duplicate
user metadata, checksums, conditions, list/pagination/copy, versions/delete markers, multipart create/upload/list/
complete/abort, tags, retention, legal hold, batch delete, bucket CORS, webhook notifications, and signed GET/PUT
capabilities. `ObjectLockWrite` carries optional retention and legal-hold state on the initial PUT; the server rechecks
Object Lock scopes and commits the state atomically with the new version. Governance bypass is not supported.
Use `getObject(request, OutputStream)` for direct streaming or `withObject(request, handler)` for a bounded 64 KiB
backpressured pipe. The callback must consume the stream before returning; closing or abandoning it cancels and invalidates
the underlying connection. `TransferProgressListener` reports bytes. `RequestOptions` carries a deadline and validated
traceparent.
For serializable application work, the virtual-thread async client returns `CompletableFuture` values. The connection
pool remains the concurrency bound; an application-supplied executor remains application-owned and is not closed by the
client.
## Auth, retry, and telemetry
The client performs HELLO/WELCOME and a fresh access-key timestamp/nonce proof for each connection. Only an
`AUTH_ERROR` with exact `RATE_LIMITED`, `retryable=true`, and a bounded hint may reopen a fresh socket and regenerate
credentials/proof before the caller deadline. `AUTH_REPLAY`, scope/tenant denials, malformed hints, and application
operations are never retried implicitly. A streaming write needs a new body, idempotency key, and body-binding checksum
for an explicit caller retry.
Micrometer meters use bounded labels only:
| Meter | Labels / meaning |
| --- | --- |
| `lockwell.native.wire.requests` | `result=success`, `result=failure`, or `result=cancelled` |
| `lockwell.native.wire.request.duration` | request timer |
| `lockwell.native.wire.connections` | aggregate active connections |
| `lockwell.native.wire.bytes` | `direction=inbound` or `direction=outbound`; header + metadata + payload + CRC after a successful write/validated read |
Tracing accepts a `TraceparentProvider`; diagnostics and errors are redacted. Webhook signing secrets are returned once;
configure a credential-free HTTPS URL and HMAC headers rather than putting a token in a query string.
## Capability boundary
SSE-C is deliberately not advertised by this starter: critical SSE-C fields are rejected and same-named user metadata is
still user-owned, so metadata cannot manufacture internal encryption state. Use the S3 client for genuine SSE-C. The
Admin capability is a separate unimplemented authentication surface. Browser, edge, and static runtimes are not starter
targets.
## TangibleShift-shaped example
The public consumer fixture under
[`sdk/java-spring-boot-starter/consumer-tests/maven`](https://github.com/RusticStack/lockwell/tree/main/sdk/java-spring-boot-starter/consumer-tests/maven)
uses company-scoped keys and exercises immutable conditional writes, ranges, copy/list, multipart resume/abort,
versions, batch delete, CORS, webhook configuration, retention/legal hold, and signed reads. A service can keep the
same shape without copying private application code:
```java
import com.lockwell.sdk.springwire.LockwellNativeWireClient;
import com.lockwell.sdk.springwire.NativeWireTypes;
import java.io.InputStream;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
@Service
final class ArtifactStore {
private final LockwellNativeWireClient lockwell;
ArtifactStore(LockwellNativeWireClient lockwell) {
this.lockwell = lockwell;
}
void write(String company, InputStream body, long length) {
var request = new NativeWireTypes.PutObjectRequest(
company + "-artifacts", "immutable/report.pdf", length,
"application/pdf", List.of(), Optional.empty(), Optional.empty(), false,
Optional.empty(), Optional.empty(),
Optional.of(NativeWireTypes.ObjectLockWrite.retention(
NativeWireTypes.RetentionMode.COMPLIANCE, Instant.now().plus(30, ChronoUnit.DAYS))),
NativeWireTypes.RequestOptions.defaults());
lockwell.putObject(request, body);
}
}
```
Adapt the bucket naming, authorization, and retention policy to your tenant model; do not put access-key secrets in the
service source. The exact record constructors may evolve with the starter contract, so compile against the pinned
artifact and consult `NativeWireTypes` in source.
## Verification and rollback
Run the starter's Maven tests and external Maven/Gradle consumers with JDK 25 before changing a claim. The cross-language
fixture proves byte compatibility with the Go server, comma/equals/reserved-name metadata round trips, and that native
wire metadata cannot trigger SSE-C. Disable `lockwell.native-wire.enabled`, drain clients, and restore the existing
explicit S3 or HTTP-native JSON selection to roll back; no object migration is performed.
See [LNW architecture](/guide/native-wire), the [wire reference](/reference/native-wire), and the
[source README](https://github.com/RusticStack/lockwell/blob/main/sdk/java-spring-boot-starter/README.md) for the full
protocol, evidence, and compatibility ledger.
---
---
url: /reference/s3-operations.md
description: >-
The full matrix of S3 operations the Lockwell data-plane client supports, with
key options, shared by the Go, Node, and Java SDKs.
---
# S3 operations reference
The S3 data-plane client implements the S3 API surface that Lockwell supports. If you have used the AWS SDK, the
operation names and option names will look familiar.
This page lists every operation in one place so you can see the full breadth at a glance. Each operation is available in
the Go, Node, and Java SDKs with matching names.
For the JSON alternative (bearer tokens instead of SigV4, no XML), see the
[native data-plane API](/reference/native-api). For tenant and key management, see the
[Admin API](/reference/admin-api).
## Buckets
| Operation | Purpose | Key options |
| --------------------- | ----------------------------------------------- | ------------------------------- |
| `CreateBucket` | Create a private bucket | Object Lock enabled at creation |
| `HeadBucket` | Check that a bucket exists and you can reach it | |
| `DeleteBucket` | Delete an empty bucket | |
| `PutBucketVersioning` | Enable or suspend versioning | `Enabled`, `Suspended` |
| `GetBucketVersioning` | Read the versioning state | |
Buckets are always private. There is no public-bucket or anonymous-access toggle.
::: info A public-bucket or anonymous-access toggle is a deliberate non-goal. Share objects with a presigned GET URL or
a native signed URL instead. :::
## Objects: write
| Operation | Purpose | Key options |
| --------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `PutObject` | Write an object from a buffer or a stream | content type, user metadata, idempotency key, checksum algorithm, SSE-S3, retention, legal hold |
| `CopyObject` | Server-side copy within or across buckets | metadata directive (COPY or REPLACE), copy-source conditionals, SSE |
| `DeleteObject` | Delete one object or one version | version id |
| `DeleteObjects` | Delete up to 1000 objects in one request | quiet mode, per-key errors with partial success |
`PutObject` accepts a stream, so you can upload an object larger than memory without buffering it. See
[Upload & download](/guide/data-operations).
## Objects: read
| Operation | Purpose | Key options |
| ------------ | ------------------------------------- | -------------------------------------------------------------- |
| `GetObject` | Stream an object body | byte range, version id, part number, response header overrides |
| `HeadObject` | Read object metadata without the body | byte range, version id, response header overrides |
## Listing
| Operation | Purpose | Key options |
| ---------------------- | ---------------------------------------- | ------------------------------------------------------------ |
| `ListObjectsV2` | List objects by prefix, token-paged | prefix, delimiter, start-after, continuation token, max keys |
| `ListObjects` | List objects, marker-paged (the v1 form) | prefix, delimiter, marker, max keys |
| `ListObjectVersions` | List versions and delete markers | prefix, delimiter, key marker, version-id marker, max keys |
| `ListMultipartUploads` | List in-progress multipart uploads | prefix, delimiter, key marker, upload-id marker, max uploads |
| `ListParts` | List the parts of one multipart upload | part-number marker, max parts |
Every listing operation has a paginator that follows the continuation tokens for you: `ListObjectsV2Paginator`,
`ListObjectVersionsPaginator`, `ListMultipartUploadsPaginator`, and `ListPartsPaginator`. Each exposes `HasMorePages()`
and `NextPage()`. See [Listing & pagination](/guide/listing-objects).
## Multipart uploads
| Operation | Purpose | Key options |
| ------------------------- | --------------------------------------------------- | ---------------------------------- |
| `CreateMultipartUpload` | Start a multipart upload | same write options as `PutObject` |
| `UploadPart` | Upload one part | per-part checksum |
| `UploadPartCopy` | Fill a part by server-side copy from another object | copy-source range and conditionals |
| `CompleteMultipartUpload` | Assemble the uploaded parts into one object | idempotency key |
| `AbortMultipartUpload` | Discard an upload and its parts | |
See [Multipart uploads](/guide/multipart-uploads) for a full large-file example.
## Tagging
| Operation | Purpose | Key options |
| --------------------- | -------------------------------- | ----------- |
| `PutObjectTagging` | Replace the tag set on an object | version id |
| `GetObjectTagging` | Read the tag set | version id |
| `DeleteObjectTagging` | Remove all tags | version id |
## Versioning
Versioning is controlled with `PutBucketVersioning` and `GetBucketVersioning` (above). Once enabled, every write keeps
the prior version, a delete writes a delete marker, and you can read or delete a specific `versionId`. List versions
with `ListObjectVersions`. See [Versioning](/guide/versioning).
## Object Lock
| Operation | Purpose | Key options |
| -------------------- | --------------------------------------------- | ----------- |
| `GetObjectRetention` | Read the retention mode and retain-until date | version id |
| `GetObjectLegalHold` | Read the legal-hold status | version id |
Retention and legal holds are set when you write the object, through the `PutObject` options (retention mode and
retain-until date, legal hold on or off). Object Lock must be enabled when the bucket is created. Governance-mode bypass
is not supported. See [Object Lock](/guide/object-lock).
## Presigned URLs
| Operation | Purpose | Key options |
| ------------------ | --------------------------------------------------- | ----------------------------- |
| `PresignGetObject` | Build a signed GET URL a browser can fetch directly | expiry (capped by the server) |
The Lockwell S3 service accepts query-SigV4 presigned GET, PUT, HEAD, and DELETE requests from compatible external
clients. The first-party Go, Node, and Java S3 clients generate presigned GET, PUT, HEAD, and DELETE URLs. To let a
browser upload directly with a first-party client, use a native signed PUT URL from the
[app kit or native client](/guide/signed-urls).
That is a deliberate split: presigned writes on the S3 surface stay off, and the native signed URL is the supported
upload path.
## Checksums and integrity
Request a checksum on any write with the checksum option, using CRC32, CRC32C, CRC64NVME, SHA-1, or SHA-256. The SDK
computes the digest on the client, the server verifies it, and the value comes back on the response. Multipart uploads
support a checksum per part. See [Checksums & integrity](/guide/checksums).
## Conditional writes and idempotency
The first-party S3 clients support create-only `PutObject` with `If-None-Match: *`. Overwrite-only
`If-Match: ` writes use the native client.
`CopyObject` does support copy-source conditionals (`If-Match`, `If-None-Match`, `If-Modified-Since`,
`If-Unmodified-Since` evaluated against the source object). Set an idempotency key on a write so a retried request is
applied once. See [Conditional writes & idempotency](/guide/conditional-writes).
## Retries
Construct a client with a retry policy. The default policy makes three attempts with exponential backoff and jitter, and
retries idempotent requests (GET, HEAD, DELETE) along with writes that carry an idempotency key. The disabled policy
makes a single attempt. See [Errors & retries](/guide/errors-and-retries).
## Server-side encryption
Objects are encrypted at rest by default with a per-tenant data key. Request the SSE-S3 server-managed mode explicitly
with the encryption option on a write. SSE-KMS is not supported. The S3 wire API supports SSE-C when at-rest encryption
is enabled: callers provide the same AES-256 customer key and MD5 headers on every applicable write, read, copy, and
multipart request. The first-party Go, Node, and Java S3 clients expose typed SSE-C and copy-source SSE-C helpers.
### Reserved metadata isolation
User metadata is lossless, duplicate-preserving, and user-owned. The merged metadata remediation stores it separately
from internal SSE-C and Object Lock state, including historical reserved names such as
`x-amz-meta-lockwell-sse-customer-key-md5`. Caller-controlled metadata cannot manufacture an internal encryption marker;
the same comma/equals/reserved-name round trips are covered through S3, native HTTP, and LNW/1. This does not make SSE-C
available on native transports: use the S3 surface for genuine customer-provided keys.
## What the S3 client does not do
These are deliberate non-goals. They are not roadmap gaps, and the SDK will not expose them:
* Provider-specific POST policies or STS credentials. The typed S3 presigners cover GET, PUT, HEAD, and DELETE;
the native client provides constrained GET/PUT URLs for browser-direct flows.
* Public or anonymous buckets, public sharing, ACLs.
* SSE-KMS. SSE-C and copy-source SSE-C are available on the S3 wire and through typed first-party SDK helpers.
* IAM, STS, AssumeRole, and bucket policies (use Lockwell access keys and scopes).
* Website hosting, S3 Select, Inventory, Intelligent-Tiering, Object Lambda, S3 Express.
* Event notifications on the S3 client (configure them on the [native client](/guide/webhooks) instead).
For the reasoning behind these boundaries, see the repository's
[final replacement contract](https://github.com/RusticStack/lockwell/blob/main/docs/final-replacement-contract.md).
---
---
url: /reference/native-api.md
description: >-
The wire-level reference for the Lockwell native JSON data-plane API at
/api/v1/, including bearer-token auth, routes, signed URLs, and error shapes.
---
# Native data-plane API
The native JSON data-plane API lives at `/api/v1/` on the public listener: the same host:port as the S3 API, a different
path prefix. No SigV4 signing, no XML. JSON in, JSON out.
It is a new transport and auth layer over the same domain pipeline the S3 handler uses (the same object coordinator,
encryption, dedup, checksums, versioning, object-lock, scope enforcement, quota, and audit). A native write is
encrypted-at-rest, deduped, quota-checked, and retention-gated exactly like an S3 write.
This page documents the legacy HTTP/JSON surface. Existing integrations can
use [Go's explicit compatibility client](/sdks/go#pkg-lockwellnative-the-legacy-httpjson-compatibility-client),
[Node](/sdks/node#nativeclient-the-native-client), or
[Java](/sdks/java#lockwellnativeclient-the-native-client). New Go applications
should use the direct binary [LNW/1 client](/sdks/go#pkg-lockwellwire-the-lnw1-native-wire-client).
## Authentication
The native API does not use SigV4 on data calls. A caller exchanges its existing S3 access key for a short-lived native
bearer token.
### Mint a token: `POST /api/v1/auth/token`
OAuth client-credentials style. Present your S3 access-key id plus secret either as HTTP Basic
(`Authorization: Basic base64(accessKeyId:secretKey)`) or a JSON body `{"accessKeyId": "...", "secretKey": "..."}`. On
success you get a short-lived bearer token:
```text
lwtk_.
```
* TTL is `security.native_api_token_ttl` (default `1h`). Keep it short: a leaked token is replayable until it expires.
* The token is stateless and signed (HMAC under a per-deployment key derived from the at-rest master key), so the hot
path verifies it with no per-request DB lookup for the token itself.
* Revoking or expiring the underlying access key (or disabling the tenant) invalidates outstanding tokens promptly. The
verify path re-checks revocation on every request, so a token can never outlive or out-scope the key it points at.
* This endpoint accepts the secret once, so it must run behind TLS. It is rate-limited per access-key id and audited
(success and failure).
Send the token on every subsequent call as `Authorization: Bearer `. The SDKs cache it until shortly before
expiry, refresh transparently, and re-mint once on a 401.
::: warning The bearer token is replayable until it expires. Keep its TTL short and always mint it over TLS, since
`auth/token` accepts the secret in the clear. :::
### Signed-URL auth (no bearer token)
`GET|PUT /api/v1/signed/{bucket}/{key...}?token=…` is authorized solely by the query `token`: an HMAC-signed, expiring
URL token (`lwurl_…`) minted by `POST /api/v1/sign-url`, under a separate per-deployment key.
At access time the handler re-checks the underlying key's revocation, re-runs the per-operation scope and bucket-policy
gates against the current scope, and enforces that the request method and bucket/key match the signed token. A tampered,
expired, wrong-method, wrong-resource, revoked, or scope-exceeding URL is rejected (401/403). See
[signed URLs](/guide/signed-urls).
Browser preflight for signed URLs uses `OPTIONS /api/v1/signed/{bucket}/{key...}?token=…`. The token still has to
authorize the requested method and resource, and the bucket's stored CORS rules still have to match the origin and
requested headers. CORS never grants object access by itself.
## Routes
All routes except `/healthz`, `/openapi.json`, and `/auth/token` require a valid bearer token. The tenant is taken from
the signed token, never the request path, so cross-tenant access is structurally impossible (another tenant's bucket
returns `404`, never a leak).
### Buckets
| Method + path | Purpose |
| ----------------------------------------- | ---------------------------------------------------------- |
| `GET /buckets` | list the tenant's buckets |
| `POST /buckets` | create a private bucket (versioning / object-lock options) |
| `GET /buckets/{bucket}` | get a bucket |
| `DELETE /buckets/{bucket}` | delete an empty bucket |
| `GET\|PUT /buckets/{bucket}/versioning` | read / set versioning state |
| `GET\|PUT\|DELETE /buckets/{bucket}/cors` | read / set / clear browser CORS rules |
### Objects
| Method + path | Purpose |
| --------------------------------------------- | --------------------------------------------------------------- |
| `PUT /buckets/{bucket}/objects/{key...}` | streaming upload |
| `GET /buckets/{bucket}/objects/{key...}` | streaming download (`Range` supported) |
| `HEAD /buckets/{bucket}/objects/{key...}` | metadata only |
| `DELETE /buckets/{bucket}/objects/{key...}` | delete (delete marker in a versioned bucket) |
| `GET /buckets/{bucket}/objects` | list (`prefix` / `delimiter` / `maxKeys` / `continuationToken`) |
| `POST /buckets/{bucket}/objects:batchDelete` | batch delete, per-key results |
| `POST /buckets/{bucket}/object-copy/{key...}` | same-tenant server-side copy |
The upload supports several controls:
* An `Idempotency-Key` header, mapped onto the same idempotency store the S3 `X-Lockwell-Idempotency-Key` path uses.
* Native conditional writes (`If-Match` / `If-None-Match`).
* Optional server-side checksum verification (`X-Lockwell-Checksum-` for CRC32, CRC32C, CRC64NVME, SHA1, SHA256). A
bad digest is rejected before any bytes are committed.
The copy source is the JSON body
(`{sourceBucket, sourceKey, sourceVersionId?, metadataDirective?, …, requireAbsent?, requireMatchEtag?}`). Cross-tenant
copy is impossible, since the source resolves under the token's tenant.
### Versions, tags, and per-object WORM
| Method + path | Purpose |
| --------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `GET /buckets/{bucket}/versions` | list versions and delete markers (`prefix` / `keyMarker` / `versionIdMarker`) |
| `GET\|PUT\|DELETE /buckets/{bucket}/object-tags/{key...}` | get / replace / clear the JSON tag set |
| `GET\|PUT /buckets/{bucket}/object-retention/{key...}` | per-object retention (`{mode: GOVERNANCE\|COMPLIANCE, retainUntil}`) |
| `GET\|PUT /buckets/{bucket}/object-legal-hold/{key...}` | legal-hold status (`{status: ON\|OFF}`) |
These four sub-resources use a distinct path prefix (not a suffix on `/objects/{key...}`) so the key wildcard preserves
embedded slashes.
::: info Retention can be extended but never shortened. There is no governance bypass on the native path, a deliberate
non-goal that stays closed. :::
### Multipart
| Method + path | Purpose |
| ------------------------------------------------------------------------ | ------------------------ |
| `POST /buckets/{bucket}/multipart/{key...}` | create an upload |
| `PUT /buckets/{bucket}/multipart/{uploadId}/parts/{partNumber}/{key...}` | upload a part |
| `GET /buckets/{bucket}/multipart/{uploadId}/parts/{key...}` | list parts |
| `POST /buckets/{bucket}/multipart/{uploadId}/complete/{key...}` | complete |
| `DELETE /buckets/{bucket}/multipart/{uploadId}/{key...}` | abort |
| `GET /buckets/{bucket}/multipart` | list in-progress uploads |
### Signed URLs
* `POST /sign-url` (bearer): mint a method-, resource-, and scope-bounded signed URL.
* `GET|PUT /signed/{bucket}/{key...}?token=...` (no bearer): use a signed URL.
`method` is `GET` (download) or `PUT` (upload): the native API supports signed write URLs. The TTL is clamped to
`security.max_presign_ttl`, and the URL can never exceed the minting key's live scope (a read-only key minting a PUT URL
is `403`).
`POST /sign-url` accepts
```text
{method, bucket, key|keyPrefix, ttlSeconds?, contentType?, contentLengthMax?, checksumAlg?, checksumVal?,
idempotencyKey?, responseContentType?, responseContentDisposition?, reason?}
```
PUT constraints (`contentType`, `contentLengthMax`, `checksum*`, `idempotencyKey`, `keyPrefix`), GET response overrides
(`responseContentType`, `responseContentDisposition`), and the optional audit `reason` are HMAC-covered and enforced or
recorded at dispatch; cross-method fields are rejected.
### Bucket CORS
| Method + path | Purpose |
| ----------------------------------- | ---------------------------------------- |
| `GET /buckets/{bucket}/cors` | get browser CORS rules |
| `PUT /buckets/{bucket}/cors` | set browser CORS rules |
| `DELETE /buckets/{bucket}/cors` | clear browser CORS rules |
| `OPTIONS /signed/{bucket}/{key...}` | signed-URL browser preflight (no bearer) |
The native shape is camelCase JSON:
```json
{
"rules": [
{
"id": "browser-direct",
"allowedOrigins": ["https://app.example.com"],
"allowedMethods": ["GET", "HEAD", "PUT"],
"allowedHeaders": ["content-type"],
"exposeHeaders": ["ETag", "X-Lockwell-Version-Id"],
"maxAgeSeconds": 600
}
]
}
```
`PUT /buckets/{bucket}/cors` validates through the same CORS validator and bucket-config store as the S3 `?cors` XML
route. It is an admin-scoped bucket operation; ordinary data keys cannot change browser policy. Matching authenticated
or signed URL object responses emit `Access-Control-*` headers, but the normal bearer/signed-token, scope,
bucket-policy, quota, object-lock, and audit gates still run.
### Bucket event notifications
| Method + path | Purpose |
| ---------------------------------------- | ------------------------------------------ |
| `GET /buckets/{bucket}/notifications` | get the configuration |
| `PUT /buckets/{bucket}/notifications` | set it (an empty `configs` list clears it) |
| `DELETE /buckets/{bucket}/notifications` | clear it |
Only the webhook target is supported. An `sns`/`sqs`/`lambda` target is `501`. A new config ID returns the
server-generated `signingSecret` exactly once; it is sealed at rest and omitted from GET and same-ID updates, which
carry only `hasSecret`. See [webhooks](/guide/webhooks).
### Native fields surfaced
Object reads surface Lockwell-native details S3 XML hides: native checksums (CRC32/CRC32C/CRC64NVME/SHA1/SHA256),
encryption and compression status, storage class, version id, retention and legal-hold, and content length. They come
back as JSON fields and `X-Lockwell-*` response headers.
## Error shape (`problem+json`)
Failures return an `application/problem+json` body:
```json
{
"code": "not_found",
"message": "bucket \"reports\" not found",
"status": 404,
"requestId": "req_01HXY…"
}
```
`code` is a stable machine-readable string. `requestId` correlates the failure with its server-side audit row (also
echoed in the `X-Request-Id` header). Status mapping:
| Status | Meaning |
| ------ | ------------------------------------------------------------- |
| `401` | missing/invalid/expired bearer token, or a revoked access key |
| `403` | access-key scope or bucket-policy denial |
| `404` | bucket/key not found |
| `409` | bucket already exists |
| `412` | conditional-write or copy-source precondition not met |
| `501` | unsupported notification target (SNS/SQS/Lambda) |
| `507` | tenant storage quota exceeded |
## Security parity
The native API is private by default and never served anonymously. Every control the S3 path enforces is enforced
identically here, through the same domain services:
* Tenant isolation (from the token, never the path).
* Per-operation `read`/`write`/`delete`/`admin` plus bucket/prefix scope enforcement.
* Explicit-deny bucket policies.
* Encryption, dedup, quota, object-lock, retention, and legal-hold.
* Audit on every request, including denials.
It does not relax any S3 control or enable any public/anonymous access. See [tenancy and auth](/guide/tenancy-and-auth).
## OpenAPI
The full machine-readable contract is an OpenAPI 3 document, available two ways:
* Served live at `GET /api/v1/openapi.json` (unauthenticated, since you need the contract before you hold a token).
* Committed at
[`internal/nativeapi/openapi.json`](https://github.com/RusticStack/lockwell/blob/main/internal/nativeapi/openapi.json)
(the canonical source; the served document is the same bytes).
Each operation carries a unique `operationId` (`putObject`, `listObjects`, `signURL`, `setBucketNotifications`, …), so
you can generate a client in any language with `openapi-generator` (`make codegen` produces TypeScript/Python/Go clients
for both the native and admin specs). Prefer the first-party SDKs for Go, Node, and Java; codegen is the path for every
other language.
If the deployment selects the binary transport, read the separate [Native Wire reference](/reference/native-wire) and
[`/native-wire-v1.json`](/native-wire-v1.json). LNW/1 is not an alternate JSON encoding: it has its own 40-byte frame,
TLV/document rules, handshake, capability mask, and stable error registry. The HTTP OpenAPI document does not describe
that listener.
## Interactive reference
Every native operation below is generated from that OpenAPI document, so it always matches the shipped server. Expand an
operation for its path and query parameters, request and response schemas, and copy-paste `curl` / `fetch` samples. The
example host is a placeholder; swap in your own deployment.
---
---
url: /reference/native-wire.md
description: >-
Machine-oriented LNW/1 frame, capability, operation, field, limit, and error
reference.
---
# Native Wire reference
This page is a compact index for agents and implementers. The normative source is
[`docs/native-wire-v1.md`](https://github.com/RusticStack/lockwell/blob/main/docs/native-wire-v1.md); the complete machine
contract is [`/native-wire-v1.json`](/native-wire-v1.json). Both are versioned with the repository. LNW/1 is experimental,
opt-in, and disabled by default.
## Envelope
| Item | Value |
| --- | --- |
| Magic | ASCII `LKW1` (`4c4b5731`) |
| Version | `1` |
| Byte order | Unsigned big-endian |
| Header | 40 bytes |
| Body | metadata TLVs, payload, CRC32C (4 bytes) |
| Text | shortest-form UTF-8, NUL-free |
| Unknown optional field | Skip |
| Unknown critical field | Reject the stream (`UNSUPPORTED_FIELD`) |
| Downgrade / code reuse | Forbidden |
Header offsets are fixed and must not be inferred from a language ABI:
| Offset | Size | Field |
| ---: | ---: | --- |
| 0 | 4 | magic |
| 4 | 2 | version |
| 6 | 1 | frame type |
| 7 | 1 | flags |
| 8 | 8 | connection sequence |
| 16 | 8 | request ID |
| 24 | 4 | stream ID |
| 28 | 2 | operation/error/control code |
| 30 | 2 | reserved (zero) |
| 32 | 4 | metadata length |
| 36 | 4 | payload length |
## Defaults and security
| Limit or policy | Default |
| --- | --- |
| Metadata / DATA payload / frame | 64 KiB / 1 MiB / 1,114,156 bytes |
| Streams / connections | 128 / 1,024 |
| Stream / connection window | 4 MiB / 16 MiB |
| Auth skew | 300 seconds (maximum 5 minutes) |
| Replay state | 1,024 principals × 256 nonces per principal |
| Upload admission | `security.max_concurrent_uploads = 128`, process-wide |
| Upload idle / maximum duration | 2 minutes / 24 hours per stream |
| Auth retry hint maximum | 600,000 ms |
| Non-loopback TLS | TLS 1.3 + hostname verification |
User metadata is an ordered, duplicate-preserving user namespace. Internal SSE-C and Object Lock fields are typed and
separate. The effective lockwelld mask excludes `SSE_C` (128); use S3 for genuine SSE-C. Admin bit 16 and operation range
`0x1000`–`0x10ff` are reserved and unimplemented.
## Frame types
| Code | Type | Role |
| ---: | --- | --- |
| `0x01` | HELLO | Client version, capabilities, receive limits |
| `0x02` | WELCOME | Server selection and intersection |
| `0x03` | AUTH | Access-key timestamp, nonce, transcript proof |
| `0x04` | AUTH\_OK | Tenant, session, expiry, capabilities, effective limits |
| `0x05` | AUTH\_ERROR | Bounded authentication failure |
| `0x10` | REQUEST | Starts one operation on an odd client stream |
| `0x11` | DATA | Flow-controlled bytes |
| `0x12` | END | Closes one direction |
| `0x13` | CANCEL | Cancels the stream context |
| `0x20` | RESPONSE | Starts a successful response |
| `0x21` | ERROR | Typed terminal response |
| `0x30` | WINDOW\_UPDATE | Grants stream or connection credit |
| `0x31` / `0x32` | PING / PONG | Eight opaque liveness bytes |
| `0x33` | GOAWAY | Drain or protocol reason and last accepted stream |
| `0x34` | CLOSE | Authenticated close acknowledgement |
## Capabilities
| Bit | Name | Advertised by lockwelld |
| ---: | --- | --- |
| 1 | BUCKETS | yes |
| 2 | OBJECTS | yes |
| 4 | PAGINATION | yes |
| 8 | MULTIPART | yes |
| 16 | VERSIONING | yes |
| 32 | OBJECT\_LOCK | yes |
| 64 | TAGS | yes |
| 128 | SSE\_C | **no** (no enforced native path) |
| 256 | SIGNED\_CAPABILITY | yes |
| 512 | TRACE\_CONTEXT | yes |
| 1024 | CORS | yes |
| 2048 | NOTIFICATIONS | yes |
| 65536 | ADMIN (reserved) | **no** |
## Operations
| Code range | Operations | Required capability |
| --- | --- | --- |
| `0x0001`–`0x0002` | `CAPABILITIES`, `READINESS` | authenticated discovery |
| `0x0100`–`0x0105` | bucket CRUD and versioning | BUCKETS |
| `0x0200`–`0x0207` | object CRUD, ranges, copy, versions, batch delete | OBJECTS |
| `0x0300`–`0x0305` | multipart create/part/list/complete/abort/list | MULTIPART |
| `0x0400`–`0x0402` | tags get/put/delete | TAGS |
| `0x0500`–`0x0503` | retention and legal hold | OBJECT\_LOCK |
| `0x0600` | signed capability mint | SIGNED\_CAPABILITY |
| `0x0700`–`0x0702` | bucket CORS get/put/delete | CORS |
| `0x0710`–`0x0712` | bucket webhook notifications get/put/delete | NOTIFICATIONS |
Clients must not send an operation until its capability is selected. The complete field and document schemas are in the
JSON registry; response metadata includes status, ETag, version, length/type, request ID, traceparent, timestamps,
delete-marker, retention/legal-hold, and ordered user metadata.
## Errors and retry contract
| Code | Name | Typical handling |
| ---: | --- | --- |
| `0x0103` | AUTH\_REPLAY | terminal; do not retry the proof |
| `0x0300` | RATE\_LIMITED | retry only from a fresh connection with fresh credentials/proof when marked retryable |
| `0x0301` | UNAVAILABLE | bounded caller retry when operation/body is replay-safe |
| `0x0302` | DEADLINE\_EXCEEDED | terminal for the stream; partial upload state is removed |
| `0x0303` | CANCELLED | caller cancellation; no implicit replay |
| `0x0204` / `0x0205` | RETENTION\_DENIED / LEGAL\_HOLD\_DENIED | authorization/policy denial |
| `0x0207` / `0x0208` | CHECKSUM\_MISMATCH / TOO\_LARGE | fix input or limits; do not retry unchanged |
`AUTH_ERROR` fields are `retryable` (required bool), `retryAfterMillis` (optional, ≤600000), and bounded `message` (≤512
bytes). Error frames contain safe messages and correlation only; secrets and payloads are never reflected.
## Implementations and source links
* [Native-wire architecture and rollout](/guide/native-wire)
* [`@kelphect/sdk-native` guide](/sdks/bun-native) (Node 22+, Bun 1.4+; browser denied)
* [`@kelphect/sdk-solidstart` guide](/sdks/solidstart) (SolidStart v2; Node/Bun Nitro server presets only)
* [`@kelphect/sdk-nextjs` guide](/sdks/nextjs) (Next.js 16.3.3–16.x; Node/Bun-compatible server runtime only)
* [Spring Boot starter guide](/sdks/java-spring-wire) (JDK 25, Spring Boot 4.1.1)
* [Raw protocol Markdown on GitHub](https://github.com/RusticStack/lockwell/blob/main/docs/native-wire-v1.md)
* [Shared fixtures](https://github.com/RusticStack/lockwell/tree/main/tests/native-wire)
---
---
url: /reference/admin-api.md
description: >-
The wire-level reference for the Lockwell JSON Admin API at /admin/api/v1/,
covering bearer-token auth, RBAC roles, tenant lifecycle, keys, quotas, and
audit.
---
# Admin API
The JSON Admin API lives at `/admin/api/v1/` on the admin listener (never the public S3 port). It is the control plane:
tenants, service accounts, scoped access keys, quotas, usage, and audit.
It reuses the exact in-process domain services the HTML admin UI uses (tenant lifecycle, the metadata repo, the SigV4
secret cipher, the auditor), so it cannot bypass any authorization, audit, retention, or quota gate.
For most apps, reach this API through a first-party SDK rather than calling it directly:
[Go](/sdks/go#pkg-lockwelladmin-the-admin-client), [Node](/sdks/node#adminclient-the-admin-client), or
[Java](/sdks/java#lockwelladminclient-the-admin-client). This page is the wire-level reference.
## Authentication
Authentication is by an admin API bearer token: a high-entropy secret minted offline with `lockwell admin-token create`,
stored only hashed (SHA-256), and distinct from S3 access keys. The wire form is prefixed `lwadm_…`. Send it as:
```text
Authorization: Bearer lwadm_…
```
* Token bootstrap is offline-only (`lockwell admin-token create` needs filesystem access, the same trust model as
`lockwell admin-create`). There is no JSON route to mint the first token.
* Bearer tokens are not sent automatically by browsers, so the API is for server-to-server use and carries no CSRF flow.
* Anonymous, unauthenticated, revoked, and expired tokens are denied with `401`.
* Every request, success and denial, writes an audit row through the existing auditor. Tokens are rate-limited per
token.
## RBAC roles
Authorization composes the token's RBAC role with an optional single-tenant scope. A tenant-scoped token cannot cross to
another tenant: a cross-tenant target is a `403`, never a `404` existence leak.
| Role | Can do |
| ---------- | -------------------------------------------------------------------------------- |
| `viewer` | read-only: list/get tenants, accounts, keys, quota, usage, audit |
| `operator` | the above plus create tenants/accounts/keys, set/clear quota, rotate/revoke keys |
| `owner` | the above plus the destructive tenant lifecycle: `disable` and `delete` |
## Routes
The server base is `/admin/api/v1`. `GET /healthz` and `GET /openapi.json` are unauthenticated; everything else requires
a valid admin token.
### Tenants
| Method + path | Role | Purpose |
| ---------------------------- | -------- | ------------------------------------------------------- |
| `GET /tenants` | viewer | list tenants (global token = all; scoped = its tenant) |
| `POST /tenants` | operator | create a tenant |
| `GET /tenants/{id}` | viewer | get one tenant |
| `POST /tenants/{id}/disable` | owner | disable a tenant; `reason` required |
| `POST /tenants/{id}/delete` | owner | delete a disabled tenant; `reason` + `confirm` required |
`disable` revokes the tenant's active access keys. `delete` requires the tenant to be disabled first, requires `confirm`
to equal the tenant id, and fails closed with a `412` when a retention window or legal hold gates the delete.
### Accounts, keys, quota, usage
| Method + path | Role | Purpose |
| ---------------------------------------- | ----------------- | ------------------------------------------- |
| `GET /tenants/{id}/accounts` | viewer | list service accounts |
| `POST /tenants/{id}/accounts` | operator | create a service account |
| `GET /tenants/{id}/keys` | viewer | list access keys (secrets never returned) |
| `POST /tenants/{id}/keys` | operator | create an access key (secret returned once) |
| `POST /tenants/{id}/keys/{keyId}/rotate` | operator | rotate a key (new secret returned once) |
| `POST /tenants/{id}/keys/{keyId}/revoke` | operator | revoke a key; `reason` required |
| `GET\|PUT\|DELETE /tenants/{id}/quota` | viewer / operator | get / set / clear the tenant quota |
| `GET /tenants/{id}/usage` | viewer | storage usage report |
The secret on a created or rotated key is shown exactly once and is never recoverable afterward; persist it immediately.
`listKeys` returns only metadata.
The key request body accepts a `scopes` string (verb list `read,write,delete,admin`, or the resource form
`op=read:bucket=reports:prefix=in/,op=write:bucket=reports:prefix=in/`) and an optional `expiresAt` (RFC3339 or
`YYYY-MM-DD`).
::: warning A created or rotated key returns its secret exactly once. Capture it from the response immediately; there is
no endpoint that reads it back. :::
### Audit
| Method + path | Role | Purpose |
| ---------------------------------- | ------ | ------------------- |
| `GET /audit?tenant=&since=&limit=` | viewer | query the audit log |
`since` is a Go duration string (e.g. `24h`); `limit` is clamped to `[1, 1000]` (default 100). A tenant-scoped token is
forced to its own tenant regardless of the `tenant` parameter; an explicit cross-tenant `tenant` from a scoped token is
a `403`.
## `reason` / `confirm` and dry runs
Destructive operations are gated on the wire:
* `reason` is required on `disable`, `delete`, and `revoke` (a `400` otherwise).
* `confirm` must equal the tenant id on `delete` (a `400` otherwise).
* `?dryRun=true` on any mutation returns the plan the call would execute and applies nothing. For tenant
`disable`/`delete` the plan enumerates the buckets, objects, versions, delete markers, legal-held and retained
versions, access keys, and physical bytes that would be affected, so you can preview an offboarding before committing
it.
```text
POST /admin/api/v1/tenants/acme/delete?dryRun=true
Authorization: Bearer lwadm_…
Content-Type: application/json
{ "reason": "offboarding", "confirm": "acme" }
```
The SDKs surface this directly: every mutation takes a `dryRun` option (Go/Node) or has a `…DryRun` twin (Java). See
[tenancy and auth](/guide/tenancy-and-auth).
## Error shape
Failures return an RFC-7807-style JSON problem with a stable `code`, `message`, `status`, and `requestId` (also echoed
in `X-Request-Id`), so an operator can correlate a failure with its audit row:
```json
{
"code": "precondition_failed",
"message": "tenant has legal-held object versions",
"status": 412,
"requestId": "req_01HXY…"
}
```
Status mapping:
| Status | Meaning |
| ------ | ------------------------------------------------------------ |
| `400` | validation (missing `reason`, bad `confirm`, malformed body) |
| `401` | missing/invalid/revoked/expired admin token |
| `403` | RBAC role or cross-tenant scope denial |
| `404` | target not found |
| `412` | retention / legal-hold gated tenant delete (fail-closed) |
| `429` | per-token rate limit exceeded |
## OpenAPI
The full machine-readable contract is an OpenAPI 3 document, available two ways:
* Served live at `GET /admin/api/v1/openapi.json` (unauthenticated, since an operator needs the contract before holding
a token; it leaks no tenant data).
* Committed at
[`internal/adminapi/openapi.json`](https://github.com/RusticStack/lockwell/blob/main/internal/adminapi/openapi.json)
(the canonical source; a human-readable YAML twin lives alongside at `internal/adminapi/openapi.yaml`).
Each operation carries a unique `operationId` (`listTenants`, `createTenantKey`, `queryAuditLog`, …), so you can
generate a client in any language with `openapi-generator`. Prefer the first-party Go/Node/Java admin clients; codegen
is the path for every other language.
## Not exposed here
This JSON API mirrors the tenant / account / key / quota / audit subset of the admin surface. Operational controls that
live on the HTML admin UI (encryption-key rotation and rewrap, lifecycle, repair/scrub, placement, backup/restore) are
not part of the JSON Admin API.
There is also no public-access, bucket-policy, or notification configuration surface here. Notifications are configured
on the [native data plane](/reference/native-api#bucket-event-notifications).
## Interactive reference
Every admin operation below is generated from the OpenAPI document, so it always matches the shipped server. Expand an
operation for its parameters, request and response schemas, and copy-paste samples. Send the admin token as
`Authorization: Bearer `. The example host is a placeholder; swap in your own admin endpoint.
---
---
url: /reference/sdk-capabilities.md
description: >-
Machine-checkable capability and public-symbol index for Lockwell's Go,
Node/TypeScript, Java, Native Wire, and merged server framework integrations.
---
# SDK capability index
This page is the human-readable companion to [`/sdk-capabilities.json`](/sdk-capabilities.json). Source and executable
tests are authoritative; “supported” means a public SDK method exists and the repository exercises its request shape or
behavior. Live provider/replacement evidence remains a separate release gate.
The shared language-neutral snippets and security notes are available as
[`/sdk-public-api-examples-v1.json`](/sdk-public-api-examples-v1.json).
## Shared capability map
| Capability | Go | Node/TypeScript | Java | Notes |
| -------------------------------- | --------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | ----------------------------------------- |
| Endpoint prefixes / path style | `New` | `new Client` | `LockwellClient.builder` | Prefix participates in signing/presigning |
| Explicit S3 region | `WithRegion` | `region` | `.region` | Default `us-east-1` |
| Custom TLS transport / CA | `WithHTTPClient` | `fetch` / `createNodeFetch` | `.httpClient` | Verification stays enabled |
| Attempt timeout / cancellation | `WithRequestTimeout`, context | `timeoutMs`, `AbortSignal` | `.requestTimeout`, future cancellation | Defaults differ by surface/language |
| Retry policy | `RetryPolicy` | `RetryPolicy` | `RetryPolicy` | Only replay-safe requests |
| Progress | `ProgressFunc` | `onProgress` / `TransferProgress` | `ProgressListener` / `TransferProgressListener` | Callback failure cancels |
| Response correlation | `WithResponseMetadata` | `onResponseMetadata` | `.responseMetadataListener` | Request ids + optional traceparent |
| Bucket CRUD / versioning | all three surfaces | all three surfaces | all three surfaces | Admin is not an object client |
| Object CRUD / ranges / streaming | S3 + native | S3 + native | S3 + native + async S3 | Close streaming downloads |
| Conditional create | S3 `WithPutIfNoneMatch`, native `IfNoneMatch` | S3/native `ifNoneMatch` | S3 `.ifNoneMatch`, native `.ifAbsent` | Native also supports overwrite-by-ETag |
| Checksums | five S3 algorithms + native maps | five S3 algorithms + native maps | five S3 algorithms + native maps | Per-part/composite supported |
| Multipart / list / abort | S3 + native | S3 + native | S3 + native + async S3 | Streaming parts supported |
| SSE-S3 / SSE-C | typed S3 options | typed S3 options | typed S3 overloads | SSE-C is S3-only; SSE-KMS unsupported |
| Tagging / versions | S3 + native | S3 + native | S3 + native + async S3 | Includes delete markers |
| Retention / legal hold | get/set/put | S3 reads + native get/set | typed S3 get/set + native get/set | No governance bypass |
| S3 presign | GET/PUT/HEAD/DELETE | GET/PUT/HEAD/DELETE | GET/PUT/HEAD/DELETE | Query-SigV4 |
| Native signed URLs | GET/PUT constraints | GET/PUT + prefix specialization | GET/PUT constraints | No bearer token on use |
| CORS / webhook notification | native | native + kit | native + kit | Webhook only; no SNS/SQS/Lambda |
| Health / readiness | native + admin | native + admin | native + admin sync/async | Credential-free probes |
| Tenant/key/quota/usage/audit | admin | admin | admin | Dry-run where server supports it |
| App kit / ERP helpers | `lockwellkit` helpers | `LockwellKit`, `ErpScopes`, `StorageProfiles`, `ErpErrors` | `LockwellKit`, `ErpScopes`, `StorageProfiles`, `ErpErrors` | Pure helpers do not choose policy |
| Edge runtime | not applicable | `/edge` native/admin/kit/helpers | not applicable | S3 client intentionally omitted |
The typed SSE-C helpers are implemented on the S3 surface. The merged metadata remediation keeps user metadata lossless
and user-owned (including historical reserved names), while internal SSE-C and Object Lock state is typed and separate.
Capability presence still does not grant SSE-C to the native wire; the effective LNW server mask excludes it.
## Native Wire transport map
LNW/1 is an experimental, opt-in, disabled-by-default transport for the native data plane. It is not a fourth domain
surface and does not change the S3 or HTTP-native JSON contracts.
| Consumer | Package / entry | Runtime floor | Qualified scope | Explicit absences |
| --- | --- | --- | --- | --- |
| Shared TypeScript client | `@kelphect/sdk-native@0.1.0` (`/node`, `/bun`, `/protocol`, `/testing`) | Node 22+, Bun 1.4+ | raw TLS 1.3 LNW/1; server-only | browser/default, SSE-C, ADMIN, fallback |
| SolidStart adapter | `@kelphect/sdk-solidstart@0.1.0` (`/server`, `/node`, `/bun`) | Node 22+ or Bun 1.4+ server; build toolchain Node 24+ | SolidStart v2; Nitro `node_server`, `node_cluster`, `bun` | edge/static/unknown presets, SSE-C, ADMIN, fallback |
| Next.js adapter | `@kelphect/sdk-nextjs@0.1.0` (root `react-server`/`node`) | Next.js 16.3.3–16.x; Node 22+, Bun 1.4+ | App Router server components/actions/Node Route Handlers; standalone/container and representative Node serverless | Client/browser, Edge/middleware, SSE-C, ADMIN, fallback |
| Spring starter | `com.lockwell:lockwell-spring-boot-starter:0.2.2` | JDK 25; Spring Boot 4.1.1 tested | lifecycle sync/virtual-thread async, streams, Object Lock, tags, multipart, CORS, notifications, signed capabilities | SSE-C, ADMIN, browser/edge, fallback |
The Go standalone LNW client, Node primary-LNW transport, Java shared-core LNW client, and Nuxt adapter
remain open source-owner work and are deliberately not listed as supported consumers until their PRs merge and their exact
cross-language gates pass. See the [Native Wire guide](/guide/native-wire) and [wire reference](/reference/native-wire).
## Go public namespaces
* `pkg/lockwellsdk`: `Client`, `Credentials`, `APIError`, `RetryPolicy`, `Progress`, `ResponseMetadata`, bucket/object/
copy/list/paginator/multipart/tagging/Object Lock/checksum types, all `With*` option functions, and four presign
methods.
* `pkg/lockwellnative`: `Client`, `NativeError`, health types,
object/list/version/tag/retention/legal-hold/multipart/CORS/ notification/signed-URL types and iterators.
* `pkg/lockwelladmin`: `Client`, `AdminError`, health, tenant/account/key/quota/usage/audit inputs and results.
* `pkg/lockwellkit`: `Kit`, provisioning/ensure/key-scope/signed-URL/webhook helpers and ERP mapping/profile
classifiers.
The complete method signatures live on the [Go SDK page](/sdks/go) and in package documentation generated from source.
Additional exported result, option, iterator, health, progress, and ERP helper symbols are indexed here so a source
addition cannot silently outrun the website:
```text
BatchDeleteError BatchDeleted CallbackErr ClassifyERPError DataRightsDownloadURL DataRightsExportWrite
DeleteError DeletedObject ERPErrorCategory ERPErrorClassification ERPErrorSurface ERPErrorSurfaceAdmin
ERPErrorSurfaceNative ERPErrorSurfaceUnknown ERPKeyInput ERPObjectWriteRecipe ERPPurpose ERPPurposeDataRights
ERPPurposeExports ERPPurposeFiscalArchive ERPPurposeImports ERPPurposePath ERPPurposePathFor
ERPPurposeSupportBundles ERPRetentionSpec ERPRetryDecision ERPRetryDoNotRetry ERPRetryOperatorAction
ERPRetryRefreshCredentials ERPRetryWithBackoff ERPScopedClauses ERPSignedDownloadRecipe ERPSignedUploadRecipe
ERPTenantMapping EnsureBucket EnsureKey EnsureKeyInput EnsureKeyResult EnsureTenant EnsureTenantProvisioning
EnsureTenantProvisioningInput EnsureTenantProvisioningResult ErrorBody ExportDownloadURL FiscalArchiveBucketInput
FiscalArchiveWrite GetObjectWithProgress GoString HealthStatus ImportObjectWrite ImportUploadURL LegalHoldOff LegalHoldOn
MultipartUploadEntry NewERPTenantMapping ObjectEntry PartListItem ProfileDataRights ProfileExports ProfileFiscalArchive
ProfileImports ProfileMetadataKey ProfileSupportBundles ReadinessComponent ReadinessStatus RedactedSupportBundleWrite
ResponseMetadataFunc RetentionCompliance SignURLWithResult SignedDownloadURLInput SignedDownloadURLWithResult SignedURL
TotalKnown URLForKey VersionEntry WithGetObjectProgress WithGetProgress WithLegalHold WithPartNumberMarker
WithUploadIDMarker WithUploadsDelimiter WithUploadsKeyMarker
```
The merged admin/consensus additions are also public Go symbols and are intentionally indexed here:
```text
ConsensusChange ConsensusLifecycleInput ConsensusMember ConsensusMemberInput ConsensusReplaceInput
DrainConsensusMember ExecuteConsensusRebalance GetConsensusMembership JoinConsensusMember RebalanceInput
RebalancePlan RebalanceResult RebalanceSummary RemoveConsensusMember ReplaceConsensusMember VersioningDisabled
```
## Node/TypeScript exports
The default entry exports `Client`, `NativeClient`, `AdminClient`, `LockwellKit`, typed error classes and predicates,
`RetryPolicy`, `TimeoutError`, checksum helpers/constants, `buildPresignedGetUrl`, `buildPresignedObjectUrl`,
`urlForKey`, `createNodeFetch`, `verifyWebhook`, `WEBHOOK_SIGNATURE_HEADER_NAME`, `ErpScopes`, `StorageProfiles`,
`ErpErrors`, their constants, and standalone helper functions. `/edge` exports the native/admin/kit/ERP/retry/WebCrypto
subset and omits the S3 client, Node checksum/presign helpers, and Node transport.
See [Node SDK](/sdks/node) for method tables and runtime ownership rules.
The named constant and standalone-helper exports are:
```text
ERP_ERROR_CATEGORIES ERP_ERROR_SURFACES ERP_PURPOSES ERP_RETRY_DECISIONS LEGAL_HOLD_OFF LEGAL_HOLD_ON
PROFILE_METADATA_KEY RETENTION_COMPLIANCE STORAGE_PROFILES classifyAdmin classifyNative companyPrefix
complianceRetention legalHoldEnabled retentionSpec withLegalHold
```
## Java public packages
* `com.lockwell.sdk`: `LockwellClient`, `LockwellAsyncClient`, `Credentials`, `ApiException`, `RetryPolicy`,
`ResponseMetadata`, `Progress`, `TransferProgress`, `Checksums`, `Presign`, `SigV4Signer`, `EndpointPath`, retention/
legal-hold enums, paginator, and health result types.
* `com.lockwell.sdk.nativeapi`: `LockwellNativeClient`, `NativeTypes`, `NativeException`, and JSON helpers used by the
public native types.
* `com.lockwell.sdk.admin`: `LockwellAdminClient`, `AdminTypes`, and `AdminException`.
* `com.lockwell.sdk.kit`: `LockwellKit`, `KitTypes`, `ErpScopes`, `StorageProfiles`, and `ErpErrors`.
* `com.lockwell.sdk.spring`: `LockwellProperties` and `LockwellAutoConfiguration` for S3 sync/async beans.
See [Java SDK](/sdks/java) and [Java native production guide](/sdks/java-native).
Public nested/result types that are easy to miss in narrative guides are:
```text
CompleteChecksumResult CompleteMultipartResult CompleteResult CreateMultipartResult DeleteError DeleteMarkerEntry
DeletedEntry DeletedObject Direction EnsureKeyResult ListEntry ObjectEntry ObjectLockUpdateResult ObjectWriteRecipe
PartEntry RetentionMode RetentionResult RetentionSpec SignedDownloadRecipe SignedUploadRecipe SignedUrlMethod TenantMapping
UploadPartResult VersionEntry
```
## `@kelphect/sdk-native` public surface
The server-only TypeScript LNW package exports `LockwellNativeClient`, `createLockwellClient`, `Credentials`,
`CredentialProvider`, `TLSOptions`, `NativeClientConfig`, `RequestOptions`, `MetricsSink`, `TraceSink`, `LogSink`,
`UserMetadataEntry`, `ResponseMetadata`, `ClientDiagnostics`, bucket/object/version/multipart/tag/retention/legal-hold/
CORS/notification/signed-capability request and result types, and the stable `LockwellError` subclasses
`ProtocolError`, `TransportError`, `AuthenticationError`, `AuthorizationError`, `ServiceError`, `CancelledError`,
`DeadlineExceededError`, `ClientClosedError`, and the redaction helper `redact`. The `/protocol` entry exports `FrameType`,
`Operation`, `Capability`, `CLIENT_CAPABILITIES`, `FieldNumber`, `ErrorCode`, `DEFAULT_LIMITS`, `MAGIC`, `VERSION`,
`HEADER_BYTES`, and `CRC_BYTES`; `/testing` exports codec fixtures and the injectable socket boundary. See [Native
TypeScript](/sdks/bun-native) for runnable usage and package checks.
## `@kelphect/sdk-solidstart` public surface
The SolidStart v2 adapter exports the Vite guard (`lockwellSolidStart`, `validateSolidStartRuntime`), runtime refusal
error (`UNSUPPORTED_RUNTIME_CODE`, `UnsupportedSolidStartRuntimeError`), server-only lifecycle/configuration helpers,
`createLockwellSolidStartClient`, `createNodeLockwellSolidStartClient`, and
`createBunLockwellSolidStartClient`, request/response route helpers (`bindLockwellRequest`,
`createObjectRouteHandlers`, `getObjectResponse`, `headObjectResponse`, `putObjectResponse`, `uploadPartResponse`),
`errorResponse`, `parseSingleRange`, and server-function/action helpers (`requireSolidStartRequest`,
`runCurrentLockwellServerFunction`, `runLockwellAction`). Configuration helpers include
`resolveLockwellSolidStartServerConfig`, `redactLockwellSolidStartServerConfig`, and
`InvalidLockwellSolidStartConfigError` (`INVALID_SOLIDSTART_CONFIG_CODE`); lifecycle state uses
`LockwellSolidStartClientLifecycle`, `LockwellSolidStartConfigCollisionError` (`SOLIDSTART_CONFIG_COLLISION_CODE`),
and the testing reset helper. Request-event integration exposes `MissingSolidStartRequestEventError`
(`NO_SOLIDSTART_REQUEST_CODE`). It supports only Node 22+/Bun 1.4+ server targets and Nitro
`node_server`/`node_cluster`/`bun`; browser, edge, static, and unknown targets fail closed. See
[SolidStart v2](/sdks/solidstart).
## `@kelphect/sdk-nextjs` public surface
The merged Next.js adapter exports `NextLockwellAdapter`, `createNextLockwell`, `getNextLockwell`,
`disposeNextLockwell`, `lockwellConfigFromEnv`, `LockwellSecret`, `lockwellDeploymentConfig`, and
`lockwellDeploymentDiagnostics`. Route helpers include `putRequest`, `getResponse`, and `headCached`; cache helpers are
`lockwellBucketTag`, `lockwellObjectTag`, `expireLockwellObject`, and `revalidateLockwellObject`. Request/error and
observability helpers are `lockwellRequestContext`, `safeLockwellError`, `lockwellErrorResponse`,
`registerLockwellInstrumentation`, and `lockwellTraceSink`. The adapter re-exports the typed shared native client
operations through `adapter.client`. It supports Next.js 16.3.3–16.x on Node 22+ or Bun 1.4+ Node-compatible servers;
Client Components, browser bundles, Edge, and middleware fail closed. See [Next.js 16.3](/sdks/nextjs) and the
[implementation README](https://github.com/RusticStack/lockwell/blob/main/sdk/nextjs/README.md).
Advanced typed/configuration exports are `CacheMutation`, `GetResponseOptions`, `PutRequestOptions`,
`LockwellCacheOptions`, `LockwellEnvironment`, `NextLockwellConfig`, `NextLockwellCredentialProvider`,
`NextLockwellTLSOptions`, and `SafeLockwellError`. Error and lifecycle classes are `LockwellConfigurationError`,
`LockwellRequestError`, and `UnsupportedLockwellRuntimeError`; runtime helpers are `assertLockwellNodeRuntime`,
`assertLockwellServerRuntime`, `resolveLifecycle`, `resolveNativeConfig`, and `cachedHeadObject`. The lifecycle type is
`LockwellLifecycle` (`"auto"`, `"long-lived"`, or `"serverless"`).
## Spring Boot Native Wire public surface
The opt-in starter publishes `com.lockwell.sdk.springwire.LockwellNativeWireClient`,
`LockwellNativeWireAsyncClient`, `LockwellNativeWireHealthIndicator`, `LockwellNativeWireProperties`,
`NativeWireTypes`, `ResponseMetadata`, `TransferProgressListener`, and `LockwellWireException`. It is JDK 25-first,
Spring Boot 4.1.1-tested, and exposes sync/virtual-thread async streaming, Object Lock, checksums, multipart, tags,
CORS, webhook, and signed-capability operations. SSE-C and the reserved Admin wire surface are absent. See [Spring Boot
Native Wire](/sdks/java-spring-wire).
## Deliberate absences
There are no first-party .NET, Rust, PHP, or Ruby SDKs. Those languages are explicit product non-goals; do not use
an experimental branch or closed proposal as a production
claim. There are no public SDK methods for public buckets, anonymous reads, IAM/STS, SSE-KMS, bucket-policy editing,
website hosting, Select, Lambda/Object Lambda, tiering, or arbitrary notification targets. CLI/Web UI-only operator
workflows are not silently represented as SDK methods.
---
---
url: /reference/content-provenance.md
description: >-
Source authority, licensing, generated artifacts, canonical URLs, and
machine-fetchable formats for Lockwell's public technical documentation.
---
# Documentation provenance and licensing
Lockwell's public documentation is maintained beside the implementation and executable tests in the
[`RusticStack/lockwell`](https://github.com/RusticStack/lockwell) repository. Source and tests are authoritative when a
page, generated index, or older deployment disagrees with them. A successful pull-request preview is evidence for that
commit only; production may remain on an earlier commit until the change merges and deploys.
## Content license
The documentation and examples are covered by the repository's
[PolyForm Noncommercial 1.0.0 license](https://github.com/RusticStack/lockwell/blob/main/LICENSE). Commercial use,
including TangibleShift integration, requires a written grant from the rights holder. Third-party names, trademarks,
linked specifications, and dependencies retain their respective rights. See the repository's
[third-party license inventory](https://github.com/RusticStack/lockwell/blob/main/docs/third-party-licenses.md).
Code samples on this site explain the corresponding Lockwell SDK or protocol contract. They do not change the license,
publish a package, grant production approval, or expand an API beyond the tested source.
## Canonical and fetchable representations
Every documentation page has one canonical HTML URL and a same-path `.md` representation. The HTML is statically
pre-rendered with its primary heading and article content, so JavaScript is not required to read it. These discovery
artifacts are stable entry points:
* [`/documentation-index.json`](/documentation-index.json): formats, indexes, source authority, and license metadata.
* [`/sdk-capabilities.json`](/sdk-capabilities.json): machine-readable shipping SDK capability index.
* [`/llms.txt`](/llms.txt): concise page catalog for agents.
* [`/llms-full.txt`](/llms-full.txt): combined Markdown corpus.
* [`/sitemap.xml`](/sitemap.xml): canonical crawl inventory.
* [`/robots.txt`](/robots.txt): crawler policy and discovery links.
Generated native/admin OpenAPI and protocol indexes are linked from the documentation index. Their source commit must
match the site preview or deployment being evaluated.
## Generation and verification
The VitePress build copies committed machine-readable contracts, generates HTML, sitemap, raw Markdown, and LLM corpora,
then validates links, canonical URLs, descriptions, semantic headings, deterministic unique anchors, JSON-LD, and
discovery coverage. A separate fetch smoke sends browser, command-line, search-crawler, and AI-crawler user agents to
the same routes and requires equivalent public content; no crawler receives a privileged or weakened-security path.
## Reporting drift
Report a documentation mismatch with the page URL, source commit, exported symbol or operation, and the executable test
that disagrees. Never include access keys, bearer tokens, customer encryption keys, signed URL query strings, object
payloads, or private tenant metadata in an issue.
---
---
url: /benchmarks.md
description: >-
The full benchmark ledger as an explorable table. Lockwell vs MinIO with our
open harness and with MinIO's own tool (warp), every operation, size, and
concurrency, throughput and latency views, the caveats that matter, and the
commands that reproduce every row.
---
# Benchmarks
The promoted public matrix is **`20260810T105308Z-f3fd294`**. It ran on the project's Oracle Ampere A1 ARM64 host and
contains 51 rows each for Lockwell, MinIO, Garage, and SeaweedFS, zero request errors, and five completed
repair/scrub/backup/restore drills. Its generated competitive gate has **20 failures**, so the promoted evidence does
not support a performance-leadership or provider-replacement claim. The immutable manifest, hashes, raw rows, profile,
and gate are in `benchmark-baselines/phase1/`.
The latest complete unchanged-policy diagnostic is `bench-results/20260815T-full-access-log-23194cf`, bound to merged
`23194cf2dcbc93bad904d1743d82c365f9b4fe4d`; it reduced the generated gate to **10 failed checks out of 142** with the
same four targets and five drills, but it is not committed or promoted. Until a reviewed run is promoted, the tracked
baseline above remains the public release ledger and B-001 remains active.
The explorer below is the historical Lockwell-vs-MinIO presentation dataset. It remains useful for navigating operation,
size, concurrency, throughput, and latency dimensions, but it is not the current four-target release ledger. Read the
[caveats](#caveats) before quoting anything; they are part of the result.
::: tip How to read latency Throughput (MiB/s, ops/s) is "how much per second": **higher is better**. The p50 and p95
views are **response times in milliseconds**: p50 is the median request, p95 the slow tail, and **lower is better**. A
Lockwell p50 at half of MinIO's means Lockwell answers twice as fast. The Advantage column already does this arithmetic
for you, in the right direction, on every view. :::
## The ledger
## Caveats
These are part of the result, not footnotes to hide.
* **Durability tier.** The bench configuration runs Lockwell in its grouped-durability tier (the write-ahead log is
fsynced every 10 ms, not per commit, matching Garage's model; a power loss can cost up to ~10 ms of acknowledged
writes). MinIO runs its defaults, which sync per operation. Lockwell's default tier is strict per-commit sync; if your
threat model requires it, benchmark that tier instead. This asymmetry flatters Lockwell most on small-object PUT,
which is exactly where the warp gap is largest.
* **Current host.** The promoted four-target baseline ran server and client containers on one Oracle Ampere A1 ARM64
host (four cores, about 24 GiB RAM). Absolute results are host- and image-specific; the complete profile ships so the
matrix can be rerun rather than generalized to unrelated hardware.
* **MinIO version.** Each run pulls `minio/minio:latest` at run time. Version-to-version variance is real, so cross-run
comparisons of old tables mix that in.
* **CPU.** Lockwell sustains the higher throughput while using more CPU than MinIO at peak. It trades compute for
throughput and disk; if you are CPU-bound, weigh that.
* **Failed rows remain visible.** The promoted baseline records 20 local-leadership failures across GET, HEAD, LIST,
mixed-RW, multipart PUT, and PUT; the latest complete diagnostic records 10. `BLOCKERS.md` lists the exact tuples,
and the generated gate remains authoritative for each artifact. Neither result clears B-001.
* **Storage.** Core throughput runs disable compression, deduplication, and encryption for an apples-to-apples engine
comparison. Feature-profile storage-efficiency results are separate and must not be substituted for the core matrix.
## Neutral hardware
Dev-machine numbers carry dev-machine noise. The standing plan is to run the same two harnesses on a fresh low-cost
cloud box (the EUR 5 Hetzner class Lockwell is designed to fit), where nothing else is running, the exact specs are
public, and anyone can rent the identical machine and check.
[`scripts/bench-remote-hetzner.sh`](https://github.com/RusticStack/lockwell/blob/main/scripts/bench-remote-hetzner.sh)
provisions the server with `hcloud`, runs `make bench` and `make bench-warp`, copies the evidence back, and destroys the
box; the dataset selector above grows a new entry whenever such a run lands.
## Reproduce
```sh
make bench # the full matrix harness (Lockwell, MinIO, Garage, SeaweedFS)
make bench-warp # MinIO's warp against Lockwell and MinIO on the same stack
```
Both write raw per-run evidence under ignored `bench-results/`. Promote a completed matrix with
`go run scripts/promote-benchmark-baseline.go ...`; only its validated allowlist is committed under
`benchmark-baselines/phase1/`. The historical explorer is regenerated with
`node website/scripts/build-bench-data.mjs `. The methodology and regression thresholds live in
[docs/benchmark-baselines.md](https://github.com/RusticStack/lockwell/blob/main/docs/benchmark-baselines.md).
---
---
url: /es.md
description: >-
Almacenamiento de objetos privado, multi-tenant y cifrado, compatible con S3,
con SDKs oficiales para Go, Node y Java. Cada afirmación es una medición, y el
repositorio incluye las herramientas para repetirla.
---
Lockwell es almacenamiento de objetos privado, multi-tenant y cifrado, self-hosted en un solo binario. Expone tres APIs
sobre el mismo almacén: una API compatible con S3 (SigV4, XML), una API JSON nativa en `/api/v1/` y una API de
administración JSON en `/admin/api/v1/` en un listener privado. Los SDKs oficiales para Go, Node y Java cubren las tres,
y un app kit se encarga del aprovisionamiento de tenants, claves con alcance, subidas firmadas desde el navegador y
verificación de webhooks.
Lockwell está disponible como software source-available y self-hosted. El servicio gestionado es solo una lista de
espera cualificada: todavía no ofrece almacenamiento alojado, pagos, asignación ni SLA.
Medición histórica (2026-06-11, una máquina, Docker Compose; reproducir con `make bench` y
`make prod-authority-test`):
* Recuperación tras crash en 16 segundos con todas las escrituras confirmadas intactas.
* 0,54x el disco que usa MinIO para los mismos bytes escritos.
* 2,2x el rendimiento de PUT multiparte de MinIO a 64 MiB con 64 clientes concurrentes.
* 0 fallos en 24 clientes SDK de S3 en la suite de compatibilidad de producción.
Estas filas fechadas no son una afirmación actual de sustitución de proveedor. La prueba competitiva generada más
reciente aún tiene fallos; consulta el [registro de benchmarks](/benchmarks) antes de decidir una migración.
Rechazado por diseño: buckets públicos, lecturas anónimas, SSE-KMS sin un KMS, y fallbacks silenciosos en llamadas S3 no
soportadas. Todo lo que queda fuera de la superficie documentada falla cerrado. Compartir de forma segura se hace con
enlaces firmados que caducan. La lista completa, con alternativas para cada rechazo, está en
[Cuándo no usar Lockwell](/guide/when-not-to-use).
Empieza en [Getting started](/guide/getting-started), elige un SDK en [SDKs](/sdks/) o consulta los
[benchmarks](/benchmarks). La documentación técnica está, por ahora, en inglés.
---
---
url: /pt.md
description: >-
Armazenamento de objetos privado, multi-tenant e cifrado, compatível com S3,
com SDKs oficiais para Go, Node e Java. Cada afirmação é uma medição, e o
repositório traz as ferramentas para a repetir.
---
O Lockwell é armazenamento de objetos privado, multi-tenant e cifrado, self-hosted num único binário. Expõe três APIs
sobre o mesmo armazenamento: uma API compatível com S3 (SigV4, XML), uma API JSON nativa em `/api/v1/` e uma API de
administração JSON em `/admin/api/v1/` num listener privado. Os SDKs oficiais para Go, Node e Java cobrem as três, e um
app kit trata do provisionamento de tenants, chaves com âmbito, uploads assinados no browser e verificação de webhooks.
O Lockwell está disponível como software source-available e self-hosted. O serviço gerido é apenas uma lista de espera
qualificada: não oferece ainda armazenamento alojado, pagamentos, alocação ou SLA.
Medição histórica (2026-06-11, uma máquina, Docker Compose; reproduzir com `make bench` e
`make prod-authority-test`):
* Recuperação após crash em 16 segundos com todas as escritas confirmadas intactas.
* 0,54x o disco que o MinIO usa para os mesmos bytes escritos.
* 2,2x o débito de PUT multipart do MinIO a 64 MiB com 64 clientes concorrentes.
* 0 falhas em 24 clientes SDK S3 na suite de compatibilidade de produção.
Estas linhas datadas não constituem uma afirmação atual de substituição de fornecedor. O gate competitivo gerado mais
recente ainda tem falhas; consulte o [ledger de benchmarks](/benchmarks) antes de decidir uma migração.
Recusado por desenho: buckets públicos, leituras anónimas, SSE-KMS sem um KMS, e fallbacks silenciosos em chamadas S3
não suportadas. Tudo fora da superfície documentada falha fechado. A partilha segura faz-se com links assinados que
expiram. A lista completa, com alternativas para cada recusa, está em
[Quando não usar o Lockwell](/guide/when-not-to-use).
Comece em [Getting started](/guide/getting-started), escolha um SDK em [SDKs](/sdks/), ou consulte os
[benchmarks](/benchmarks). A documentação técnica está, por agora, em inglês.
---
---
url: /de.md
description: >-
Privater, multi-tenant, verschlüsselter, S3-kompatibler Objektspeicher mit
offiziellen SDKs für Go, Node und Java. Jede Aussage ist eine Messung, und das
Repository liefert die Werkzeuge, um sie zu wiederholen.
---
Lockwell ist privater, multi-tenant, verschlüsselter Objektspeicher, self-hosted in einem einzigen Binary. Es stellt
drei APIs über demselben Speicher bereit: eine S3-kompatible API (SigV4, XML), eine native JSON-API unter `/api/v1/` und
eine JSON-Admin-API unter `/admin/api/v1/` an einem privaten Listener. Offizielle SDKs für Go, Node und Java decken alle
drei ab, und ein App-Kit übernimmt Tenant-Provisionierung, Schlüssel mit Geltungsbereich, signierte Browser-Uploads und
Webhook-Verifikation.
Lockwell ist als source-available, selbst gehostete Software verfügbar. Der verwaltete Dienst ist nur eine qualifizierte
Warteliste: gehosteter Speicher, Zahlung, Zuteilung und SLA werden noch nicht angeboten.
Historische Messung (2026-06-11, ein Host, Docker Compose; reproduzieren mit `make bench` und
`make prod-authority-test`):
* Crash-Übernahme in 16 Sekunden, alle bestätigten Schreibvorgänge intakt.
* 0,54x des Plattenplatzes, den MinIO für dieselben geschriebenen Bytes braucht.
* 2,2x MinIOs Multipart-PUT-Durchsatz bei 64 MiB mit 64 gleichzeitigen Clients.
* 0 Fehler über 24 S3-SDK-Clients in der Produktions-Kompatibilitätssuite.
Diese datierten Zeilen sind keine aktuelle Anbieter-Ersatzbehauptung. Das neueste generierte Wettbewerbs-Gate enthält
weiterhin Fehler; vor einer Migrationsentscheidung das aktuelle [Benchmark-Ledger](/benchmarks) prüfen.
Verweigert by design: öffentliche Buckets, anonyme Lesezugriffe, SSE-KMS ohne KMS und stilles Durchwinken nicht
unterstützter S3-Aufrufe. Alles außerhalb der dokumentierten Oberfläche schlägt geschlossen fehl. Sicheres Teilen
funktioniert über signierte Links mit Ablaufzeit. Die vollständige Liste, mit Alternativen je Verweigerung, steht in
[Wann Lockwell nicht passt](/guide/when-not-to-use).
Start bei [Getting started](/guide/getting-started), SDK-Wahl unter [SDKs](/sdks/), oder die [Benchmarks](/benchmarks)
ansehen. Die technische Dokumentation ist vorerst auf Englisch.