# Identity & access — how-to

Goal-oriented, runnable guides for modeling who acts in your tenant and scoping what they
can touch. Every snippet uses **synthetic data only** and is grounded in calls that run
against the live platform. For the concepts behind these steps, read
[explanation.md](explanation.md); for every option and limit, see [reference.md](reference.md).

Snippets use the Node SDK unless the CLI is the more natural surface. The API spec is
currently at **0.29.9**; a call available only on a newer client is marked **(SDK 0.26+)**,
and everything else also runs on a `0.23` client. Nothing on this page is 0.26-only.

## Construct a client

Every guide assumes a constructed client. You build it once with a token and an environment
(the API base URL), then call sub-clients grouped by area.

```ts
import { VectrosClient } from '@vectros-ai/sdk';

const client = new VectrosClient({
  token: process.env.VECTROS_API_KEY!,      // sk_live_* for production, sk_test_* for dev
  environment: 'https://api.vectros.ai',    // staging: https://api.staging.vectros.ai
});
```

Sub-clients you'll use here: `client.identity.*` (users / orgs / clients), `client.auth.*`
(contexts, roles, access profiles, scoped keys, token minting), and `client.records.*` /
`client.search.*` to exercise a scoped credential.

---

## Create an org and a user

**Goal:** model one customer organization and one person inside it.

**Prerequisites:** a root `sk_*` key (or any credential permitted to manage identities).

Identities are tenant-wide, idempotent by the `externalId` you supply, and orthogonal to
app contexts — you create them once and reference them everywhere.

```ts
// 1. Create the org. `externalId` is YOUR id for it; create is idempotent on it.
const org = await client.identity.createOrg({
  externalId: 'clinic-001',
  name: 'Northside Family Clinic',
  payload: { region: 'northeast' },   // free-form attributes, round-tripped as-is
});
// org.id is the Vectros-assigned UUID — use it everywhere below.

// 2. Create a HUMAN user. Users carry `email`, not `name`.
const user = await client.identity.createUser({
  externalId: 'user-jdoe',
  email: 'jdoe@example.com',
  payload: { profile: { role: 'clinician' } },
});

// 3. Create a client (an external customer you serve), owned by the org.
const customer = await client.identity.createClient({
  externalId: 'cust-1042',
  name: 'Jane Doe',
  orgId: org.id!,
});
```

**Expected result:** each call returns the created identity with `status: 'ACTIVE'` and its
Vectros `id`. Re-running any create with the same `externalId` returns the **existing**
identity unchanged (the other fields on the second call are ignored) — so these calls are
safe to repeat.

To create a **machine** identity instead of a person, pass `type: 'SERVICE'` to
`createUser`. A service user is the principal a long-running agent or scoped key acts as.

**The same flow on the CLI:**

```bash
vectros identity create --type org    --external-id clinic-001 --name "Northside Family Clinic"
vectros identity create --type user   --external-id user-jdoe  --email jdoe@example.com
vectros identity create --type client --external-id cust-1042  --name "Jane Doe" --org <orgId>

# Make a SERVICE user (machine principal) instead of a HUMAN one:
vectros identity create --type user --external-id agent-bot --service
```

Look identities up by your own id with `vectros identity list --type org --external-id clinic-001`.

---

## Create an app context and operate inside it

**Goal:** give one app (or one customer) its own isolated data partition.

**Prerequisites:** a **root `sk_*` key**. Creating or deleting an app context is a
root-only operation — a scoped key or token (`ssk_*` / `st_*`) cannot create or tear down a
context, even one carrying the wildcard `*` scope.

A context is the hard isolation boundary — all records, documents, folders, and schemas
live inside one. The id must match `^[a-z][a-z0-9-]{2,30}$`.

```ts
const ctx = await client.auth.createAppContext({
  contextId: 'clinic-intake',
  name: 'Clinic Intake App',
  description: 'Intake records for the Northside pilot',
});
```

