# Getting started

Lockwell is an object store you host yourself. Your app talks to it through one SDK: files in, files out, per-customer
isolation built in.

::: info Early access
Lockwell is closed source during early access. The package versions on this page are historical builds; early-access
customers receive current packages, the deployment repository and a licence with their access. Request access at
[hello@rusticstack.com](mailto:hello@rusticstack.com). An MIT-licensed source release is planned.
:::

This page gets you to a working result: run the server, store a file, read it back, and let a browser upload directly.
Concepts can wait; when a term is new, [Concepts in plain words](/guide/concepts) defines it in a sentence.

## 1. Run Lockwell

One container, one volume, no external database.

```sh
git clone https://github.com/RusticStack/lockwell-deploy.git
cd lockwell-deploy
cp .env.example .env
docker compose up -d --build
```

Before `up`, open `.env` and replace the two `CHANGE_ME` values with long random strings. They become your first access
key, and the container refuses to start while the placeholders are still in place.

Check it is alive:

```sh
curl -fsS http://localhost:9000/health
```

You now have two listeners and one credential:

- `http://localhost:9000`: the public listener (object I/O)
- `http://localhost:9001`: the admin listener (management, kept private)
- the access key from `.env`, already bootstrapped with full data access

Already running Lockwell somewhere? Skip ahead and use your own endpoint and key.

## 2. Install the SDK

::: code-group

```sh [Node]
npm i @kelphect/sdk
```

```sh [Go]
go get github.com/RusticStack/lockwell-sdk-go/lockwellsdk@latest
```

```sh [Java]
# com.lockwell:lockwell-sdk on GitHub Packages. See Installation for the
# repository + auth setup.
```

:::

Registries, prerequisites, and client options live on the [Installation](/guide/installation) page.

This quickstart intentionally uses the HTTP-native JSON client. LNW/1 is an experimental, opt-in binary transport on a
separate TLS 1.3 listener; use the [Native Wire guide](/guide/native-wire) and its
[server-only SDK guides](/sdks/bun-native) after the listener and runtime have passed their qualification gates.

## 3. Store a file and read it back

Point the native client at the public listener with the key from `.env`. It mints and refreshes its own short-lived
token; you never touch auth plumbing.

::: code-group

```ts [Node]
import { NativeClient } from "@kelphect/sdk";

const native = new NativeClient({
  endpoint: "http://localhost:9000",
  accessKeyId: process.env.LOCKWELL_ROOT_ACCESS_KEY_ID,
  secretKey: process.env.LOCKWELL_ROOT_SECRET_KEY,
});

await native.createBucket("inbox");
await native.putObject("inbox", "hello.txt", "hi there", { contentType: "text/plain" });

const got = await native.getObject("inbox", "hello.txt");
console.log(got.body.toString()); // "hi there"
```

```go [Go]
package main

import (
 "context"
 "io"
 "log"
 "os"
 "strings"

 "github.com/RusticStack/lockwell-sdk-go/lockwellnative"
)

func main() {
 ctx := context.Background()

 native, err := lockwellnative.New("http://localhost:9000",
  os.Getenv("LOCKWELL_ROOT_ACCESS_KEY_ID"), os.Getenv("LOCKWELL_ROOT_SECRET_KEY"))
 if err != nil {
  log.Fatal(err)
 }

 if _, err := native.CreateBucket(ctx, lockwellnative.CreateBucketInput{Name: "inbox"}); err != nil {
  log.Fatal(err)
 }
 if _, err := native.PutObject(ctx, lockwellnative.PutObjectInput{
  Bucket: "inbox", Key: "hello.txt",
  Body: strings.NewReader("hi there"), ContentType: "text/plain",
 }); err != nil {
  log.Fatal(err)
 }

 obj, err := native.GetObject(ctx, lockwellnative.GetObjectInput{Bucket: "inbox", Key: "hello.txt"})
 if err != nil {
  log.Fatal(err)
 }
 defer obj.Close()
 body, _ := io.ReadAll(obj)
 log.Println(string(body)) // "hi there"
}
```

