# Blueprints

> What a blueprint captures and omits, the three binding types (`gatekeeper`, `aiModel`, `agentSpawner`), blueprint annotations stored on `GatekeeperRecord`, 128-bit hex IDs versus stable bundled IDs, and the one-way Gadget DO to User DO to Workers KV propagation with its `dirty` flag. Includes `.gadget` export/import and share-link semantics.

- Repository: cloudflare/cloudflare-os
- GitHub: https://github.com/cloudflare/cloudflare-os
- Human docs: https://grok-wiki.com/public/docs/cloudflare-cloudflare-os-838773bb92dd
- Complete Markdown: https://grok-wiki.com/public/docs/cloudflare-cloudflare-os-838773bb92dd/llms-full.txt

## Source Files

- `docs/blueprints.md`
- `packages/workshop-backend/src/blueprint-archive.ts`
- `packages/workshop-shared/src/api.ts`
- `packages/workshop-backend/src/format-blueprints.ts`
- `packages/workshop-backend/src/overseer.ts`

---

---
title: "Blueprints"
description: "What a blueprint captures and omits, the three binding types (`gatekeeper`, `aiModel`, `agentSpawner`), blueprint annotations stored on `GatekeeperRecord`, 128-bit hex IDs versus stable bundled IDs, and the one-way Gadget DO to User DO to Workers KV propagation with its `dirty` flag. Includes `.gadget` export/import and share-link semantics."
---

A blueprint is a shareable snapshot of a gadget's source code plus a description of the bindings that code requires. It carries no credentials, no SQLite storage, and no chat history, so every gadget created from a blueprint gets its own bindings, storage, and chat history. Blueprint metadata lives in the `BLUEPRINTS` KV namespace, code snapshots live in the `BLUEPRINT_CONTENT` R2 bucket under `<blueprintId>/<version>`, and the authoritative record lives in the source gadget's Durable Object.

## What a blueprint captures

| Captured | Detail |
| --- | --- |
| Source code | Snapshot of the gadget's committed Yjs document, stripped of edit history. Only final file contents (one insert operation per file), producing a minimal encoding. |
| Binding requirements | One description per named binding: connection type, gatekeeper name, URL pattern, and configuration hints. No credentials, no live connections. |
| Metadata | Title, description, optional screenshot metadata, author info, version number, timestamps. |

Not captured:

- Gadget SQLite storage contents.
- AI chat history and edit history.
- Live connections or credentials — only the *shape* of each binding.

<Note>
A single gadget can have multiple blueprints at different code versions (for example a "stable" and a "latest" blueprint of the same gadget). A blueprint is always owned by the gadget's owner regardless of which collaborator created it; bundled blueprints have no owning user.
</Note>

## Blueprint IDs and share links

Blueprint IDs come in two flavors:

- **Random 128-bit hex** — generated server-side by `randomBlueprintId()` in `packages/workshop-backend/src/blueprint-archive.ts`, which fills 16 bytes from `crypto.getRandomValues()` and calls `.toHex()`.
- **Stable bundled IDs** — deployment-installed format blueprints carry readable, stable IDs such as `format.document`. These are ordinary blueprint records: no reserved prefix, no fallback branch in the read path.

```ts
// packages/workshop-backend/src/blueprint-archive.ts
export function randomBlueprintId(): string {
  let idBytes = new Uint8Array(16);
  crypto.getRandomValues(idBytes);
  return idBytes.toHex();
}
```

Share links have the form `https://<host>/blueprint/<blueprint-id>`. Knowing the ID is sufficient to read metadata:

| Operation | Auth required |
| --- | --- |
| View title, description, author, required bindings | No — knowing the ID is enough, a blueprint is "just data" |
| Create a gadget from the blueprint | Yes |

<Warning>
Two keys in the `BLUEPRINTS` namespace are reserved and are never treated as blueprint IDs: `.featured` (`FEATURED_BLUEPRINTS_KEY`) and `.adminConfig` (`ADMIN_CONFIG_KEY`). `isReservedBlueprintKey()` guards `readBlueprintKvRecord()`, which returns `null` for either key.
</Warning>

## Binding types

Blueprints support three binding types, matching the three gatekeeper types.

<ParamField body="gatekeeper" type='type: "gatekeeper"'>
An external resource connection (for example Google Drive or a REST API). The blueprint records the gatekeeper adapter name and a URL pattern describing the expected resource. On instantiation, the user picks a connected account and configures a matching resource.
</ParamField>