```bash
vectros context create clinic-intake --name "Clinic Intake App"
vectros context list
vectros context get clinic-intake
```

Once the context exists, data written under a credential scoped to it is partitioned there
and is unreachable from any other context — the isolation is enforced by the platform, not
by your filters.

**Tearing a context down** is deliberate and irreversible. The delete is a confirm-gated
asynchronous cascade: you must pass a `confirm` token equal to the `contextId`, and the
context then drains all of its children in the background.

```ts
// Without `confirm` this rejects with 400 and touches nothing.
await client.auth.deleteAppContext({ contextId: 'clinic-intake', confirm: 'clinic-intake' });
// The context flips to `purging` immediately, reaching `deleted` once the drain completes.
```

---

## Define a role and grant a principal access to a context

**Goal:** create a reusable, identity-agnostic permission shape and bind a principal to it
inside a context.

**Prerequisites:** an existing context; a principal id (`usr_<userId>` or `key_<keyId>`).

A **role** is defined once and reused; an **access profile** binds a principal to either a
role or inline scopes. Always author explicit `resource:op` action forms.

```bash
# A reusable read-only role in the context.
vectros role create --context clinic-intake \
  --role-id intake-reader --name "Intake Reader" \
  --actions records:r,search:r

# Bind a user principal to that role (the binding is a separate step from issuing a key).
vectros access grant --principal usr_<userId> --context clinic-intake --role intake-reader

# Or bind inline single-clause scopes without a named role:
vectros access grant --principal usr_<userId> --context clinic-intake --actions records:r,search:r
```

The same through the SDK, using inline scopes (the wire form is snake_case `allowed_actions`):

```ts
await client.auth.createAccessProfile({
  contextId: 'clinic-intake',
  body: {
    principalId: 'usr_<userId>',
    scopes: [{ allowed_actions: ['records:r', 'search:r'] }],
    status: 'active',
  },
});
```

**Expected result:** the profile is created (or, on a repeat, the existing one is returned
unchanged). A profile carries **exactly one** of `scopes` or `roleId` — set one and the
other is cleared. To see every context a principal can reach:
`vectros access list --principal usr_<userId>`.

> A profile-create accepts **one** scope clause per request today. For multi-clause shapes,
> define a multi-clause **role** (in a blueprint) and reference it by `roleId`.

---

## Mint a least-privilege scoped key (`ssk_*`)

**Goal:** issue a permanent, identity-bearing credential that can never exceed a profile —
the right shape for an agent or a bot.

**Prerequisites:** a principal that **already has an access profile** in the target context
(the previous guide). The key inherits that profile and cannot exceed it.

```bash
# Issue an ssk_* for the bound principal. The raw secret is shown ONCE.
vectros key issue --principal usr_<userId> --context clinic-intake --name agent-key --format env
# → VECTROS_API_KEY=ssk_live_...
```

`--format env` prints `VECTROS_API_KEY=ssk_live_…` so you can drop it straight into an
agent's environment. Other formats: `human` (a labeled block with the secret), `raw` (just
the secret), `json`.

**Expected result:** the command prints the new key id, its binding, and the raw `ssk_*`
**once**. There is no way to re-read a key's secret. If you lose it — or want to rotate —
revoke and re-issue:

```bash
vectros key rotate --principal usr_<userId> --context clinic-intake --name agent-key --format env
vectros key list --context clinic-intake
vectros key revoke <keyId>      # stops working within ~5 minutes (authorizer cache)
```

The key authenticates **as** its bound principal: every call it makes is attributed to that
identity, and it is confined to that principal's profile.

---

## Mint a short-lived token (`st_*`) and the front-end-safe pattern

**Goal:** hand a browser a narrowed, short-lived credential without ever exposing a root
key.

**Prerequisites:** a backend holding an `sk_*` (or `ssk_*`).

