# Blueprints

## Explanation — what & why

A **blueprint** is an app model as configuration. One file declares everything a
small Vectros app needs to exist — the record schemas, a least-privilege access
profile, a service principal, optional seed data, and optional roles — with
stable identifiers so applying it twice converges instead of duplicating.

Blueprints exist to collapse "stand up a new app's data layer" from a sequence of
API calls into a single reviewable artifact. You write the file once; the CLI's
`bootstrap` and `blueprint-test` commands turn it into provisioned
infrastructure and a working scoped key (see [cli.md](cli.md)).

Two boundaries define what a blueprint is and is not:

- **It describes format, not trust.** The `@vectros-ai/blueprints` package owns
  the format and its structural validation only. The security boundary — the
  scope gate that confines a blueprint to data-plane access — lives in the CLI
  binary. A blueprint is untrusted input; the binary is the trust boundary.
- **It is data-plane only.** A blueprint can request scopes on records, schemas,
  search, documents, folders, and inference — nothing else. Control-plane scopes
  are hard-rejected when the CLI applies it.

This doc is the **format reference** the blueprint walkthroughs build on. For
narrated, end-to-end builds, see the walkthroughs (getting-started,
clinical-intake, coding-agent-memory, second-brain).

## How-to

### Scaffold, validate, and apply

The full authoring loop runs through the CLI:

```bash
vectros blueprint init my-app                  # scaffold ./my-app.blueprint.yaml
vectros blueprint validate ./my-app.blueprint.yaml
vectros blueprint plan ./my-app.blueprint.yaml
vectros bootstrap --blueprint ./my-app.blueprint.yaml
```

You can scaffold from a bundled exemplar with `init --from <name>`. See
[cli.md](cli.md) for the lifecycle commands.

### A minimal blueprint

Blueprints are authored as YAML or JSON. A minimal one declares a context, one
schema, an access profile, and a service principal:

```yaml
name: my-app
version: 1.0.0
description: A minimal example app.
contextId: my-app
contextName: My App

schemas:
  - typeName: note
    displayName: Note
    indexMode: HYBRID          # HYBRID | SEMANTIC | TEXT
    fields:
      - { fieldId: title, fieldType: string, required: true, searchable: true }
      - { fieldId: body,  fieldType: string, searchable: true }
    lookupFields: [title]        # extra exact-match index(es). Do NOT list externalId:
                                 # it's the record's first-class id (sent top-level on
                                 # each record), with its own finder — declaring it as a
                                 # field or lookup is rejected.

accessProfile:
  allowedActions: [records:r, records:c, records:u, search:r, schemas:r]

servicePrincipal:
  externalId: my-app
  displayName: My App
```

`indexMode`, the per-field `validation`/`renderHints`, and the schema-level
`capabilities`/`active`/ownership fields are all optional — the example stays
minimal on purpose. Note the deliberate absence of `records:d` in
`allowedActions`: least privilege is the authoring default.

## Reference — what a blueprint can express

### Top-level fields

| Field | Required | Meaning |
|---|---|---|
| `name` | yes | Stable blueprint id — the `--blueprint <name>` selector and idempotency key. |
| `version` | yes | Blueprint version string. |
| `description` | yes | Human-readable description. |
| `contextId` | yes | The app context the profile and key bind to. 3–31 chars; lowercase letter first, then lowercase letters/digits/dashes. |
| `contextName` | no | Human-readable context name; defaults from `name`. |
| `schemas` | no | The record/surface schemas to provision (see below). |
| `accessProfile` | yes | The least-privilege scope the bootstrap mints for the blueprint's own key. |
| `servicePrincipal` | yes | The service principal the key is bound to. |
| `seed` | no | Deterministic seed records. |
| `roles` | no | Reusable, multi-clause scope rules. |
| `identities` | no | Declared principals (see the glue caveat below). |
| `inputs` | no | Install-time variables (resolved with `--set`/`--values`). |

### Schemas

Each schema entry declares a record type:

| Field | Meaning |
|---|---|
| `typeName` | The record type name. |
| `displayName` | Human-readable label. |
| `indexMode` | `HYBRID` (keyword + semantic), `SEMANTIC`, or `TEXT`. |
| `fields[]` | Field definitions (see below). |
| `lookupFields[]` | Fields to index for direct lookup. Bare field name (equality), or `{ fieldName, unique?, rangeEnabled?, sortBy?, allowOverflow? }`. `rangeEnabled` adds ordered `from`/`to`/`prefix` queries (use it for dates/sequences/scores; the order is lexical, so it's wrong for ordinal enums). Each schema has **7 fast equality slots** (range lookups use a row, not a slot; ownership ids + `externalId` ride their own); an 8th equality lookup needs `allowOverflow`. The index shape is **migration-locked** — pick it deliberately. A `sensitive` field may be an equality (blind-index) lookup, never `rangeEnabled`. Up to 10. |
| `allowedSurfaces[]` | Which typed surfaces may bind the schema: `record`, `document`, `user`, `org`, `client`. Defaults to `[record]`. |
| `capabilities.auditHistory` | Whether writes emit version history (platform default on). |
| `active` | Whether the schema accepts new records. |
| `userId` / `orgId` / `clientId` | Schema-level ownership defaults. |

Each field definition supports:

| Field key | Meaning |
|---|---|
| `fieldId` | Field name. |
| `fieldType` | Field type (e.g. `string`, `array`, `reference`). |
| `required` / `searchable` / `filterable` | Per-field flags. |
| `enumValues[]` | Allowed values for an enumerated field. |
| `validation` | Validation rules: `required`, `minLength`/`maxLength`, `min`/`max`, `pattern`, `email`/`url`/`phone`, `step`/`multipleOf`, `minItems`/`maxItems`. |
| `renderHints` | UI hints: `label`, `widget` (`text`/`textarea`/`select`/`date`/`checkbox`), `order`, `section`, `helpText`, `displayField` (mark the record's headline column — at most one per schema). |
| `sensitive` | Marks the field as sensitive: it is redacted/destroyed at write time, blind-indexed for lookups, excluded from the search index, and masked on read unless a token carries the reveal scope. |
| `targetTypeName` / `targetSurface` / `targetField` / `cardinality` | On a `reference` field, the typed link target. `targetTypeName` and `targetSurface` (`record`/`document`/`user`/`org`/`client`) are **both required** — the same typeName can exist on multiple surfaces, so the surface disambiguates the lookup. `targetField` defaults to the target's `externalId` (must be a unique lookup); `cardinality` = `one` (default) / `many`. The target's existence is enforced at write by default. |

### Access profile

```yaml
accessProfile:
  allowedActions: [records:cru, search:r, schemas:r]
  dataScope:
    orgId: [org_acme, null]
```

- `allowedActions` — the scopes the minted key carries. Author explicit
  `resource:op` forms; coarse verbs and `resource:*` grant nothing at runtime.
  Subject to the data-plane scope gate.
- `dataScope` — optional ownership binding (`userId`/`orgId`/`clientId` →
  value lists). A `null` element is the documented **null sentinel**: it
  additively grants access to tenant-level (owner-less) records *in addition to*
  the listed owners. Omitting `null` restricts the key to the listed owners only
  — so it will not see tenant-level/seed records.

### Service principal, seed, roles

```yaml
servicePrincipal:
  externalId: my-app
  displayName: My App

seed:
  - typeName: note
    externalId: seed-welcome
    fields: { title: Welcome, body: Created by the bootstrap loader. }   # externalId is the seed's top-level field above, NOT a payload field

roles:
  editor:
    - allowedActions: [records:cru, search:r]
      dataScope:
        userId: ['${{ self.userId }}']
```

- `servicePrincipal` — the principal the minted key binds to.
- `seed` — deterministic records (keyed by `externalId` for idempotency).
- `roles` — a map of role id → ordered clauses. Each clause is an
  `(allowedActions, dataScope)` pair, evaluated per-clause at runtime. Roles are
  identity-agnostic and reusable; bind them to principals with
  `vectros access grant --role <id>`. Every role clause is also gated to the
  data plane, so a role cannot be a control-plane back door around
  `accessProfile`.
- `${{ self.* }}` placeholders are a **runtime per-principal** sentinel,
  resolved per-request by the platform. They are only valid inside a role
  clause's `dataScope`; using one anywhere else is an authoring error.

### Inputs (install-time variables)

A blueprint may declare an `inputs:` block and reference values with
`${{ inputs.<name> }}` (plus the built-ins `${{ vectros.context }}` /
`${{ vectros.suffix }}`). Supply values at validate/plan/apply time with `--set`
or `--values`. Inputs apply to **file** blueprints only.

### Bundled blueprints

Four blueprints ship with the library:

| Name | What it provisions |
|---|---|
| `task-management` | Structured task tracking, shareable across sessions, agents, and users. The authoring exemplar. |
| `coding-agent-memory` | A persistent memory store for a coding agent. |
| `second-brain` | A personal knowledge base — capture notes, ideas, and links, then ask them anything. |
| `clinical-intake` | A clinical intake data model (synthetic/illustrative). |

List them with `vectros blueprint list`; apply one with
`vectros bootstrap --blueprint <name>`.

## Notes & limits — honest glue caveats

- **A bridge token is a human prerequisite.** Applying a blueprint with
  `bootstrap` requires a bridge token from the developer portal — there
  is no fully unattended path that mints one for you.
- **`identities:` blocks are declared but not yet wired.** The format accepts an
  `identities` block (principals referenced via `${{ identities.<name> }}`) and
  validates it, but the apply pass that ensures those principals exist is not yet
  active. The CLI **fails closed** on a blueprint that uses identities the loader
  cannot yet resolve, rather than applying it partially.
- **`reference` fields enforce the target at write.** A blueprint can declare a
  typed reference between record types; by default the platform requires the
  referenced record to exist when the referencing record is written — so if a seed
  record references another, seed the target **first**. (To opt out for an
  order-independent bulk load, a schema would set `capabilities.validateReferences:
  false` — not exposed in the bundled blueprints.) There is no reverse-reference
  query on this surface; to ask "which records reference X", declare the reference
  field as an equality lookup.
- **No control-plane scopes.** A blueprint cannot request keys, profiles, app
  contexts, users, billing, admin, clients, or orgs scopes — the CLI scope gate
  hard-rejects them, mints nothing, and exits non-zero.

## Where to go next

- [cli.md](cli.md) — `init`, `validate`, `plan`, `bootstrap`, and
  `blueprint-test` operate on these files.
- [mcp.md](mcp.md) — the scoped key a blueprint mints is what an MCP agent runs
  on.
- [sdk.md](sdk.md) — the operations a blueprint's schemas and scopes govern.
- The blueprint walkthroughs — narrated, end-to-end builds on top of this format.