<ParamField body="aiModel" type='type: "aiModel"'>
A language model binding. The blueprint may suggest a specific provider/model. On instantiation, the user picks from their own configured models.
</ParamField>

<ParamField body="agentSpawner" type='type: "agentSpawner"'>
An agent spawner binding. The blueprint carries over the spawner configuration (prompt types, env restrictions) from the source gadget. The user only chooses which model the spawner should use, or no model.
</ParamField>

## Binding annotations

Before publishing, the author can annotate the gadget's named bindings from the **Blueprint** modal in the gadget editor header. Annotations control how each required connection is presented to someone creating a gadget from the blueprint. All named bindings are included in the blueprint regardless of annotation.

| Annotation | Behavior |
| --- | --- |
| Name | Friendly connection name shown to consumers. Defaults to the current resource title; the binding name remains the stable key used by code. |
| Description | Optional helper text explaining what kind of resource to connect. |
| Suggest value | Optionally embeds the specific resource URL or model name as a suggestion — a suggestion, not a requirement. |

The annotation is persisted on the `GatekeeperRecord` as the `blueprintAnnotation` field. `packages/workshop-backend/src/overseer.ts` defines a `LegacyBlueprintBindingAnnotation` variant (`BlueprintBindingAnnotation & { included?: boolean }`) for older records, and derives the default title with:

```ts
// packages/workshop-backend/src/overseer.ts
function defaultBlueprintBindingTitle(record: GatekeeperRecord, bindingName?: string): string {
  return record.resourceTitle || bindingName || "Connection";
}
```

## Storage architecture

Blueprint data is written to three stores with strictly one-way propagation, plus R2 for the code snapshot.

```mermaid
flowchart LR
  subgraph gadget["Gadget DO — authoritative"]
    A["blueprints collection<br/>BlueprintGadgetRecord<br/>metadata + exported code version + dirty"]
  end
  subgraph user["User DO — denormalized"]
    B["blueprints collection<br/>BlueprintUserRecord<br/>metadata + source gadget ref"]
  end
  subgraph kv["Workers KV — public lookup"]
    C["BLUEPRINTS namespace<br/>BlueprintKvRecord keyed by hex ID<br/>reserved: .featured, .adminConfig"]
  end
  subgraph r2["R2 — code content"]
    D["BLUEPRINT_CONTENT<br/>key &lt;blueprintId&gt;/&lt;version&gt;<br/>gzip Yjs V2 full state"]
  end
  A -->|propagate| B
  B -->|propagate| C
  A -->|put snapshot| D
  C -->|read| E["PublicApi.getBlueprint()"]
  D -->|read| F["readBlueprintContent()"]
```

<AccordionGroup>
<Accordion title="Gadget DO — blueprints collection">
The authoritative source. Stores `BlueprintGadgetRecord` including full metadata, the code version that was exported, and the `dirty` flag used to track propagation failures.
</Accordion>
<Accordion title="User DO — blueprints collection">
A denormalized copy for efficient listing. Stores `BlueprintUserRecord` with metadata and a reference to the source gadget, so a user can audit and manage their blueprints even after the source gadget is deleted.
</Accordion>
<Accordion title="Workers KV — BLUEPRINTS namespace">
The public-facing lookup store. Stores `BlueprintKvRecord` keyed by blueprint hex ID; this is what `PublicApi.getBlueprint()` reads. The record shape is `{ metadata, ownerId?, gadgetId? }`, where a missing `gadgetId` means the blueprint was uploaded rather than published from a gadget on this instance, and a missing `ownerId` means the deployment installed it itself.
</Accordion>
<Accordion title="R2 — BLUEPRINT_CONTENT bucket">
Code content only, keyed `<blueprintId>/<version>`, stored as a Yjs V2-encoded document (full state, not incremental updates). `readBlueprintContent()` pipes the object through `DecompressionStream("gzip")` and returns the decompressed bytes, or `null` when the object does not exist. Old versions are retained on update to avoid race conditions during concurrent instantiation; deleting a blueprint cleans up all its R2 versions.
</Accordion>
</AccordionGroup>

### The `dirty` flag

`dirty` is set to `true` before propagation begins and cleared only after every downstream write succeeds. If a failure leaves it set, the UI surfaces a warning with a **Retry** button. This is the only recovery mechanism for a partially propagated blueprint — the propagation direction is one-way, so KV is never treated as a source of truth for the Gadget DO.

## `.gadget` archive format

