# The account folder, file by file

Schema `streamline.config/1`. JSON Schemas: `https://streamliner.work/schema/v1/<kind>.schema.json` (also in the
Streamline repo under `schema/`). Run the checker after every edit (SKILL.md step 5).

## Contents
- Layout
- manifest.json
- enrolment.json
- tenant.json
- private/tenant.json
- Paths the write API accepts
- URLs

## Layout

```
<account>/
  tenant.json                    brand, locale, auth, roles, kinds (streamline order)          device-visible
  agents/<kind>.json             one streamline: what people see (streamlines.md)               device-visible
  templates/<id>.json            one view template (templates.md)                               device-visible
  forms/<id>.json                one sealed form / hosted page / sealed view (forms.md)          device-visible
  assets/<sha256>.png|jpg|pdf    logos and images; the file name prefixes the content's sha256   device-visible
  manifest.json                  signed index of every device-visible file
  enrolment.json                 the publisher public keys devices pin (not part of the signed set)
  private/tenant.json            account policy: networks, access seeds, serverKeys, budgets, transcripts
  private/agents/<kind>.json     instructions, model, credentialRef, MCP servers + tool allowlist, network, limits
  private/connections/<id>.json  login details for password / api-key connections (back end only)
  private/briefs/<id>.md         a brief: shared instructions streamlines include (briefs.md)      relay only
```

Device-visible files are served only to signed-in members, and each member gets only the streamlines their roles
open. `private/` never reaches a member's device (an owner's or admin's approved device reads it to edit and publish it). Templates and forms are account-level and shared by id across streamlines.

## manifest.json

Written by `scripts/sign-config.mjs`, never by hand.

```json
{ "schema": "streamline.config/1", "tenant": "meridian", "version": 9, "published": "2026-10-09T17:40:00Z",
  "minApp": "1.0.0 (1)", "refresh": { "foregroundSeconds": 900, "minSeconds": 60 },
  "files": [ { "path": "tenant.json", "sha256": "4b1f…", "bytes": 3120 } ],
  "signature": { "alg": "Ed25519", "keyId": "meridian-2026a", "sig": "base64url…" } }
```

- `version` must go up on **every** publish, including a change to `private/` alone.
- `files[]` lists every device-visible file (not `private/`, `manifest.json`, `enrolment.json`).
- `signature.sig` is Ed25519 over the JCS (RFC 8785) bytes of the manifest without `signature`, by one of the
  account's publisher keys. Optional `nextKeys: [{keyId, pub}]` rotates keys (devices trust them because the current
  key signed them).

## enrolment.json

```json
{ "tenant": "meridian", "configHost": "/", "keys": [ { "keyId": "meridian-2026a", "pub": "<base64url Ed25519 public key>" } ] }
```

Public halves only. Changing it changes the keys *new* devices pin; treat it as an owner-only change.

## tenant.json

```json
{
  "id": "meridian",
  "mode": "enterprise",
  "brand": { "name": "Meridian Jewelers", "shortName": "Meridian", "logo": "assets/3f9a0c6e1d….png",
             "accent": { "light": "#1F4E79", "dark": "#8DB8E8" } },
  "locale": { "language": "en-US", "currency": "USD", "timeZone": "America/Los_Angeles" },
  "auth": { "kind": "invite" },
  "gateway": { "url": "https://<back end>/v1" },
  "connections": [ { "id": "pos", "name": "Meridian POS", "type": "oauth" } ],
  "roles": [
    { "id": "associate", "name": "Sales associate", "claims": { "groups": ["mj-associates"] } },
    { "id": "store_manager", "name": "Store manager", "claims": { "groups": ["mj-managers"] } }
  ],
  "kinds": ["register", "approvals"],
  "imageHosts": ["img.meridian.example"],
  "policy": { "chatRetentionDays": 2, "appLockAfterSeconds": 300, "sealedScreenshots": "blur" },
  "support": { "text": "Questions? Call the help desk at ext. 400.", "url": "https://help.meridian.example" }
}
```