Mint an `st_*` scoped to exactly one user's data. The token carries its scope internally and
cannot widen it.

```ts
// On your backend, in your login handler — NEVER in browser code.
const minted = await client.auth.mintToken({
  scope: {
    allowedActions: ['records:r', 'search:r'],
    dataScope: { userId: ['<that user\'s id>'] },   // narrow to one user's data
  },
  // expiresInSeconds defaults to 3600 (1h); cap is 86400 (24h). Mint short.
});
// → { token: "st_...", expiresAt: <unix-seconds> }
```

Hand `minted.token` to the browser. The browser constructs its own client with that token
and calls Vectros directly:

```ts
// In the browser, with the st_* received from your backend:
const browserClient = new VectrosClient({
  token: minted.token,
  environment: 'https://api.vectros.ai',
});
const myRecords = await browserClient.records.listRecords({ type: 'intake_form' });
```

**Expected result:** the root key never leaves your backend; the browser's token is confined
to one user for at most its lifetime; if the browser is compromised, blast radius is one user
for one token-lifetime. The token cannot be revoked in flight — keep the lifetime short.

> The mint endpoint accepts **one** scope clause per request. For a compound shape, bind the
> principal to a multi-clause role and mint via the scoped-key path instead.

---

## Restrict a credential to one customer's data with `dataScope`

**Goal:** confine reads and searches to a single org's (or client's) records, including the
strict-scope rules.

**Prerequisites:** records tagged with an `orgId` (or `clientId`) you can scope to.

`dataScope` is enforced as a server-side filter below any filter the caller supplies. It is
**strict**: a scoped call must include the matching filter explicitly.

```ts
// Mint a token confined to one org.
const minted = await client.auth.mintToken({
  scope: {
    allowedActions: ['records:r', 'search:r'],
    dataScope: { orgId: ['<org id>'] },
  },
});
const scoped = new VectrosClient({ token: minted.token, environment: 'https://api.vectros.ai' });

// The call MUST carry orgId — strict scope requires the field explicitly.
const list = await scoped.records.listRecords({ type: 'intake_form', orgId: '<org id>' });
// Only org-tagged records come back; tenant-only (unowned) records are filtered out.

const hits = await scoped.search.content({
  query: 'follow-up appointment',
  mode: 'TEXT',
  limit: 100,
  orgId: '<org id>',
});
```

**Expected result:** the credential sees only rows owned by that org. Omitting `orgId` on the
call is rejected with a message naming the required field. To *also* reach **tenant-level**
(owner-less) records under the same credential, opt in explicitly with a `null` in the value
list at mint time: `dataScope: { orgId: ['<org id>', null] }`. The `null` is never implied.

The same pattern works for `clientId` and `userId` — substitute the field on both the mint
and the call.

---

## Verify what a credential actually is

**Goal:** confirm the principal, tenant, and scope a credential resolves to.

A lightweight identity-binding check returns the authenticated principal's shape:

```ts
const who = await client.auth.ping();
// For an sk_* root key: principalType 'root_key' (no action list — it's wildcard).
// For an ssk_* scoped key: principalType 'scoped_key' + its allowedActions.
// For an st_* token: principalType 'token' + tokenExpiresAt.
```

This is the fastest way to confirm a freshly minted scoped key carries the actions you
expect before you ship it. An invalid credential is denied at the edge with a 403.

---

## Where to go next

- [reference.md](reference.md) — every identity/access method, the full scope grammar, the
  data-plane allowlist, error codes, and an honest "notes & limits."
- [explanation.md](explanation.md) — the why behind contexts, scopes, profiles, and the
  three credential types.
- The **blueprint walkthroughs** (getting-started, clinical-intake, coding-agent-memory,
  second-brain) — end-to-end builds that provision a context, principal, profile, and scoped
  key in one command.
- The **generated API reference** (rendered from the OpenAPI specification) — canonical
  request and response shapes.