Blueprints download from `/blueprint/<id>` as `.gadget` files and upload from the home blueprints tab into another Workshop instance. `PublicApi.downloadBlueprint(id)` returns a `ReadableStream<Uint8Array>` containing only `BlueprintMetadata` plus the current code snapshot — not the full KV record.

```text
byte offset  size  field
0            8     magic  0xec2e2d3a2300e317   (BigUint64, big-endian)
8            4     format version = 1          (Uint32)
12           4     JSON metadata byte length   (Uint32)
16           8     raw content byte length     (BigUint64)
24           N     UTF-8 JSON BlueprintMetadata
24 + N       M     gzip-compressed Yjs snapshot, copied from
                   BLUEPRINT_CONTENT/<blueprintId>/<version>
```

The 24-byte prefix is emitted by `encodeBlueprintArchivePrefix()`; `buildBlueprintArchiveStream()` writes the prefix with `preventClose: true` and then pipes the R2 content stream into the same writable, aborting the transform on error. Import and export stream through `pipeTo()` rather than buffering the whole archive in worker memory.

### Limits and validation

| Constant | Value | Purpose |
| --- | --- | --- |
| `BLUEPRINT_ARCHIVE_MAGIC` | `0xec2e2d3a2300e317n` | Identifies a `.gadget` container |
| `BLUEPRINT_ARCHIVE_VERSION` | `1` | Format version |
| `BLUEPRINT_ARCHIVE_PREFIX_BYTES` | `24` | Fixed header size |
| `MAX_BLUEPRINT_METADATA_BYTES` | `64 * 1024` (64 KiB) | Caps JSON metadata |
| `MAX_BLUEPRINT_CONTENT_BYTES` | `32 * 1024 * 1024` (32 MiB) | Caps the snapshot payload |
| `MAX_OUTPUT_STRING_LENGTH` | `40` | Longest accepted output slug/noun (UI display limit, not a safety limit) |

The two size caps exist so a malformed archive cannot force unbounded allocation in the worker. The prefix reader throws `Unexpected end of gadget archive.` when the stream ends early, and `Archive content stream already opened.` if `readExact()` or `takeTail()` is called after the tail has been taken.

`sanitizeBlueprintOutput()` accepts a declared output format only if `id`, `noun`, and `plural` all pass `outputString()` (non-empty after trim, ≤ 40 chars) and `icon` passes `isOutputIcon()`. Anything else degrades to `undefined` — the blueprint is treated as declaring a generic app rather than reaching the UI with an unknown icon key.

<Warning>
The archive omits `ownerId`, `gadgetId`, and screenshot bytes. Imported archives clear any screenshot marker, because screenshots are stored separately from archive content.
</Warning>

## Public read surface

| RPC | Behavior |
| --- | --- |
| `PublicApi.getBlueprint(id)` | Returns `BlueprintPublicInfo` or `null`. No authentication — knowing the ID is sufficient. |
| `PublicApi.downloadBlueprint(id)` | Returns a `ReadableStream<Uint8Array>` of the `.gadget` archive. |
| `AuthenticatedApi.adminIsBlueprintFeatured()` | Admin-only. Whether a published blueprint is currently featured. |
| `AuthenticatedApi.adminSetBlueprintFeatured()` | Admin-only. Marks or unmarks a blueprint as featured. |

Admin usernames come from the backend worker's `ADMINS` binding, configured as an array of usernames. Only gadget-backed published blueprints are featureable; uploaded/imported library blueprints are intentionally excluded.

KV read helpers in `blueprint-archive.ts` narrow their env to exactly the bindings they need:

```ts
export type BlueprintKvEnv = Pick<Cloudflare.Env, 'BLUEPRINTS'>;

export async function readBlueprintKvRecord(
  env: BlueprintKvEnv,
  blueprintId: string,
): Promise<BlueprintKvRecord | null>
```

Dates round-trip through JSON as strings, so `reviveBlueprintMetadata()` re-hydrates `metadata.created` and `metadata.lastUpdated` into `Date` objects on every parse — including inside `parseFeaturedBlueprints()`, which revives each entry of the `.featured` list.

## Library, pinning, and explore

The home page blueprints tab lists blueprints the user has published plus what is in their library. Pinning keeps a blueprint at the top; pinning a public blueprint that is not already in the library adds it to the library first, then pins it.

| Library entry kind | Created by | Ownership | Removal effect |
| --- | --- | --- | --- |
| Saved by reference | `addBlueprintToLibrary()` | Blueprint stays owned by the original publisher; the entry caches public metadata for list rendering | Deletes only your personal library entry |
| Uploaded | `importBlueprint()` from a `.gadget` archive | New local blueprint ID on the current deployment, snapshot stored in this deployment's R2/KV, recorded with `uploaded: true` | Deletes the imported blueprint content as well |