| Field | Notes |
|---|---|
| `id` | `[a-z0-9-]{3,40}`; equals the account id the write API uses. |
| `mode` | `enterprise` (an organization), `personal`, or `demo` (scripted agent, mock backend; the only mode that may have card fields). |
| `brand` | `name`, `shortName` (≤ 16 chars), `logo` (an `assets/` path), `accent` and optional `background` as `{light, dark}` `#RRGGBB`, optional `tagline` (one line under the name on the join screen and in Settings). |
| `theme` | The account's baseline theme: a preset (`base`, the default; `quiet`, `harbour`, `refined-calm`, `amber`; light-only `paperwhite`, dark-only `nord`) or one of `themes[].id`. Keep `brand.accent` equal to its accent. Colors only. |
| `themes[]` | Up to 8 own themes: `{ id, name, light, dark? }`, each side a shared ColorTheme id or a ColorTheme document (hex colors under `surfaces`, `accents`, `semantics`, `text`, `actions`). |
| `locale` | BCP 47 language, ISO 4217 currency (the `money` formatter's default), IANA time zone. |
| `auth.kind` | `invite` (the default: Sign in with Apple, passkey or email code; memberships from invites or directory sync), `oidc` (the organization's IdP: `issuer`, `clientId`, `scopes`, `resources`, `roleClaim`), `connection` (sign in to a named connection), `demo`. See access-and-networks.md. |
| `gateway.url` | The back end's base URL (relay, connections, enrollment). Given to you by Streamliner. |
| `connections[]` | The systems people sign in to (access-and-networks.md). |
| `roles[]` | Job roles: `id`, `name`, `claims` (for OIDC: claim → accepted values). Streamlines and tools name role ids. |
| `kinds[]` | The streamlines, in sidebar order. Each needs `agents/<kind>.json` and `private/agents/<kind>.json`. |
| `imageHosts[]` | The only hosts template images may load from. |
| `policy` | `chatRetentionDays` (default 2 for organizations), `appLockAfterSeconds`, `sealedScreenshots` (`blur`), `attachmentsMaxBytes`, `uploadRetentionDays` (1–30, default 7), `voice` (`false` turns the mic and Speak replies off account-wide), `syncAccountList` (`false` keeps the organization out of members' organization list in iCloud, which otherwise shows it on their other devices, signed out; default true). |
| `support` | `text`, `url`, optional `email` (where Report an Issue offers a mail link about this account's streamlines). |

## private/tenant.json

```json
{
  "network": { "mode": "none" },
  "networks": { "head-office": { "name": "Head office", "cidrs": ["203.0.113.0/24"] } },
  "roles": { "regional": { "networks": ["head-office"] } },
  "users": { "u-marco": { "networks": ["head-office"] } },
  "access": { "u-marco": "admin" },
  "serverKeys": [ { "id": "claude-setup-oct", "alg": "Ed25519", "publicKey": "<base64url 32 bytes>", "scopes": ["config"] } ],
  "budgets": { "warnAt": 0.8, "perUser": { "tokensPerDay": 3000000, "costPerDayUSD": 5 } },
  "transcripts": { "retentionDays": 365 }
}
```

- `serverKeys` holds **public halves only**. A publish that would leave no enabled key with the `config` scope is
  refused (`would_lock_out`), so keep the key you publish with.
- Networks, budgets and transcripts: access-and-networks.md.

## Paths the write API accepts

`tenant.json`, `enrolment.json`, `manifest.json`, `agents|templates|forms/<id>.json`,
`assets/<sha256 prefix>.png|jpg|jpeg|pdf`, `demo/backend.js|agent.js|fixtures.json` (demo accounts),
`private/tenant.json`, `private/agents|connections/<id>.json`, `private/briefs/<id>.md`. Anything else is refused (`bad_path`): keep notes,
fixtures and drafts outside the folder.

## URLs

Absolute `https://` everywhere. A URL starting with `/` resolves against the config host (demo accounts only).
`http://` is refused except `http://localhost` for local testing.
