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/blueprintspackage 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
| 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? } — 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.auditHistory | Whether writes emit version history (platform default on). |
active | Whether the schema accepts new records. |
userId / scopes | Schema-level ownership defaults. scopes is an array of <namespace>:<value> entries (e.g. org:acme), the same grammar records/entities use. |
basedOn | Id 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 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 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 namespace — org, 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
uniqueorrangeEnabled— declare those as a separate, single-field lookup instead. - Record schemas only.
allowedSurfacesmust 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
arrayorobjectfield (one sort-key position holds one value), areferencefield withcardinality: many, or a field that is itself declaredrangeEnabledelsewhere 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
sortBytarget. Within a fixed tuple every matching record shares the same value for that leg, so the sort payload would be constant and asortFrom/sortTowindow would return either everything or nothing, at double the sort-key cost. Sort by a different field,createdAt, orlastUpdatedinstead. sortByandallowOverflowstill apply otherwise:sortByorders results within a group when a query supplies fewer values than the lookup declares (seelookupFields[]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 explicitresource:opforms; coarse verbs andresource:*grant nothing at runtime. Subject to the data-plane scope gate.dataScope— optional ownership binding (userIdorscope:<namespace>→ value lists;scope:org/scope:clientfor the built-in namespaces, orscope:<namespace>for one you registered). Anullelement is the documented null sentinel: it additively grants access to tenant-level (owner-less) records in addition to the listed owners. Omittingnullrestricts 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 byexternalIdfor 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 withvectros access grant --role <id>. Every role clause is also gated to the data plane, so a role cannot be a control-plane back door aroundaccessProfile.${{ self.* }}placeholders are a runtime per-principal sentinel, resolved per-request by the platform. They are only valid inside a role clause'sdataScope; 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:
| 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. |
agentic-sdlc | A 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-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
bootstraprequires 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 akind(user, or an entity namespace —org,client, or one you registered) plus anexternalId; the loader resolves it through the ordinary identity API —createUserforkind: 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.referencefields 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 setcapabilities.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, includingorg/client) scopes — the CLI scope gate hard-rejects them, mints nothing, and exits non-zero. Declaring principals viaidentities:(above) works around this the other way — the loader resolves them itself, rather than minting anentitiesscope for the bootstrapped key.
Where to go next
- cli.md —
init,validate,plan,bootstrap, andblueprint-testoperate 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.