diff --git a/examples/cloudflare-ai-search-namespace/.gitignore b/examples/cloudflare-ai-search-namespace/.gitignore
new file mode 100644
index 0000000000..cc54d25a17
--- /dev/null
+++ b/examples/cloudflare-ai-search-namespace/.gitignore
@@ -0,0 +1,3 @@
+
+# sst
+.sst
diff --git a/examples/cloudflare-ai-search-namespace/index.ts b/examples/cloudflare-ai-search-namespace/index.ts
new file mode 100644
index 0000000000..a6113f0a17
--- /dev/null
+++ b/examples/cloudflare-ai-search-namespace/index.ts
@@ -0,0 +1,87 @@
+import { Resource } from "sst";
+
+export default {
+ async fetch(req: Request) {
+ const url = new URL(req.url);
+ const path = url.pathname;
+
+ // List all instances in the namespace.
+ if (req.method === "GET" && path === "/instances") {
+ const result = await Resource.Search.list();
+ return Response.json(result);
+ }
+
+ // Create an instance: POST /instances { "id": "my-docs" }
+ if (req.method === "POST" && path === "/instances") {
+ const body = (await req.json()) as { id: string };
+ if (!body.id)
+ return Response.json({ error: "Missing 'id'" }, { status: 400 });
+ const instance = await Resource.Search.create({ id: body.id });
+ const info = await instance.info();
+ return Response.json(info, { status: 201 });
+ }
+
+ // Delete an instance: DELETE /instances/my-docs
+ const deleteMatch = path.match(/^\/instances\/([^/]+)$/);
+ if (req.method === "DELETE" && deleteMatch) {
+ await Resource.Search.delete(deleteMatch[1]);
+ return new Response(null, { status: 204 });
+ }
+
+ // Upload a document to a specific instance:
+ // PUT /instances/my-docs/items?filename=guide.md
+ const uploadMatch = path.match(/^\/instances\/([^/]+)\/items$/);
+ if (req.method === "PUT" && uploadMatch) {
+ const instance = Resource.Search.get(uploadMatch[1]);
+ const filename = url.searchParams.get("filename");
+ if (!filename || !req.body)
+ return Response.json(
+ { error: "Provide ?filename= and a request body" },
+ { status: 400 },
+ );
+ const item = await instance.items.uploadAndPoll(filename, req.body);
+ return Response.json(item, { status: 201 });
+ }
+
+ // Search a specific instance:
+ // GET /instances/my-docs/search?q=caching
+ const searchMatch = path.match(/^\/instances\/([^/]+)\/search$/);
+ if (req.method === "GET" && searchMatch) {
+ const instance = Resource.Search.get(searchMatch[1]);
+ const query = url.searchParams.get("q") ?? "";
+ const results = await instance.search({ query });
+ return Response.json(results);
+ }
+
+ // Search across multiple instances at once:
+ // GET /search?q=caching&instances=docs,blog
+ if (req.method === "GET" && path === "/search") {
+ const query = url.searchParams.get("q") ?? "";
+ const ids = (url.searchParams.get("instances") ?? "").split(",");
+ if (!ids.length)
+ return Response.json(
+ { error: "Provide ?instances=id1,id2" },
+ { status: 400 },
+ );
+ const results = await Resource.Search.search({
+ query,
+ ai_search_options: { instance_ids: ids },
+ });
+ return Response.json(results);
+ }
+
+ return Response.json(
+ {
+ routes: [
+ "GET /instances — list instances",
+ "POST /instances {id} — create instance",
+ "DELETE /instances/:name — delete instance",
+ "PUT /instances/:name/items?filename — upload document",
+ "GET /instances/:name/search?q= — search one instance",
+ "GET /search?q=&instances=a,b — search across instances",
+ ],
+ },
+ { status: 404 },
+ );
+ },
+};
diff --git a/examples/cloudflare-ai-search-namespace/package.json b/examples/cloudflare-ai-search-namespace/package.json
new file mode 100644
index 0000000000..4876011947
--- /dev/null
+++ b/examples/cloudflare-ai-search-namespace/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "cloudflare-ai-search-namespace",
+ "version": "0.0.0",
+ "dependencies": {
+ "sst": "file:../../sdk/js"
+ },
+ "devDependencies": {
+ "@cloudflare/workers-types": "^4.20260424.1"
+ }
+}
diff --git a/examples/cloudflare-ai-search-namespace/sst.config.ts b/examples/cloudflare-ai-search-namespace/sst.config.ts
new file mode 100644
index 0000000000..586ac9ae18
--- /dev/null
+++ b/examples/cloudflare-ai-search-namespace/sst.config.ts
@@ -0,0 +1,38 @@
+///
+
+/**
+ * ## Cloudflare AI Search Namespace
+ *
+ * Bind to an AI Search namespace to manage multiple instances at runtime. You
+ * can create, list, search, and delete instances dynamically — useful for
+ * multi-tenant apps or admin tools.
+ *
+ * Every Cloudflare account has a `default` namespace. You can also create
+ * custom namespaces to isolate groups of instances.
+ *
+ * For a simpler single-instance setup, see the `cloudflare-ai-search` example.
+ */
+export default $config({
+ app(input) {
+ return {
+ name: "cloudflare-ai-search-namespace",
+ removal: input?.stage === "production" ? "retain" : "remove",
+ home: "cloudflare",
+ };
+ },
+ async run() {
+ const search = new sst.cloudflare.AiSearch("Search", {
+ namespace: "default",
+ });
+
+ const worker = new sst.cloudflare.Worker("Worker", {
+ handler: "index.ts",
+ link: [search],
+ url: true,
+ });
+
+ return {
+ url: worker.url,
+ };
+ },
+});
diff --git a/examples/cloudflare-ai-search-namespace/tsconfig.json b/examples/cloudflare-ai-search-namespace/tsconfig.json
new file mode 100644
index 0000000000..9e26dfeeb6
--- /dev/null
+++ b/examples/cloudflare-ai-search-namespace/tsconfig.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/examples/cloudflare-ai-search/.gitignore b/examples/cloudflare-ai-search/.gitignore
new file mode 100644
index 0000000000..cc54d25a17
--- /dev/null
+++ b/examples/cloudflare-ai-search/.gitignore
@@ -0,0 +1,3 @@
+
+# sst
+.sst
diff --git a/examples/cloudflare-ai-search/index.ts b/examples/cloudflare-ai-search/index.ts
new file mode 100644
index 0000000000..6037af487e
--- /dev/null
+++ b/examples/cloudflare-ai-search/index.ts
@@ -0,0 +1,81 @@
+import { Resource } from "sst";
+
+export default {
+ async fetch(req: Request) {
+ const url = new URL(req.url);
+
+ // Upload a document: PUT /items?filename=guide.md
+ if (req.method === "PUT" && url.pathname === "/items") {
+ const filename = url.searchParams.get("filename");
+ if (!filename || !req.body)
+ return Response.json(
+ { error: "Provide ?filename= and a request body" },
+ { status: 400 },
+ );
+ const item = await Resource.Search.items.uploadAndPoll(
+ filename,
+ req.body,
+ );
+ return Response.json(item, { status: 201 });
+ }
+
+ // Search: GET /search?q=how+does+caching+work
+ if (req.method === "GET" && url.pathname === "/search") {
+ const query = url.searchParams.get("q") ?? "";
+ const results = await Resource.Search.search({ query });
+ return Response.json(results);
+ }
+
+ // Search with filters: GET /search?q=deploy&filter_field=category&filter_value=docs
+ // Demonstrates metadata filtering with Vectorize filter syntax.
+ if (req.method === "GET" && url.pathname === "/search/filtered") {
+ const query = url.searchParams.get("q") ?? "";
+ const field = url.searchParams.get("filter_field") ?? "";
+ const value = url.searchParams.get("filter_value") ?? "";
+ const results = await Resource.Search.search({
+ query,
+ ai_search_options: {
+ retrieval: {
+ filters: { [field]: value },
+ },
+ },
+ });
+ return Response.json(results);
+ }
+
+ // Chat completions: POST /chat { "question": "What is Cloudflare?" }
+ // Returns an AI-generated answer grounded in your indexed content.
+ if (req.method === "POST" && url.pathname === "/chat") {
+ const body = (await req.json()) as { question: string };
+ const answer = await Resource.Search.chatCompletions({
+ messages: [{ role: "user", content: body.question }],
+ });
+ return Response.json(answer);
+ }
+
+ // Streaming chat: POST /chat/stream { "question": "What is Cloudflare?" }
+ if (req.method === "POST" && url.pathname === "/chat/stream") {
+ const body = (await req.json()) as { question: string };
+ const stream = await Resource.Search.chatCompletions({
+ messages: [{ role: "user", content: body.question }],
+ stream: true,
+ });
+ return new Response(stream, {
+ headers: { "content-type": "text/event-stream" },
+ });
+ }
+
+ return Response.json(
+ {
+ routes: [
+ "PUT /items?filename= — upload a document (body = content)",
+ "GET /search?q= — search indexed content",
+ "GET /search/filtered?q=&... — search with metadata filters",
+ "POST /chat {question} — AI answer grounded in your content",
+ "POST /chat/stream {question} — streaming AI answer",
+ ],
+ },
+ { status: 404 },
+ );
+ },
+};
diff --git a/examples/cloudflare-ai-search/package.json b/examples/cloudflare-ai-search/package.json
new file mode 100644
index 0000000000..21d00e5827
--- /dev/null
+++ b/examples/cloudflare-ai-search/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "cloudflare-ai-search",
+ "version": "0.0.0",
+ "dependencies": {
+ "sst": "file:../../sdk/js"
+ },
+ "devDependencies": {
+ "@cloudflare/workers-types": "^4.20260424.1"
+ }
+}
diff --git a/examples/cloudflare-ai-search/sst.config.ts b/examples/cloudflare-ai-search/sst.config.ts
new file mode 100644
index 0000000000..4fb62bc1a6
--- /dev/null
+++ b/examples/cloudflare-ai-search/sst.config.ts
@@ -0,0 +1,36 @@
+///
+
+/**
+ * ## Cloudflare AI Search
+ *
+ * Bind to a single AI Search instance and link it to a worker. The instance
+ * must already exist in your Cloudflare account — you can create one in the
+ * dashboard or with the namespace binding example.
+ *
+ * Once linked, you can search your indexed content and get AI-generated
+ * answers using chat completions.
+ */
+export default $config({
+ app(input) {
+ return {
+ name: "cloudflare-ai-search",
+ removal: input?.stage === "production" ? "retain" : "remove",
+ home: "cloudflare",
+ };
+ },
+ async run() {
+ const search = new sst.cloudflare.AiSearch("Search", {
+ instance: "my-docs",
+ });
+
+ const worker = new sst.cloudflare.Worker("Worker", {
+ handler: "index.ts",
+ link: [search],
+ url: true,
+ });
+
+ return {
+ url: worker.url,
+ };
+ },
+});
diff --git a/examples/cloudflare-ai-search/tsconfig.json b/examples/cloudflare-ai-search/tsconfig.json
new file mode 100644
index 0000000000..9e26dfeeb6
--- /dev/null
+++ b/examples/cloudflare-ai-search/tsconfig.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/pkg/types/typescript/typescript.go b/pkg/types/typescript/typescript.go
index 345493ba30..c1465ae530 100644
--- a/pkg/types/typescript/typescript.go
+++ b/pkg/types/typescript/typescript.go
@@ -18,15 +18,17 @@ type WarningEvent struct {
}
var mapping = map[string]string{
- "aiBindings": "Ai",
- "r2BucketBindings": "R2Bucket",
- "d1DatabaseBindings": "D1Database",
- "kvNamespaceBindings": "KVNamespace",
- "queueBindings": "Queue",
- "serviceBindings": "Service",
- "hyperdriveBindings": "Hyperdrive",
- "versionMetadataBindings": "WorkerVersionMetadata",
- "workflowBindings": "Workflow",
+ "aiBindings": "Ai",
+ "r2BucketBindings": "R2Bucket",
+ "d1DatabaseBindings": "D1Database",
+ "kvNamespaceBindings": "KVNamespace",
+ "queueBindings": "Queue",
+ "serviceBindings": "Service",
+ "hyperdriveBindings": "Hyperdrive",
+ "versionMetadataBindings": "WorkerVersionMetadata",
+ "aiSearchBindings": "AiSearchInstance",
+ "aiSearchNamespaceBindings": "AiSearchNamespace",
+ "workflowBindings": "Workflow",
}
var header = strings.Join([]string{
diff --git a/platform/src/components/cloudflare/ai-search.ts b/platform/src/components/cloudflare/ai-search.ts
new file mode 100644
index 0000000000..b4aef67592
--- /dev/null
+++ b/platform/src/components/cloudflare/ai-search.ts
@@ -0,0 +1,216 @@
+import { ComponentResourceOptions, Output, output } from "@pulumi/pulumi";
+import { Component } from "../component";
+import { Link } from "../link";
+import { Input } from "../input";
+import { binding } from "./binding";
+
+export interface AiSearchArgs {
+ /**
+ * The name of the AI Search instance to bind to.
+ *
+ * Use this when you know which instance you need at deploy time. The instance
+ * must exist in the `default` namespace.
+ *
+ * You must specify either `instance` or `namespace`, but not both.
+ *
+ * @example
+ * ```ts title="sst.config.ts"
+ * const search = new sst.cloudflare.AiSearch("MySearch", {
+ * instance: "my-docs-index"
+ * });
+ * ```
+ */
+ instance?: Input;
+ /**
+ * The name of the AI Search namespace to bind to.
+ *
+ * Use this when you need access to multiple instances at runtime. You can
+ * get, create, list, and delete instances within the namespace.
+ *
+ * A default namespace is created automatically for every account. If the
+ * namespace does not exist, it will be created on deploy.
+ *
+ * You must specify either `instance` or `namespace`, but not both.
+ *
+ * @example
+ * ```ts title="sst.config.ts"
+ * const search = new sst.cloudflare.AiSearch("MySearch", {
+ * namespace: "my-namespace"
+ * });
+ * ```
+ */
+ namespace?: Input;
+}
+
+/**
+ * The `AiSearch` component lets you add a [Cloudflare AI Search](https://developers.cloudflare.com/ai-search/)
+ * binding to your app.
+ *
+ * AI Search is a managed search service. Connect a website, an R2 bucket, or upload
+ * your own documents, and AI Search indexes your content for natural language queries.
+ *
+ * There are two types of bindings:
+ *
+ * - **Instance binding**: Binds directly to a single AI Search instance. Use this when
+ * you know which instance you need at deploy time.
+ * - **Namespace binding**: Binds to a namespace that can contain multiple instances.
+ * Use this when you need to access or manage multiple instances at runtime.
+ *
+ * @example
+ *
+ * #### Instance binding
+ *
+ * Bind to a specific AI Search instance.
+ *
+ * ```ts title="sst.config.ts"
+ * const search = new sst.cloudflare.AiSearch("MySearch", {
+ * instance: "my-docs-index"
+ * });
+ * ```
+ *
+ * #### Namespace binding
+ *
+ * Bind to a namespace to access multiple instances.
+ *
+ * ```ts title="sst.config.ts"
+ * const search = new sst.cloudflare.AiSearch("MySearch", {
+ * namespace: "my-namespace"
+ * });
+ * ```
+ *
+ * #### Link to a worker
+ *
+ * You can link AI Search to a worker.
+ *
+ * ```ts {3} title="sst.config.ts"
+ * new sst.cloudflare.Worker("MyWorker", {
+ * handler: "./index.ts",
+ * link: [search],
+ * url: true
+ * });
+ * ```
+ *
+ * Once linked, you can use the binding to search your indexed content.
+ *
+ * For an **instance binding**, call methods directly:
+ *
+ * ```ts title="index.ts"
+ * const results = await env.MySearch.search({
+ * messages: [{ role: "user", content: "What is Cloudflare?" }]
+ * });
+ * ```
+ *
+ * For a **namespace binding**, get an instance handle first:
+ *
+ * ```ts title="index.ts"
+ * const instance = env.MySearch.get("my-docs-index");
+ * const results = await instance.search({
+ * messages: [{ role: "user", content: "What is Cloudflare?" }]
+ * });
+ * ```
+ */
+export class AiSearch extends Component implements Link.Linkable {
+ private _instance?: Output;
+ private _namespace?: Output;
+
+ constructor(
+ name: string,
+ args: AiSearchArgs,
+ opts?: ComponentResourceOptions,
+ ) {
+ super(__pulumiType, name, args, opts);
+
+ if (args.instance && args.namespace) {
+ throw new Error(
+ `Cannot specify both "instance" and "namespace" for AiSearch "${name}". Choose one.`,
+ );
+ }
+ if (!args.instance && !args.namespace) {
+ throw new Error(
+ `Must specify either "instance" or "namespace" for AiSearch "${name}".`,
+ );
+ }
+
+ if (args.instance) {
+ this._instance = output(args.instance);
+ }
+ if (args.namespace) {
+ this._namespace = output(args.namespace);
+ }
+ }
+
+ /**
+ * The name of the AI Search instance, if using an instance binding.
+ */
+ public get instance() {
+ return this._instance;
+ }
+
+ /**
+ * The name of the AI Search namespace, if using a namespace binding.
+ */
+ public get namespace() {
+ return this._namespace;
+ }
+
+ /**
+ * When you link AI Search, it will be available to the worker and you can
+ * interact with it using its binding methods.
+ *
+ * For an instance binding:
+ * @example
+ * ```ts title="index.ts"
+ * const results = await env.MySearch.search({
+ * messages: [{ role: "user", content: "What is Cloudflare?" }]
+ * });
+ * ```
+ *
+ * For a namespace binding:
+ * @example
+ * ```ts title="index.ts"
+ * const instance = env.MySearch.get("my-docs-index");
+ * const results = await instance.search({
+ * messages: [{ role: "user", content: "What is Cloudflare?" }]
+ * });
+ * ```
+ *
+ * @internal
+ */
+ getSSTLink() {
+ if (this._instance) {
+ const instanceName = this._instance;
+ return {
+ properties: {
+ instanceName,
+ },
+ include: [
+ binding({
+ type: "aiSearchBindings",
+ properties: {
+ instanceName,
+ },
+ }),
+ ],
+ };
+ }
+
+ const namespace = this._namespace!;
+ return {
+ properties: {
+ namespace,
+ },
+ include: [
+ binding({
+ type: "aiSearchNamespaceBindings",
+ properties: {
+ namespace,
+ },
+ }),
+ ],
+ };
+ }
+}
+
+const __pulumiType = "sst:cloudflare:AiSearch";
+// @ts-expect-error
+AiSearch.__pulumiType = __pulumiType;
diff --git a/platform/src/components/cloudflare/binding.ts b/platform/src/components/cloudflare/binding.ts
index cc901601f4..b277a76e12 100644
--- a/platform/src/components/cloudflare/binding.ts
+++ b/platform/src/components/cloudflare/binding.ts
@@ -79,6 +79,20 @@ export interface VersionMetadataBinding {
properties: Record;
}
+export interface AiSearchBinding {
+ type: "aiSearchBindings";
+ properties: {
+ instanceName: Input;
+ };
+}
+
+export interface AiSearchNamespaceBinding {
+ type: "aiSearchNamespaceBindings";
+ properties: {
+ namespace: Input;
+ };
+}
+
export interface WorkflowBinding {
type: "workflowBindings";
properties: {
@@ -99,6 +113,8 @@ export type Binding =
| D1DatabaseBinding
| HyperdriveBinding
| VersionMetadataBinding
+ | AiSearchBinding
+ | AiSearchNamespaceBinding
| WorkflowBinding;
export function binding(input: Binding & {}) {
diff --git a/platform/src/components/cloudflare/helpers/wrangler.ts b/platform/src/components/cloudflare/helpers/wrangler.ts
index 04658a19cf..99d0f56c50 100644
--- a/platform/src/components/cloudflare/helpers/wrangler.ts
+++ b/platform/src/components/cloudflare/helpers/wrangler.ts
@@ -52,6 +52,8 @@ export function createWranglerConfig(input: {
const hyperdrives: Record[] = [];
const services: Record[] = [];
const queueProducers: Record[] = [];
+ const aiSearch: Record[] = [];
+ const aiSearchNamespaces: Record[] = [];
const workflows: Record[] = [];
let ai: Record | undefined;
let versionMetadata: Record | undefined;
@@ -126,6 +128,20 @@ export function createWranglerConfig(input: {
binding: link.name,
};
break;
+ case "aiSearchBindings":
+ aiSearch.push({
+ binding: link.name,
+ instance_name: stringValue(properties.instanceName),
+ remote: true,
+ });
+ break;
+ case "aiSearchNamespaceBindings":
+ aiSearchNamespaces.push({
+ binding: link.name,
+ namespace: stringValue(properties.namespace),
+ remote: true,
+ });
+ break;
case "workflowBindings":
workflows.push({
binding: link.name,
@@ -167,6 +183,12 @@ export function createWranglerConfig(input: {
if (versionMetadata) {
config.version_metadata = versionMetadata;
}
+ if (aiSearch.length > 0) {
+ config.ai_search = aiSearch;
+ }
+ if (aiSearchNamespaces.length > 0) {
+ config.ai_search_namespaces = aiSearchNamespaces;
+ }
if (workflows.length > 0) {
config.workflows = workflows;
}
diff --git a/platform/src/components/cloudflare/index.ts b/platform/src/components/cloudflare/index.ts
index 0d571c5893..7acb80b968 100644
--- a/platform/src/components/cloudflare/index.ts
+++ b/platform/src/components/cloudflare/index.ts
@@ -10,6 +10,7 @@ export * from "./auth";
export * from "./queue";
export * from "./cron";
export * from "./ai";
+export * from "./ai-search";
export * from "./hyperdrive";
export * from "./astro";
export * from "./react-router";
diff --git a/platform/src/components/cloudflare/worker.ts b/platform/src/components/cloudflare/worker.ts
index 2b0e4f25be..f74e106f23 100644
--- a/platform/src/components/cloudflare/worker.ts
+++ b/platform/src/components/cloudflare/worker.ts
@@ -427,6 +427,8 @@ export class Worker extends Component implements Link.Linkable {
r2BucketBindings: "r2_bucket",
hyperdriveBindings: "hyperdrive",
versionMetadataBindings: "version_metadata",
+ aiSearchBindings: "ai_search",
+ aiSearchNamespaceBindings: "ai_search_namespace",
workflowBindings: "workflow",
}[b.binding],
name,