```java [Java]
import com.lockwell.sdk.nativeapi.LockwellNativeClient;
import com.lockwell.sdk.nativeapi.NativeTypes.GetResult;
import com.lockwell.sdk.nativeapi.NativeTypes.PutOptions;

var nativeClient = LockwellNativeClient.builder()
    .endpoint("http://localhost:9000")
    .accessKeyId(System.getenv("LOCKWELL_ROOT_ACCESS_KEY_ID"))
    .secretKey(System.getenv("LOCKWELL_ROOT_SECRET_KEY"))
    .build();

nativeClient.createBucket("inbox");
nativeClient.putObject("inbox", "hello.txt", "hi there".getBytes(),
    new PutOptions().contentType("text/plain"));

try (GetResult got = nativeClient.getObject("inbox", "hello.txt")) {
    got.body().transferTo(System.out); // "hi there"
}
```

:::

That is a private, encrypted object store taking writes. Streaming, ranges, listing, versions, and the rest of the
object API start at [Upload & download](/guide/data-operations).

## 4. Let the browser upload directly

Sign a short-lived PUT URL on your server and hand only the URL to the browser. The file bytes go straight to Lockwell,
and your key never leaves the server.

::: code-group

```ts [Node]
// Server-side: the signed URL comes back as a path; join it to the endpoint.
const signed = await native.signUrl({
  method: "PUT",
  bucket: "inbox",
  key: "photo.jpg",
  ttlSeconds: 300,
});
const url = new URL(signed.url, "http://localhost:9000").toString();

// Browser-side:
await fetch(url, { method: "PUT", body: file });
```

```go [Go]
// SignURL returns an absolute URL in Go.
url, err := native.SignURL(ctx, lockwellnative.SignURLInput{
 Method: "PUT", Bucket: "inbox", Key: "photo.jpg", TTLSeconds: 300,
})
if err != nil {
 log.Fatal(err)
}
// Return url to the browser, which PUTs the file body to it.
```

```java [Java]
// The signed URL comes back as a path; join it to the endpoint.
var signed = nativeClient.signUrlResult("PUT", "inbox", "photo.jpg", 300);
var url = java.net.URI.create("http://localhost:9000").resolve(signed.url()).toString();
// Return url to the browser, which PUTs the file body to it.
```

:::

The URL is bound to one method and one object, expires on its TTL, and can never exceed the signing key's scope. The
full flow, including the download direction, is on [Signed URLs](/guide/signed-urls).

## That's it

You ran the server, stored a file, read it back, and took a browser upload. One SDK, no second store, no glue code.

## Going multi-tenant

The quickstart used the bootstrap root key. For a real app you give each customer their own tenant and a key scoped to
it, so customers are isolated by the credential itself.

That is the job of the **admin API** and the **app kit** (`provisionTenant`, `clientForTenant`). The admin API needs an
admin token, which is minted offline because the CLI and the daemon cannot hold the embedded store at the same time:

```sh
docker compose stop lockwell
docker compose run --rm lockwell lockwell admin-token create --name my-app --role owner
docker compose start lockwell
# prints an lwadm_... token exactly once. Store it as LOCKWELL_ADMIN_TOKEN.
```

Then follow [The app kit](/guide/app-kit) for the provision-and-go flow, and [Tenancy & auth](/guide/tenancy-and-auth)
for the model behind it.

## Next steps

- [Installation](/guide/installation). Per-language registries, client options, and retry setup.
- [Upload & download](/guide/data-operations). The full object API.
- [The app kit](/guide/app-kit). Multi-tenant provisioning with near-zero glue.
- [Signed URLs](/guide/signed-urls) and [Webhooks](/guide/webhooks). Browser-direct I/O and events.
- [The three surfaces](/guide/the-three-surfaces). How S3, native, and admin fit together.
- [Native Wire (LNW/1)](/guide/native-wire). Binary transport setup, limits, retries, and rollback.
- [Deployment](/guide/deployment). TLS, the master key, backups, and production posture.
