This code sample builds a web site on Azure to search through a catalog of books. Searchable content is indexed and queried on Azure AI Search, and the app runs on Azure Container Apps.
This sample includes a C# bulk-insert app, a C# Azure Functions API, and a React/Node.js web client.
| Component | Description |
|---|---|
| bulk-insert app | Creates and loads the "good-books" index on Azure AI Search. It demonstrates index creation and batch mode indexing. Sample data is loaded from the azure-search-sample-data repository. |
| client app | Provides the client code. The web front-end includes a search page with faceted navigation, a search bar for free form search and suggested queries, and tabbed page results. It's written in JavaScript, uses Node.js for the runtime, and uses React libraries for user interaction. |
| api | Provides the Azure Functions app used by the client to send queries to the search index. |
This README is a shortened version of the full tutorial and provides just the steps for running the sample. For more information and screenshots, see the tutorial.
- An active Azure subscription
- Azure Developer CLI
- Docker
- .NET 9
- Node.js 18.x LTS or later
- Git
Because deployment assigns Azure roles automatically, the deploying user needs permission to create role assignments on the target resource group — the Owner or User Access Administrator role (which grants Microsoft.Authorization/roleAssignments/write). Without it, azd up fails when it provisions the role assignments.
You don't need to hold any Azure AI Search data-plane roles in advance. During provisioning, main.bicep assigns Search Index Data Contributor and Search Service Contributor to both the app's managed identity (for keyless query access) and the deploying user (so the postprovision bulk-insert hook can create and populate the index).
For local development of the API or client:
The repository includes deterministic UI tests and live Azure AI Search integration tests. Maintainers can find the prerequisites, suite classifications, expected failures, visual review guidance, and protected workflow instructions in TESTING.md.
-
In a terminal, use git to clone this repository to your local computer:
git clone https://github.com/Azure-Samples/azure-search-static-web-app
-
Open that local directory in Visual Studio Code.
azd auth login
azd upThis provisions all Azure resources (Azure AI Search, Container Apps, Container Registry) and deploys both containers. The default uses keyless (managed identity) authentication — no code changes required.
The postprovision hook automatically runs bulk-insert to create and populate the good-books search index after infrastructure is provisioned.
Key environment variables (set automatically by azd from infra/main.bicepparam):
| Variable | Description |
|---|---|
SEARCH_SERVICE_NAME |
Name of the Azure AI Search service (bicep output, used by seed hook) |
SEARCH_INDEX_NAME |
Search index name (default: good-books) |
By default, the search service is configured with keyless (managed identity) authentication — disableLocalAuth: true is set on the search service, and the user-assigned managed identity receives Search data-plane role assignments. The server container app receives AZURE_CLIENT_ID and authenticates via DefaultAzureCredential. No code changes required.
Optional API key auth can be enabled by setting USE_KEYLESS_AUTH to false:
azd env set USE_KEYLESS_AUTH false
azd upWhen USE_KEYLESS_AUTH=false, the infra provisions the admin key as a container secret and sets SEARCH_USE_KEY_AUTH=true on the server container, which causes api/SearchClientFactory.cs to use AzureKeyCredential instead of DefaultAzureCredential.
The keyless authentication setup uses two distinct environment variables for different purposes and times:
-
Infrastructure Provisioning Time:
useKeylessAuth(Bicep parameter)- When:
azd upexecutes the Bicep template (infra/main.bicep) - Purpose: Determines whether the Azure AI Search service disables local auth and enables role-based access control (RBAC)
- Values:
true(default) orfalse - Effect:
true: SetsdisableLocalAuth: trueon the search service, requiring keyless auth; assigns Search RBAC roles to the managed identityfalse: Allows API key auth; provisions and injects the admin key as a container secret
- Set via:
azure.yamlhooks orazd env set/azd configduring provisioning
- When:
-
Runtime:
SEARCH_USE_KEY_AUTH(Application Environment Variable)- When: Application containers start (server and bulk-insert)
- Purpose: Switches the search credential strategy at runtime (keyless vs API key)
- Values:
true(use API key) or unset/false(use keyless/DefaultAzureCredential) - Effect:
SearchClientFactory.csreads this variable and selects the credential type- When
true: UsesAzureKeyCredentialwith the injected key - When
false/unset: UsesDefaultAzureCredentialwith the managed identity identified byAZURE_CLIENT_ID
- Set via: Bicep container app environment config
-
Managed Identity Identifier:
AZURE_CLIENT_ID(Infrastructure Provisioning Time)- When:
azd upsets this on the server container app (only whenuseKeylessAuth=true) - Purpose: Identifies which managed identity
DefaultAzureCredentialshould use when multiple identities exist on the host - Value: The user-assigned managed identity's client ID
- Usage:
api/SearchClientFactory.cspasses this toDefaultAzureCredential.CreateAsync()vianew ManagedIdentityClientId(clientId)
- When:
Azure AI Search provides two types of keys for different security contexts:
| Key Type | Purpose | Permissions | Scope | When to Use |
|---|---|---|---|---|
| Admin Key | Index and service management | Full: create/delete indexes, manage synonyms, configure analyzers, add/update/delete documents | All operations (data plane + admin) | Index creation, schema changes, bulk data loading (administrative) |
| Query Key | Data queries only | Read-only: execute search queries, retrieve documents | Data-plane queries only | Client-facing search API, public search interfaces, query operations |
Best Practice: Use separate keys for their intended purposes:
- Bulk-insert (index creation): Use the admin key (required for
CreateOrUpdateIndexAsync) - Search API (queries): Use a query key (principle of least privilege)
Current Implementation: This solution uses the admin key for both bulk-insert and search queries when key auth is enabled. This is a security anti-pattern — the query-facing API has more permissions than necessary. To improve security:
- Fetch the query key instead of the admin key in the
azure.yamlpostprovision hook (useaz search query-key listand select the first key) - Pass it to the server container as
SEARCH_QUERY_KEYinstead ofSEARCH_API_KEY - Update
api/SearchClientFactory.csto useSEARCH_QUERY_KEYfor search operations
How the credential is selected: The search API (
api/SearchClientFactory.cs) reads theSEARCH_USE_KEY_AUTHenvironment variable at runtime. When it istrue,AzureKeyCredentialis used with the injected key; otherwiseDefaultAzureCredentialis used (keyless default). The same logic applies to thebulk-insertseed hook. No code changes needed to switch modes.
Note
Why these deployment files
- Client runtime config (
docker-entrypoint.sh+/config.js+url-fetch.js): Vite bakesVITE_*env at BUILD time, but the backend's Container Apps FQDN isn't known until provisioning; the client reads the backend URL at container start via an injected/config.js(window.__APP_CONFIG__). Deployment wiring, not app logic. - bulk-insert azd seed hook: the sample needs a populated search index to function; wiring
bulk-insertas anazdpostprovision hook (reading connection info from the provisioned resources via env) auto-seeds the index onazd upinstead of manually editing placeholder constants.
To redeploy after code changes: azd deploy
After azd up completes, the CLI prints the URL for the client Container App. Open that URL in your browser, enter a search query such as code, and review the results.