The Explore page (`/explore`) lists featured blueprints, which admins control via the two admin RPCs above.

## Bundled format blueprints

`packages/workshop-backend/src/format-blueprints.ts` installs the deployment's bundled output-format blueprints. Archives and their presentation come from a directory chosen at build time (`scripts/build-format-blueprints.mjs`), so a fork ships its own formats by pointing `FORMAT_BLUEPRINTS_DIR` at its own tree.

Installation writes an ordinary blueprint — metadata into `BLUEPRINTS`, code snapshot into `BLUEPRINT_CONTENT` — exactly as publishing does. Nothing downstream knows these are special.

<Steps>
<Step title="Parse through the ordinary archive reader">
`installOne()` calls `parseBlueprintArchive()` on the base64-decoded bundle bytes, so a corrupt bundled file fails the same way an uploaded one would rather than producing a half-installed blueprint.
</Step>
<Step title="Verify declared length">
The content is buffered (it already lives in the Worker bundle) and its byte length is compared with the archive's declared `contentLength`. A mismatch throws `Archive declares N content bytes but holds M.`
</Step>
<Step title="Overlay sidecar presentation">
The archive supplies code, bindings, and export dates. `title`, `description`, `author`, and `output` are overwritten from the sidecar entry.
</Step>
<Step title="Write content before metadata">
`BLUEPRINT_CONTENT.put(\`${entry.blueprintId}/${installed.version}\`, contentBytes)` runs first. Metadata without an R2 object is broken; an orphaned R2 object is merely overwritten by the next install. The archive's content section is already gzip-compressed, which is exactly what R2 holds.
</Step>
<Step title="Write the KV record">
`BLUEPRINTS.put(entry.blueprintId, JSON.stringify({metadata: installed}))`. The `BlueprintKvRecord` has no `ownerId` and no `gadgetId`, marking it as deployment-installed.
</Step>
</Steps>

`installFormatBlueprints()` iterates every entry, pushing successes onto the returned `BlueprintPublicInfo[]` and logging failures via the `workshop.formats` logger with `event: "formats.install.ok"` or `"formats.install.failed"`. One bad archive does not deny the deployment the others; a deployment with none installed simply has no standard formats.

### Reinstall detection

`formatBlueprintsManifestVersion()` builds a comparison string from every bundled entry so any change triggers reinstallation:

```ts
// packages/workshop-backend/src/format-blueprints.ts
export function formatBlueprintsManifestVersion(): string {
  return FORMAT_BLUEPRINTS
      .map(e => `${e.blueprintId}@${e.revision}+` +
          fingerprint(JSON.stringify([e.title, e.description, e.author, e.output])))
      .toSorted()
      .join(",");
}
```

Presentation fields contribute alongside `revision`, because editing a description would otherwise build, deploy, and change nothing on a deployment that had already installed. `revision` covers the one input the fingerprint cannot see: the archive bytes.

## Related pages

<CardGroup cols={2}>
<Card title="Manage bundled format blueprints" href="/bundled-format-blueprints">
The `.gadget` plus `.json` sidecar split, `FORMAT_BLUEPRINTS_DIR`, generation into `src/generated/format-blueprints.ts`, and why a deployed `blueprintId` must never be renamed.
</Card>
<Card title="Sharing, roles, and observer re-verification" href="/sharing-and-observers">
Collaborator roles, the `use` allowlist, and share-link keys stored only as HMAC-SHA-256 hashes.
</Card>
<Card title="RPC API reference" href="/rpc-api-reference">
`PublicApi`, `AuthenticatedApi`, and `Overseer`, plus `GadgetMetadata`, `AgentSpawnerConfig`, and `AiModelConfig`.
</Card>
<Card title="Routing and worker bindings" href="/routing-and-bindings">
Where `BLUEPRINTS`, `BLUEPRINT_CONTENT`, and `/blueprint-screenshot/*` are wired into the router and backend.
</Card>
<Card title="Admin configuration reference" href="/admin-configuration">
The `AdminConfig` schema, the `AdminSettings` DO as sole writer, and the reserved `.adminConfig` KV key.
</Card>
<Card title="Gatekeeper protocol" href="/gatekeeper-protocol">
`GatekeeperVendor`, `ResourceDescription`, and URL-pattern matching behind blueprint gatekeeper bindings.
</Card>
</CardGroup>
