# Part 2 — the device

## Opening an engine

```ts
const engine = await ArgalaEngine.open({
  memberId: me.id,
  deviceId,
  transport,
  store,
  keyStore,
  kinds,
  // optional
  deviceLabel: "sam's laptop",
  autoGrant: true, // default: grant other devices' key gaps during sync
  sign: true, // default: sign every record
  requireSignatures: true, // default: reject unsigned/unverifiable records
  pushDebounceMs: 250, // default: how long a write waits for its neighbours
  persistStorage: true, // default: ask the browser not to evict this store
  retry: { attempts: 4, baseMs: 250, maxMs: 8_000 },
  metadata: undefined, // partial-sync metadata — see Part 10
  now: () => Date.now(),
  onEvent: (evt) => console.debug(evt),
});
```

`open` generates this device's keypairs on first run (stored in the `KeyStore`),
registers the public halves with the relay, and loads any keyrings it already
holds.

## The device ID

```ts
const deviceId = localStorage.getItem('argala-device') ?? ArgalaEngine.newDeviceId();
localStorage.setItem('argala-device', deviceId);
```

The ID must remain stable across restarts and unique to each installation. **A
device ID already bound to public keys is never rebound** — that is the redirect
attack — so registering an ID with different keys is refused with 409 and
`status.deviceOk` becomes false. The correct response to a 409 is a new device
ID, never a retry.

## Choosing a store

```ts
import { bestStore, IdbGraphStore, MemoryGraphStore } from '@argala/client';
import { NodeSqliteGraphStore } from '@argala/client/node';

await bestStore({ name: me.id }); // browser: OPFS-SQLite, else IndexedDB
new IdbGraphStore('argala-' + me.id); // browser: IndexedDB, explicitly (a db name)
new NodeSqliteGraphStore({ path: 'data/sam.db' }); // Node / Electron main
new MemoryGraphStore(); // tests, ephemeral agents
```

> **OPFS is one tab at a time.** `bestStore` falls back to IndexedDB in a second
> tab, so two tabs of one app hold two different local stores. They converge
> through the relay, but neither sees the other's unsynced writes. If that
> matters to your UI, use `IdbGraphStore` everywhere, or run the engine in a
> SharedWorker.

## Choosing a key store

```ts
import { IdbKeyStore, MemoryKeyStore } from '@argala/client';
import { FileKeyStore } from '@argala/client/node';

new IdbKeyStore(); // browser (optional db name, default 'skm-keys')
new FileKeyStore('data/sam-keys'); // server-side agents
new MemoryKeyStore(); // tests
```

> **`FileKeyStore` is plaintext on disk** (mode 0700, one file per slot, and a
> `destroy()` for tearing a device down). It is appropriate for a server-side
> agent whose disk is already the trust boundary, but not for a laptop, where
> the OS keychain is the answer. `KeyStore` is three methods — `get(slot)`, `set(slot, value)`,
> `keys(prefix)` — so wrapping Keychain, DPAPI, or libsecret is about thirty
> lines.

## Declaring kinds

```ts
import { defineKinds } from '@argala/client';
import { z } from 'zod';

const kinds = defineKinds({
  note: z.object({ body: z.string().max(10_000) }),
  task: z.object({ title: z.string(), done: z.boolean(), due: z.number().optional() }),
});
```

**A decrypted record is untrusted input.** The relay cannot validate what it
cannot read, and any member holding the key can craft any record — so the
device validates after opening and skips what fails. Without a `KindRegistry`,
a member with the key can write anything into every other member's store.

A validator can be a zod schema or a plain predicate:

```ts
defineKinds({ blob: (data: unknown) => typeof (data as any)?.ref === 'string' });
```

The second argument decides what happens to a kind this build has never heard
of:

```ts
defineKinds({ ... }, 'accept')  // default — a newer client's records still sync
defineKinds({ ... }, 'skip')    // stricter, for a closed record set
```

`'accept'` is the default because, during a rolling deployment, the alternative
is an old tab silently rejecting new data.

## Transport

```ts
new HttpTransport({
  baseUrl: '/api/argala',
  headers: async () => ({ authorization: `Bearer ${await session.token()}` }),
  credentials: 'same-origin', // only meaningful same-origin
  timeoutMs: 30_000,
  fetch: myFetch, // for shells whose fetch is not global
});
```

`headers` is called **per request**, so a rotating token stays fresh.

Failures surface as `TransportError` with `status` and `retryable` (true for
network failures, 429, and 5xx). `Transport` is an interface if you want to run
argala over something other than HTTP.

---
