| 1 | --- |
| 2 | title: 2.0.0-beta.46 - Vectorize |
| 3 | date: 2026-05-31T08:00:00Z |
| 4 | category: release |
| 5 | excerpt: v2.0.0-beta.46 adds Cloudflare Vectorize — a bindable vector-database index plus metadata indexes for filtered queries — and fixes a state-store bootstrap bug that was silently rotating your encryption key and auth token on every update. |
| 6 | --- |
| 7 | |
| 8 | `v2.0.0-beta.46` ships **Cloudflare Vectorize** as a bindable |
| 9 | resource — index, metadata indexes, and an Effect-native runtime |
| 10 | client — plus an important fix to how the Cloudflare state store |
| 11 | bootstraps. |
| 12 | |
| 13 | ## `Cloudflare.VectorizeIndex` |
| 14 | |
| 15 | A `VectorizeIndex` is a globally distributed vector database |
| 16 | index. Declare one as a resource, bind it to a Worker, and you |
| 17 | get a typed Effect-native client for inserting and querying |
| 18 | vector embeddings. |
| 19 | |
| 20 | ```typescript |
| 21 | const index = yield* Cloudflare.VectorizeIndex("docs", { |
| 22 | dimensions: 768, |
| 23 | metric: "cosine", |
| 24 | }); |
| 25 | ``` |
| 26 | |
| 27 | Or pin `dimensions` and `metric` to a managed embedding model |
| 28 | with a `preset`: |
| 29 | |
| 30 | ```typescript |
| 31 | const index = yield* Cloudflare.VectorizeIndex("docs", { |
| 32 | preset: "@cf/baai/bge-base-en-v1.5", |
| 33 | }); |
| 34 | ``` |
| 35 | |
| 36 | The index is **immutable** — `dimensions`, `metric`, `preset`, |
| 37 | and `description` are all fixed at creation, so changing any of |
| 38 | them triggers a replacement. |
| 39 | |
| 40 | ## Bind it to a Worker |
| 41 | |
| 42 | `Cloudflare.VectorizeIndex.bind(index)` attaches the binding and |
| 43 | hands back a client whose methods — `upsert`, `query`, |
| 44 | `queryById`, `insert`, `deleteByIds`, `getByIds`, `describe` — |
| 45 | are all `yield*`-able Effects: |
| 46 | |
| 47 | ```typescript |
| 48 | export default class Worker extends Cloudflare.Worker<Worker>()( |
| 49 | "Worker", |
| 50 | { main: import.meta.filename }, |
| 51 | Effect.gen(function* () { |
| 52 | const docs = yield* Cloudflare.VectorizeIndex.bind(index); |
| 53 | |
| 54 | return { |
| 55 | fetch: Effect.gen(function* () { |
| 56 | yield* docs.upsert([ |
| 57 | { id: "1", values: [0.1, 0.2, 0.3], metadata: { kind: "doc" } }, |
| 58 | ]); |
| 59 | const matches = yield* docs.query([0.1, 0.2, 0.3], { topK: 5 }); |
| 60 | return yield* HttpServerResponse.json({ count: matches.count }); |
| 61 | }), |
| 62 | }; |
| 63 | }).pipe(Effect.provide(Cloudflare.VectorizeIndexBindingLive)), |
| 64 | ) {} |
| 65 | ``` |
| 66 | |
| 67 | ## `Cloudflare.VectorizeMetadataIndex` |
| 68 | |
| 69 | By default you can't `filter` a query on a metadata property — |
| 70 | the property has to be indexed first. `VectorizeMetadataIndex` |
| 71 | declares that index. Point it at a parent index, name the |
| 72 | property, and give its type: |
| 73 | |
| 74 | ```typescript |
| 75 | const index = yield* Cloudflare.VectorizeIndex("docs", { |
| 76 | dimensions: 768, |
| 77 | metric: "cosine", |
| 78 | }); |
| 79 | |
| 80 | yield* Cloudflare.VectorizeMetadataIndex("kind-index", { |
| 81 | indexName: index.indexName, |
| 82 | propertyName: "kind", |
| 83 | indexType: "string", |
| 84 | }); |
| 85 | ``` |
| 86 | |
| 87 | With the metadata index in place (and created before vectors are |
| 88 | inserted), `query` accepts a filter against that property: |
| 89 | |
| 90 | ```typescript |
| 91 | const matches = yield* docs.query([0.1, 0.2, 0.3], { |
| 92 | topK: 3, |
| 93 | filter: { kind: { $eq: "second" } }, |
| 94 | }); |
| 95 | ``` |
| 96 | |
| 97 | Metadata indexes are immutable too — changing the property name, |
| 98 | type, or parent index replaces them. |
| 99 | |
| 100 | Thanks **[David J. Felix](https://github.com/DavidJFelix)** and |
| 101 | **[John Royal](https://github.com/jdrydn)** for the |
| 102 | contribution. |
| 103 | ([#407](https://github.com/alchemy-run/alchemy/pull/407)) |
| 104 | |
| 105 | ## Fix: state-store bootstrap no longer rotates your keys |
| 106 | |
| 107 | :::caution |
| 108 | **Upgrade if you use the Cloudflare state store.** Prior versions |
| 109 | re-generated the state store's encryption key and auth token on |
| 110 | every bootstrap/update, because the bootstrap path always started |
| 111 | from fresh local state. A rotated encryption key means previously |
| 112 | persisted state can no longer be decrypted. |
| 113 | ::: |
| 114 | |
| 115 | The bug: updating the state store stack always used a fresh local |
| 116 | state, so the auth token — and, worse, the **encryption key** — |
| 117 | rotated on every run. beta.46 fixes the bootstrap to read from |
| 118 | **remote state**, which already holds the auth token and |
| 119 | encryption key, so neither is regenerated. You can run |
| 120 | |
| 121 | ```sh |
| 122 | bun alchemy cloudflare bootstrap --force |
| 123 | ``` |
| 124 | |
| 125 | to force an update of everything against the real state without |
| 126 | rotating the keys. |
| 127 | |
| 128 | The same change hardens a few adjacent failure modes: |
| 129 | |
| 130 | - **Decrypt failures degrade gracefully** — a failed decrypt now |
| 131 | returns `undefined` instead of throwing. |
| 132 | - **Invalid tokens self-heal** — an invalid auth token is |
| 133 | detected and refreshed. |
| 134 | - **Missing store recovers** — if the profile thinks the state |
| 135 | store exists but it's actually gone, it's recreated. |
| 136 | |
| 137 | ([#477](https://github.com/alchemy-run/alchemy/pull/477)) |
| 138 | |
| 139 | ## Where to go next |
| 140 | |
| 141 | - [Vectorize on Cloudflare](https://developers.cloudflare.com/vectorize/) |
| 142 | - [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta46) |
| 143 | - [Compare v2.0.0-beta.45 → v2.0.0-beta.46](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.45...v2.0.0-beta.46) |