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 (the loader always reconciles server-side via ?upsert=true, so re-applying the same blueprint is always safe). A schema whose typeName is already claimed by a different owner — e.g. a second blueprint modeling the same conceptual type — must declare basedOn (see the schema reference below).

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).

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, agentic-sdlc, second-brain).

How-to

Scaffold, validate, and apply

The full authoring loop runs through the CLI:

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 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:

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

FieldRequiredMeaning
nameyesStable blueprint id — the --blueprint <name> selector and idempotency key.
versionyesBlueprint version string.
descriptionyesHuman-readable description.
contextIdyesThe app context the profile and key bind to. 3–31 chars; lowercase letter first, then lowercase letters/digits/dashes.
contextNamenoHuman-readable context name; defaults from name.
schemasnoThe record/surface schemas to provision (see below).
accessProfileyesThe least-privilege scope the bootstrap mints for the blueprint's own key.
servicePrincipalyesThe service principal the key is bound to.
seednoDeterministic seed records.
rolesnoReusable, multi-clause scope rules.
identitiesnoDeclared principals (see the glue caveat below).
inputsnoInstall-time variables (resolved with --set/--values).

Schemas

Each schema entry declares a record type:

FieldMeaning
typeNameThe record type name.
displayNameHuman-readable label.
indexModeHYBRID (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? } — or, for a composite lookup over 2–3 fields together, { fieldNames: [...], sortBy?, allowOverflow? } (see Composite lookups below). rangeEnabled adds ordered from/to/prefix queries (use it for dates/sequences/scores; the order is lexical, so it's wrong for ordinal enums). sortBy sets the order an equality lookup returns — createdAt (default), lastUpdated, or another field on the schema. You can also range over that sort key (sortFrom/sortTo on a value lookup — e.g. one session's records since a timestamp). The sorted field does not need to be required: records that carry no value for it are ordered ahead of those that do, and are never inside a bounded window. sortBy and the sorted field's type are both migration-locked, so choose them when you declare the lookup; an array or object field cannot be a sortBy target. 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, and never a sortBy target. Up to 10.
allowedSurfaces[]Which typed surfaces may bind the schema: record, document, user, entity (identity entities in any namespace — org, client, or one you register). Defaults to [record].
capabilities.auditHistoryWhether writes emit version history (platform default on).
activeWhether the schema accepts new records.
userId / scopesSchema-level ownership defaults. scopes is an array of <namespace>:<value> entries (e.g. org:acme), the same grammar records/entities use.
basedOnId of an existing schema this one customizes, required when a schema named typeName already exists in this context under a different owner. Omit when this is the first schema under that name (it becomes that name's shared base — root/unscoped credential only, no userId/scopes). Must point directly at the base (one hop); immutable once set. Mirrors the platform's basedOn schema field (data-model/reference.md).

Each field definition supports:

Field keyMeaning
fieldIdField name.
fieldTypeField type (e.g. string, array, reference).
required / searchable / filterablePer-field flags.
enumValues[]Allowed values for an enumerated field.
validationValidation rules: required, minLength/maxLength, min/max, pattern, email/url/phone, step/multipleOf, minItems/maxItems.
renderHintsUI hints: label, widget (text/textarea/select/date/checkbox), order, section, helpText, displayField (mark the record's headline column — at most one per schema).
sensitiveMarks 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 / cardinalityOn a reference field, the typed link target. targetTypeName and targetSurface are both required — the same typeName can exist on multiple surfaces, so the surface disambiguates the lookup. targetSurface is record, document, user, or the name of an entity-backed namespaceorg, client (built in), or one you registered via POST /v1/namespaces (see identity-access/reference.md; the namespace must already be registered and entity-backed, or the schema is rejected at authoring time). 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.

Composite lookups

A lookup can span 2–3 fields matched together as one index, declared with fieldNames in place of fieldName:

lookupFields:
  - fieldNames: [status, area]   # queried together as one exact-match index
  • Order is fixed at declare time. A query can match a leading run of the declared fields — the first field alone, the first two, and so on — but never a later field alone; declare a separate lookup for that. Redeclaring the same fields in a different order creates an independent lookup that only sees writes made after it was declared, not the history under the old one.
  • Exact-match only. A composite lookup can't be unique or rangeEnabled — declare those as a separate, single-field lookup instead.
  • Record schemas only. allowedSurfaces must be exactly ['record']; document, user, and entity schemas can't declare a composite lookup.
  • Each leg must be a plain, single-valued, non-range field. A leg can't be an array or object field (one sort-key position holds one value), a reference field with cardinality: many, or a field that is itself declared rangeEnabled elsewhere on the schema — any of those would index only one of several values and the conjunction would silently return a subset while looking complete.
  • A leg cannot also be the composite's own sortBy target. Within a fixed tuple every matching record shares the same value for that leg, so the sort payload would be constant and a sortFrom/sortTo window would return either everything or nothing, at double the sort-key cost. Sort by a different field, createdAt, or lastUpdated instead.
  • sortBy and allowOverflow still apply otherwise: sortBy orders results within a group when a query supplies fewer values than the lookup declares (see lookupFields[] above).

Access profile

accessProfile:
  allowedActions: [records:cru, search:r, schemas:r]
  dataScope:
    "scope:org": [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 or scope:<namespace> → value lists; scope:org/scope:client for the built-in namespaces, or scope:<namespace> for one you registered). 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

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

Five blueprints ship with the library:

NameWhat it provisions
task-managementStructured task tracking, shareable across sessions, agents, and users. The authoring exemplar.
coding-agent-memoryA persistent memory store for a coding agent.
agentic-sdlcA whole-SDLC system of record for an AI development team — decisions, designs, references, runbooks, and post-mortems as documents, plus controls, conventions, gotchas, and a glossary as records, cross-linked and recalled by meaning.
second-brainA personal knowledge base — capture notes, ideas, and links, then ask them anything.
clinical-intakeA 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 resolved at apply time. Each entry declares a kind (user, or an entity namespace — org, client, or one you registered) plus an externalId; the loader resolves it through the ordinary identity API — createUser for kind: user, createEntity({ namespace: kind, ... }) for anything else — using the bridge token's own authority, and the resolved id becomes available to the rest of the blueprint as ${{ identities.<name> }}. The CLI fails closed on a blueprint that declares an entity namespace that hasn't been registered, 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, or entities (creating/managing identity entities, including org/client) scopes — the CLI scope gate hard-rejects them, mints nothing, and exits non-zero. Declaring principals via identities: (above) works around this the other way — the loader resolves them itself, rather than minting an entities scope for the bootstrapped key.

Where to go next

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