diff --git a/docs/devops/AZURE_KEY_VAULT_AND_SECRETS_ROTATION.md b/docs/devops/AZURE_KEY_VAULT_AND_SECRETS_ROTATION.md new file mode 100644 index 00000000..5c8248a5 --- /dev/null +++ b/docs/devops/AZURE_KEY_VAULT_AND_SECRETS_ROTATION.md @@ -0,0 +1,265 @@ +# BytelystAI — Azure Key Vault & Secrets Rotation (MindLyst + LysnrAI) + +> **Purpose:** Centralize **all secrets** in Azure Key Vault and establish a repeatable rotation process. +> **Scope:** Staging first (current RG: `rg-mywisprai`, vault: `kv-mywisprai`), then production. +> **Last updated:** 2026-02-14 + +--- + +## Goals + +- Use **Azure Key Vault** as the source of truth for secrets (no secrets in docs, code, or git). +- Make secret rotation **low/no-downtime** using primary/secondary keys where available. +- Have a single checklist-driven runbook for rotation and incident response. + +## Non-Goals (for now) + +- Full CI/CD + automated rotation (we’ll outline it, but can implement later). +- Client-side secret usage (mobile apps should not embed service keys). + +--- + +## Current State (Staging) + +- Resource Group: `rg-mywisprai` +- Key Vault: `kv-mywisprai` +- MindLyst Azure secrets are centralized in Key Vault under `mindlyst-*` (see “Secret Inventory” below). +- `docs/WINDSURF/AZURE_PORTAL_SETUP.md` does not contain secret values; it references Key Vault secret names and includes scripts to pull values when needed. + +> Because secrets have been committed into git at least once, treat this as a **compromise** and rotate keys ASAP. + +### Rotation TODO (Deferred) + +As of **2026-02-14**, secrets were moved into Key Vault, but the underlying service keys have **not** been rotated yet. + +- [ ] Rotate Cosmos DB keys (`cosmos-mywisprai`) and update `mindlyst-cosmos-key` +- [ ] Rotate Storage account keys (`bytelystblobs`) and update `mindlyst-blob-connection-string` +- [ ] Rotate Azure OpenAI keys (`mywisprai-openai-sweden`) and update `mindlyst-openai-key` +- [ ] Rotate Speech keys (`mywisprai-speech`) and update `mindlyst-speech-key` +- [ ] Rotate Notification Hub SAS keys (`lysnnai`/`mindlyst-hub`) and update `mindlyst-notification-hub-connection-string` +- [ ] If treating App Insights connection string as leaked: create a new App Insights component and update `mindlyst-appinsights-connection-string` + +--- + +## Target State + +### 1) Secret Sources + +- **All secrets** stored in Key Vault (`kv-`). +- Apps receive secrets via: + - Azure compute platform **Key Vault references** (preferred), or + - Environment variables injected at deploy time from Key Vault (acceptable), or + - A runtime secret loader using Managed Identity (only if platform references aren’t available). + +### 2) Environment Separation + +- Separate vaults per environment: + - `kv-bytelyst-staging` + - `kv-bytelyst-prod` + +### 3) No Secrets in Repo + +- `docs/WINDSURF/AZURE_PORTAL_SETUP.md` should contain: + - Resource names, endpoints, regions, container names, partition keys + - **Key Vault secret names** (not values) + - Commands/scripts to fetch values when needed + +--- + +## Secret Inventory (Canonical) + +Use these as the canonical secret names. If names already exist, keep them; otherwise create them. + +### MindLyst (server-side) + +| Category | Env var(s) today | Key Vault secret name | Notes | +|---|---|---|---| +| Cosmos | `COSMOS_KEY` | `mindlyst-cosmos-key` | Rotate using primary/secondary keys | +| Cosmos | `COSMOS_ENDPOINT` | `mindlyst-cosmos-endpoint` | Not secret, but ok to store | +| Cosmos | `COSMOS_DATABASE` | `mindlyst-cosmos-database` | Not secret, but ok to store | +| OpenAI | `AZURE_OPENAI_KEY` | `mindlyst-openai-key` | Rotate using key1/key2 | +| OpenAI | `AZURE_OPENAI_ENDPOINT` | `mindlyst-openai-endpoint` | Config | +| OpenAI | `AZURE_OPENAI_DEPLOYMENT` | `mindlyst-openai-deployment` | Config | +| OpenAI | `AZURE_OPENAI_API_VERSION` | `mindlyst-openai-api-version` | Config | +| Speech | `AZURE_SPEECH_KEY` | `mindlyst-speech-key` | Rotate using key1/key2 | +| Speech | `AZURE_SPEECH_REGION` | `mindlyst-speech-region` | Config | +| Blob | `AZURE_BLOB_CONNECTION_STRING` | `mindlyst-blob-connection-string` | Prefer SAS later; keys rotate key1/key2 | +| Notifications | `ANH_CONNECTION_STRING` | `mindlyst-notification-hub-connection-string` | Rotate SAS keys on the auth rule | +| Insights | `APPLICATIONINSIGHTS_CONNECTION_STRING` | `mindlyst-appinsights-connection-string` | If leaked, easiest “rotation” is new App Insights resource | +| Stripe | `STRIPE_SECRET_KEY` | `mindlyst-stripe-secret-key` | Not Azure; still belongs in Key Vault | +| Stripe | `STRIPE_WEBHOOK_SECRET` | `mindlyst-stripe-webhook-secret` | Not Azure; still belongs in Key Vault | + +### LysnrAI (server-side) + +Keep existing `wispr-*` secrets as-is, but standardize any missing ones to the same pattern: + +- `wispr-azure-openai-endpoint` +- `wispr-azure-openai-key` +- `wispr-azure-openai-deployment` +- `wispr-azure-speech-key` +- `wispr-azure-speech-region` + +--- + +## Access Model (Recommended) + +Pick one model and stick to it per environment. + +### Option A: Key Vault RBAC (recommended) + +- Enable `enableRbacAuthorization=true` on the vault +- Grant: + - Humans: `Key Vault Secrets Officer` (or least privilege) for provisioning + - Apps (Managed Identity): `Key Vault Secrets User` for runtime reads +- Advantage: consistent with Azure RBAC governance + +### Option B: Access Policies (simple for small teams) + +- Keep access policies and grant explicit secret permissions to: + - The deployment user/service principal + - The app’s managed identity +- Advantage: very explicit, fewer moving parts + +--- + +## Migration Plan (Staging First) + +### Phase 0: Stop The Bleed (Immediate) + +- [ ] Rotate all secrets that have appeared in git history (see “Rotation Runbooks”). +- [ ] Reduce Key Vault blast radius: + - [ ] Enable Key Vault diagnostics to Log Analytics + - [ ] Review Key Vault access policies / RBAC assignments +- [x] Add missing MindLyst secrets to Key Vault: + - [x] `mindlyst-openai-endpoint` + - [x] `mindlyst-openai-deployment` + - [x] `mindlyst-openai-api-version` + - [x] `mindlyst-speech-region` + - [x] `mindlyst-notification-hub-connection-string` + - [x] `mindlyst-appinsights-connection-string` +- [x] Docs hygiene: + - [x] Remove plaintext secrets from `docs/WINDSURF/AZURE_PORTAL_SETUP.md` (leave names + scripts) + - [x] Add a short “fetch from Key Vault” snippet per secret category + +### Phase 1: App Integration (Hosted Environments) + +- [ ] Decide hosting target(s): + - [ ] MindLyst web: App Service / Container Apps / other + - [ ] LysnrAI backend: where it runs +- [ ] For each app, configure a Managed Identity and grant Key Vault read access: + - [ ] MindLyst web runtime identity can read `mindlyst-*` secrets + - [ ] LysnrAI runtime identity can read `wispr-*` secrets +- [ ] Inject secrets into the app runtime: + - [ ] Prefer platform Key Vault references (App Service Key Vault reference, Container Apps secrets from Key Vault) + - [ ] Fall back to deployment-time env var injection from Key Vault +- [ ] Validate with an end-to-end smoke test: + - [ ] `/api/triage` (Azure OpenAI) + - [ ] `/api/brain-chat` (Azure OpenAI) + - [ ] `/api/memory` + `/api/brains` (Cosmos) + +### Phase 2: Operationalize (Rotation + Audits) + +- [ ] Define rotation cadence: + - [ ] Staging: monthly (or after any leak) + - [ ] Prod: quarterly (or after any leak) +- [ ] Add a “rotation log”: + - [ ] Create `docs/WINDSURF/SECRETS_ROTATION_LOG.md` (optional) and record dates/owners +- [ ] Add automated checks: + - [ ] Secret scanning in CI (git-secrets / gitleaks) + - [x] Block commits containing patterns like `AccountKey=` / `SharedAccessKey=` (Husky: `scripts/secret-scan-staged.sh`) + +--- + +## Rotation Runbooks + +General rule: rotate by switching to the **secondary** key first (if supported), regenerate the primary, then switch back. + +### Cosmos DB Key Rotation (Serverless) + +Supports primary/secondary keys. + +- [ ] Determine current key used by apps (usually “primary”). +- [ ] Update Key Vault `mindlyst-cosmos-key` to the **secondary** Cosmos key. +- [ ] Redeploy / restart apps that use Cosmos. +- [ ] Verify Cosmos reads/writes: + - [ ] `GET /api/memory` + - [ ] `POST /api/memory` +- [ ] Regenerate the Cosmos **primary** key in Azure. +- [ ] Update Key Vault `mindlyst-cosmos-key` back to the new primary (or keep secondary as active if you prefer). +- [ ] Redeploy / restart again. + +Rollback: switch Key Vault secret back to the other key and restart. + +### Storage Account Key Rotation + +Supports key1/key2. + +- [ ] Update Key Vault `mindlyst-blob-connection-string` to use the **other** storage key. +- [ ] Redeploy / restart apps that use Blob. +- [ ] Regenerate the original key in Azure. +- [ ] Update Key Vault secret again (optional switch-back). + +### Azure OpenAI Key Rotation + +Supports key1/key2. + +- [ ] Update Key Vault `mindlyst-openai-key` to use the **other** key. +- [ ] Verify `/api/triage` and `/api/brain-chat`. +- [ ] Regenerate the old key in Azure. + +### Speech Key Rotation + +Supports key1/key2. + +- [ ] Update Key Vault `mindlyst-speech-key` to use the **other** key. +- [ ] Verify STT/TTS pipeline (when implemented). +- [ ] Regenerate the old key in Azure. + +### Notification Hub SAS Key Rotation + +Rotate the SAS keys on the authorization rule used by `ANH_CONNECTION_STRING`. + +- [ ] Switch to the **secondary** connection string for the auth rule (update Key Vault `mindlyst-notification-hub-connection-string`). +- [ ] Verify push send (server-side) and device receive (once implemented). +- [ ] Regenerate the primary SAS key on the auth rule. +- [ ] Switch back if desired. + +### Application Insights “Rotation” + +App Insights doesn’t have an easy “rotate key” flow that’s comparable to key1/key2 (treat the connection string as sensitive anyway). + +Preferred approach if leaked: + +- [ ] Create a new App Insights resource (workspace-based recommended). +- [ ] Update Key Vault `mindlyst-appinsights-connection-string`. +- [ ] Redeploy apps. +- [ ] Keep old App Insights temporarily for historical traces, then delete if desired. + +### Stripe Keys Rotation (External) + +- [ ] Rotate keys in Stripe dashboard. +- [ ] Update Key Vault `mindlyst-stripe-secret-key` / `mindlyst-stripe-webhook-secret`. +- [ ] Redeploy apps. + +--- + +## Implementation Tasks (Checkbox List) + +### Key Vault Completion (Staging) + +- [ ] Add missing `mindlyst-*` secrets to `kv-mywisprai` (see inventory) +- [ ] Add missing `wispr-*` secrets to `kv-mywisprai` (if needed) +- [ ] Ensure the `mindlyst-*` secret names match what Bicep creates (`infra/azure/bytelyst-shared/`) + +### App Config Changes + +- [ ] Add a “Key Vault-first” config strategy for hosted environments: + - [ ] App Service Key Vault references (if using App Service) + - [ ] Container Apps secrets from Key Vault (if using Container Apps) +- [ ] Ensure no app requires a secret at build-time (only at runtime) + +### Repo Hygiene + +- [ ] After rotation, remove plaintext secret values from `docs/WINDSURF/AZURE_PORTAL_SETUP.md` +- [ ] Add CI secret scanning +- [ ] (Optional) Rewrite git history to remove old secrets (only if you’re prepared for the repo impact) diff --git a/docs/devops/AZURE_PORTAL_SETUP.md b/docs/devops/AZURE_PORTAL_SETUP.md new file mode 100644 index 00000000..1b3d457e --- /dev/null +++ b/docs/devops/AZURE_PORTAL_SETUP.md @@ -0,0 +1,917 @@ +# BytelystAI — Azure Portal Setup Guide (Shared Infrastructure) + +> **Purpose:** Step-by-step instructions to provision and manage Azure infrastructure shared between **LysnrAI** and **MindLyst** under a single resource group. +> **Architecture:** Single RG, shared stateless services, separate databases. +> **Time estimate:** ~20 minutes (most resources already exist from LysnrAI) +> **Prerequisites:** An Azure account with an active subscription +> **Last updated:** 2026-02-14 +> +> **Security note (staging):** This document does **not** include secret values. Store secrets in Azure Key Vault (`kv-mywisprai`) and reference them by name (see **MindLyst Environment Variables** and **Key Vault** sections). Rotation is deferred; see `docs/WINDSURF/AZURE_KEY_VAULT_AND_SECRETS_ROTATION.md`. + +--- + +## Table of Contents + +- [Architecture Overview](#architecture-overview) +- [Existing Resources (Staging / Shared)](#existing-resources-staging--shared) +- [What to Add for MindLyst](#what-to-add-for-mindlyst) +- [Step 1: Resource Group](#step-1-resource-group) +- [Step 2: Cosmos DB — Add MindLyst Database](#step-2-cosmos-db--add-mindlyst-database) +- [Step 3: Blob Storage — Add MindLyst Containers](#step-3-blob-storage--add-mindlyst-containers) +- [Step 4: Azure OpenAI — Reuse Existing](#step-4-azure-openai--reuse-existing) +- [Step 5: Speech Service — Reuse Existing](#step-5-speech-service--reuse-existing) +- [Step 6: Key Vault — Add MindLyst Secrets](#step-6-key-vault--add-mindlyst-secrets) +- [Step 7: Notification Hub — Add MindLyst Hub](#step-7-notification-hub--add-mindlyst-hub) +- [Step 8: Application Insights — Reuse or Create](#step-8-application-insights--reuse-or-create) +- [MindLyst Environment Variables](#mindlyst-environment-variables) +- [Verification Checklist](#verification-checklist) +- [Cost Estimates](#cost-estimates) +- [Resource Naming Convention](#resource-naming-convention) +- [Az CLI Script (Copy-Paste-Ready)](#az-cli-script-copy-paste-ready) +- [Bicep IaC (Replicate To Another Account)](#bicep-iac-replicate-to-another-account) + +--- + +## Architecture Overview + +Both LysnrAI and MindLyst share a **single Azure resource group** with shared stateless services and separate data stores. + +``` +rg-mywisprai/ (1 resource group) +│ +├── cosmos-mywisprai (1 Cosmos DB account — already exists) +│ ├── mywisprai (database: 10 containers — existing) +│ └── mindlyst (database: 12 containers — MindLyst) +│ +├── bytelystblobs (1 Storage account — already exists) +│ ├── mindlyst-voice ← NEW +│ ├── mindlyst-images ← NEW +│ └── mindlyst-exports ← NEW +│ +├── mywisprai-openai-sweden (1 Azure OpenAI — shared, swedencentral) +│ └── gpt-4o-mini (1 deployment — shared by both apps) +│ +├── mywisprai-speech (1 Speech Service — shared, eastus) +│ +├── kv-mywisprai (1 Key Vault — shared) +│ ├── wispr-* (existing secrets) +│ └── mindlyst-* (MindLyst secrets) ← NEW +│ +├── lysnnai (1 Notification Hub namespace) +│ ├── notificationhub (existing hub) +│ └── mindlyst-hub (MindLyst APNs + FCM) ← NEW +│ +└── bytelyst-appinsights (1 Application Insights) ← NEW +``` + +**Total: 8 Azure resources** instead of 14+ with separate resource groups. + +### Why Share? + +| Concern | Answer | +|---------|--------| +| **Cost** | Cosmos DB Serverless charges per-RU, not per-account. One account = one endpoint, one key pair, simpler rotation | +| **Management** | 1 RG to monitor, 1 cost dashboard, 1 health check | +| **Data isolation** | Separate databases within Cosmos = full container-level isolation. Queries can't cross databases | +| **Teardown safety** | Delete the `mindlyst` database → LysnrAI data untouched | +| **Stateless services** | OpenAI & Speech are stateless — same key serves both apps, no data leakage | + +### What Must Stay Separate + +| Resource | Why | +|----------|-----| +| **Notification Hub hubs** | Each app has its own APNs certificate and FCM server key | +| **Stripe accounts** | Different products, pricing tiers, and webhook endpoints (external, not Azure) | +| **Cosmos databases** | Each app gets its own database with its own set of containers | + +--- + +## Existing Resources (Staging / Shared) + +These resources are **already provisioned** and will be reused by MindLyst: + +| Resource | Azure Name | Region | SKU | Status | +|----------|-----------|--------|-----|--------| +| **Resource Group** | `rg-mywisprai` | `eastus` | — | Exists | +| **Cosmos DB** | `cosmos-mywisprai` | `westus2` | Serverless, NoSQL | Exists — DBs: `mywisprai` (10 containers), `mindlyst` (12 containers) | +| **Blob Storage** | `bytelystblobs` | `westus2` | StorageV2, RAGRS | Exists | +| **Azure OpenAI** | `mywisprai-openai-sweden` | `swedencentral` | S0 | Exists — deployment: `gpt-4o-mini` | +| **Speech Service** | `mywisprai-speech` | `eastus` | F0 | Exists | +| **Key Vault** | `kv-mywisprai` | `eastus` | Standard | Exists | +| **Notification Hub (namespace)** | `lysnnai` | `eastus` | Free | Exists (hubs: `notificationhub`, `mindlyst-hub`) | +| **Application Insights** | `bytelyst-appinsights` | `eastus` | Classic | Exists | + +### Cosmos DB: Existing LysnrAI database containers (staging) + +Database: `mywisprai` (existing) + +- `api_tokens` +- `audit_log` +- `devices` +- `licenses` +- `payments` +- `settings` +- `subscriptions` +- `transcripts` +- `usage_daily` +- `users` + +> Partition keys for these existing containers are not documented here. Check **Cosmos DB → Data Explorer → Container → Settings** if you need to replicate LysnrAI data layout. + +### Key Vault secrets (staging) + +Source of truth: Key Vault `kv-mywisprai` in `rg-mywisprai`. + +MindLyst secrets (canonical): + +- `mindlyst-cosmos-endpoint` +- `mindlyst-cosmos-key` +- `mindlyst-cosmos-database` +- `mindlyst-openai-endpoint` +- `mindlyst-openai-key` +- `mindlyst-openai-deployment` +- `mindlyst-openai-api-version` +- `mindlyst-speech-key` +- `mindlyst-speech-region` +- `mindlyst-blob-connection-string` +- `mindlyst-notification-hub-connection-string` +- `mindlyst-appinsights-connection-string` + +LysnrAI secrets (existing): + +- `wispr-azure-openai-deployment` +- `wispr-azure-openai-endpoint` +- `wispr-azure-openai-key` +- `wispr-azure-speech-key` +- `wispr-azure-speech-region` + +List what’s currently in the vault: + +```bash +az keyvault secret list --vault-name kv-mywisprai --query "[].name" -o tsv +``` + +--- + +## What to Add for MindLyst + +Only these additions are needed — no new accounts to create: + +| Action | Where | What | +|--------|-------|------| +| **Rename RG (optional)** | Resource Group | Keep `rg-mywisprai` for now. Only move resources if you want a cleaner name. | +| **Add database** | Cosmos DB (`cosmos-mywisprai`) | New database `mindlyst` with 12 containers | +| **Add blob containers** | Blob Storage (`bytelystblobs`) | 3 new containers: `mindlyst-voice`, `mindlyst-images`, `mindlyst-exports` | +| **Add secrets** | Key Vault (`kv-mywisprai`) | 12 secrets with `mindlyst-` prefix (keys + config) | +| **Add hub** | Notification Hub namespace (`lysnnai`) | New hub `mindlyst-hub` | +| **Create App Insights** | New resource | `bytelyst-appinsights` (shared telemetry) | + +### Current Staging State (Completed) + +As of **2026-02-14**, the MindLyst staging infra is provisioned in `rg-mywisprai`: + +- **Cosmos DB:** `cosmos-mywisprai` → database `mindlyst` with 12 containers +- **Blob Storage:** `bytelystblobs` → containers `mindlyst-voice`, `mindlyst-images`, `mindlyst-exports` +- **Key Vault:** `kv-mywisprai` → secrets `mindlyst-*` +- **Notification Hubs:** namespace `lysnnai` → hub `mindlyst-hub` +- **App Insights:** `bytelyst-appinsights` + +--- + +## Step 1: Resource Group + +### Option A: Create New RG + Move Resources (Optional) + +> Azure doesn't support direct rename. You must **move resources** to a new RG. +> +> **Caution:** Moving resources is safe when done carefully, but it can have RBAC/policy implications in larger org setups. For staging, consider skipping this until you have a clear reason. + +1. Go to [portal.azure.com](https://portal.azure.com) → **Resource groups** → search `rg-mywisprai` +2. Click on the resource group → **Overview** +3. **Select all resources** (checkbox at top of list) +4. Click **Move** → **Move to another resource group** +5. Click **Create new** → name it **`rg-bytelyst`** → same subscription +6. Click **Next** → wait for validation → click **Move** +7. Wait for move to complete (~5–10 minutes) +8. Delete the empty `rg-mywisprai` resource group + +### Option B: Keep Existing Name + +If you prefer not to move, keep `rg-mywisprai` and add MindLyst resources there. All instructions below assume the RG name — substitute as needed. + +> **Note:** Renaming is cosmetic. All connection strings and keys remain the same regardless of RG name. + +--- + +## Step 2: Cosmos DB — Add MindLyst Database + +> Reusing the existing `cosmos-mywisprai` account. Just add a new database and containers. + +### 2a. Create the MindLyst database + +1. Go to **Azure Cosmos DB** → click **`cosmos-mywisprai`** +2. Click **Data Explorer** (left sidebar) +3. Click **New Database** (top toolbar) + - **Database id:** `mindlyst` +4. Click **OK** + +You should now see both `mywisprai` and `mindlyst` databases in the tree. + +### 2b. Create all 12 containers + +For each container below, right-click the **`mindlyst`** database (or click the `…` menu) → **New Container**: + +| # | Container Name | Partition Key | Purpose | +|---|----------------|--------------|---------| +| 1 | `users` | `/id` | User accounts and profiles | +| 2 | `brains` | `/userId` | User brain configurations (War Room, Home Base, Money Guard, etc.) | +| 3 | `brain_templates` | `/id` | Brain Pack gallery templates | +| 4 | `memory_items` | `/userId` | All captured memories (text, voice, image, link, email) | +| 5 | `actions` | `/memoryItemId` | AI-suggested actions from triage | +| 6 | `entities` | `/memoryItemId` | Extracted entities (people, dates, places, amounts) | +| 7 | `reflections` | `/userId` | Weekly reflection reports and insights | +| 8 | `share_cards` | `/userId` | Generated share cards for social sharing | +| 9 | `daily_briefs` | `/userId` | Morning and evening brief content | +| 10 | `streaks` | `/userId` | Capture streak tracking and gamification | +| 11 | `notification_log` | `/userId` | Push notification delivery log and scheduling | +| 12 | `brain_insights` | `/userId` | Cross-brain insight discoveries | + +**For each container:** +1. Click **New Container** +2. **Existing Database:** select `mindlyst` (do NOT create a new database) +3. Enter the **Container id** from the table +4. Enter the **Partition key** from the table (include the `/` prefix) +5. **Throughput:** leave as **Serverless** (auto — no provisioned RUs) +6. Click **OK** + +Repeat for all 12 containers. Takes ~3 minutes. + +### 2c. Verify + +In **Data Explorer**, expand the `mindlyst` database — you should see all 12 containers listed. + +### Connection details (from Key Vault) + +Cosmos connection details are stored in Key Vault: + +- `mindlyst-cosmos-endpoint` → `COSMOS_ENDPOINT` +- `mindlyst-cosmos-key` → `COSMOS_KEY` +- `mindlyst-cosmos-database` → `COSMOS_DATABASE` (should be `mindlyst`) + +--- + +## Step 3: Blob Storage — Add MindLyst Containers + +> Reusing the existing `bytelystblobs` storage account. Just add 3 new containers. + +1. Go to **Storage accounts** → click **`bytelystblobs`** +2. Go to **Data storage** → **Containers** (left sidebar) +3. Click **+ Container** for each: + +| Container Name | Access Level | Purpose | +|----------------|-------------|---------| +| `mindlyst-voice` | **Private** | Voice capture recordings (WAV/PCM from STT pipeline) | +| `mindlyst-images` | **Private** | Captured photos, screenshots, and OCR source images | +| `mindlyst-exports` | **Private** | User data exports (JSON), weekly reflection PDFs | + +For each: +1. Click **+ Container** +2. Enter the **Name** (must be lowercase, no underscores — use hyphens) +3. **Anonymous access level:** leave as **Private (no anonymous access)** +4. Click **Create** + +### Verify + +You should now see these MindLyst containers in `bytelystblobs` (plus any other app containers you already had): + +| Container | App | +|-----------|-----| +| `mindlyst-voice` | MindLyst | +| `mindlyst-images` | MindLyst | +| `mindlyst-exports` | MindLyst | + +### Connection details (from Key Vault) + +Blob connection details are stored in Key Vault: + +- `mindlyst-blob-connection-string` → `AZURE_BLOB_CONNECTION_STRING` + +Non-secret config: + +- `AZURE_BLOB_ACCOUNT_NAME=bytelystblobs` +- Containers: `mindlyst-voice`, `mindlyst-images`, `mindlyst-exports` + +--- + +## Step 4: Azure OpenAI — Reuse Existing + +> No action needed. The existing `mywisprai-openai-sweden` resource with `gpt-4o-mini` deployment serves both apps. + +### Connection details (from Key Vault) + +Azure OpenAI connection details are stored in Key Vault: + +- `mindlyst-openai-endpoint` → `AZURE_OPENAI_ENDPOINT` +- `mindlyst-openai-key` → `AZURE_OPENAI_KEY` +- `mindlyst-openai-deployment` → `AZURE_OPENAI_DEPLOYMENT` (staging: `gpt-4o-mini`) +- `mindlyst-openai-api-version` → `AZURE_OPENAI_API_VERSION` (default: `2024-06-01`) + +MindLyst web should set: + +- `OPENAI_PROVIDER=azure` + +--- + +## Step 5: Speech Service — Reuse Existing + +> No action needed. The existing `mywisprai-speech` (F0 free tier, eastus) serves both apps. + +### Connection details (from Key Vault) + +Speech connection details are stored in Key Vault: + +- `mindlyst-speech-key` → `AZURE_SPEECH_KEY` +- `mindlyst-speech-region` → `AZURE_SPEECH_REGION` + +> **Note:** F0 free tier has a 5-hour/month STT limit. If both apps start consuming real audio, upgrade to S0 ($1/audio hour). + +--- + +## Step 6: Key Vault — MindLyst Secrets + +> Staging source of truth: `kv-mywisprai` in `rg-mywisprai`. +> +> For local dev, pull values from Key Vault into `mindlyst-native/web/.env.local` (see **MindLyst Environment Variables**). + +MindLyst secrets (canonical): + +| Key Vault secret name | Env var | Notes | +|---|---|---| +| `mindlyst-cosmos-endpoint` | `COSMOS_ENDPOINT` | Config (ok to store) | +| `mindlyst-cosmos-key` | `COSMOS_KEY` | Secret | +| `mindlyst-cosmos-database` | `COSMOS_DATABASE` | Config (`mindlyst`) | +| `mindlyst-openai-endpoint` | `AZURE_OPENAI_ENDPOINT` | Config | +| `mindlyst-openai-key` | `AZURE_OPENAI_KEY` | Secret | +| `mindlyst-openai-deployment` | `AZURE_OPENAI_DEPLOYMENT` | Config | +| `mindlyst-openai-api-version` | `AZURE_OPENAI_API_VERSION` | Config | +| `mindlyst-speech-key` | `AZURE_SPEECH_KEY` | Secret | +| `mindlyst-speech-region` | `AZURE_SPEECH_REGION` | Config | +| `mindlyst-blob-connection-string` | `AZURE_BLOB_CONNECTION_STRING` | Secret (prefer SAS later) | +| `mindlyst-notification-hub-connection-string` | `ANH_CONNECTION_STRING` | Secret | +| `mindlyst-appinsights-connection-string` | `APPLICATIONINSIGHTS_CONNECTION_STRING` | Treat as sensitive | + +List what’s currently in the vault: + +```bash +az keyvault secret list --vault-name kv-mywisprai --query "[].name" -o tsv +``` + +### Existing LysnrAI secrets (for reference) + +| Secret Name | Notes | +|-------------|-------| +| `wispr-azure-openai-deployment` | Existing OpenAI deployment name | +| `wispr-azure-openai-endpoint` | Existing OpenAI endpoint | +| `wispr-azure-openai-key` | Existing OpenAI key | +| `wispr-azure-speech-key` | Existing Speech key | +| `wispr-azure-speech-region` | Existing Speech region | + +--- + +## Step 7: Notification Hub — Add MindLyst Hub + +> MindLyst needs its own hub (different APNs cert + FCM key) but can share the namespace. + +### 7a. Check existing namespace + +1. Go to **Notification Hub Namespaces** → check if `lysnnai` exists +2. If it exists, open it → skip to 7c +3. If not, continue to 7b + +### 7b. Create namespace (if needed) + +1. Search **"Notification Hubs"** → click **Create Notification Hub Namespace** +2. Fill in: + - **Resource group:** `rg-mywisprai` + - **Namespace name:** `lysnnai` + - **Location:** `East US` + - **Pricing tier:** **Free** (up to 1M pushes/month) +3. Click **Create** + +### 7c. Create MindLyst hub + +1. Inside the namespace → click **+ Notification Hub** +2. **Hub name:** `mindlyst-hub` +3. Click **Create** + +### 7d. Configure APNs and FCM (later — when mobile app is ready) + +1. Go to `mindlyst-hub` → **Settings** → **Apple (APNS)** + - Upload your MindLyst APNs certificate or token key +2. Go to **Settings** → **Google (FCM v1)** + - Enter MindLyst Firebase project credentials + +### Connection details + +1. Go to `mindlyst-hub` → **Settings** → **Access Policies** +2. Copy the connection string for `DefaultFullSharedAccessSignature` (Send + Manage) + +Store it in Key Vault: + +- `mindlyst-notification-hub-connection-string` → `ANH_CONNECTION_STRING` + +Non-secret config: + +- `ANH_HUB_NAME=mindlyst-hub` + +--- + +## Step 8: Application Insights — Reuse or Create + +> Optional for MVP. If you want telemetry for both apps, create one shared resource. +> +> **Staging status:** Created `bytelyst-appinsights` in `rg-mywisprai` (East US). + +### Create (if not exists) + +Option A: Workspace-based (recommended) +1. Search **"Application Insights"** → click **Create** +2. Fill in: + - **Resource group:** `rg-mywisprai` + - **Name:** `bytelyst-appinsights` + - **Region:** `East US` + - **Resource Mode:** **Workspace-based** + - **Log Analytics Workspace:** create new → `bytelyst-logs` +3. Click **Review + Create** → **Create** + +Option B: Classic (matches current staging) +Use the Az CLI snippet in the **Optional: Application Insights** section below. + +### Get connection details + +1. Go to the resource → **Overview** +2. Copy **Connection String** +3. Store it in Key Vault: + - `mindlyst-appinsights-connection-string` → `APPLICATIONINSIGHTS_CONNECTION_STRING` + +### Separating telemetry per app + +In your application code, add a custom property to every trace: + +```typescript +// MindLyst +telemetryClient.context.tags["ai.cloud.role"] = "mindlyst-web"; + +// LysnrAI +telemetryClient.context.tags["ai.cloud.role"] = "mywisprai-admin"; +``` + +Then filter in the Azure Portal: **Application Insights → Logs → `where cloud_RoleName == "mindlyst-web"`** + +--- + +## MindLyst Environment Variables + +After completing all steps, create `mindlyst-native/web/.env.local`: + +> **Note:** MindLyst web API routes use `x-user-id` (or `MINDLYST_USER_ID`) as the Cosmos DB partition key for containers with `/userId`. +> +> **Hosted envs:** Prefer Key Vault references / Managed Identity (see `docs/WINDSURF/AZURE_KEY_VAULT_AND_SECRETS_ROTATION.md`). For local dev, generate `.env.local` from Key Vault. + +```bash +KV_NAME="kv-mywisprai" + +COSMOS_ENDPOINT="$(az keyvault secret show --vault-name "$KV_NAME" --name mindlyst-cosmos-endpoint --query value -o tsv)" +COSMOS_KEY="$(az keyvault secret show --vault-name "$KV_NAME" --name mindlyst-cosmos-key --query value -o tsv)" +COSMOS_DATABASE="$(az keyvault secret show --vault-name "$KV_NAME" --name mindlyst-cosmos-database --query value -o tsv)" + +AZURE_OPENAI_ENDPOINT="$(az keyvault secret show --vault-name "$KV_NAME" --name mindlyst-openai-endpoint --query value -o tsv)" +AZURE_OPENAI_KEY="$(az keyvault secret show --vault-name "$KV_NAME" --name mindlyst-openai-key --query value -o tsv)" +AZURE_OPENAI_DEPLOYMENT="$(az keyvault secret show --vault-name "$KV_NAME" --name mindlyst-openai-deployment --query value -o tsv)" +AZURE_OPENAI_API_VERSION="$(az keyvault secret show --vault-name "$KV_NAME" --name mindlyst-openai-api-version --query value -o tsv)" + +AZURE_SPEECH_KEY="$(az keyvault secret show --vault-name "$KV_NAME" --name mindlyst-speech-key --query value -o tsv)" +AZURE_SPEECH_REGION="$(az keyvault secret show --vault-name "$KV_NAME" --name mindlyst-speech-region --query value -o tsv)" + +AZURE_BLOB_CONNECTION_STRING="$(az keyvault secret show --vault-name "$KV_NAME" --name mindlyst-blob-connection-string --query value -o tsv)" + +ANH_CONNECTION_STRING="$(az keyvault secret show --vault-name "$KV_NAME" --name mindlyst-notification-hub-connection-string --query value -o tsv)" +APPLICATIONINSIGHTS_CONNECTION_STRING="$(az keyvault secret show --vault-name "$KV_NAME" --name mindlyst-appinsights-connection-string --query value -o tsv)" + +cat > mindlyst-native/web/.env.local < **Azure resources cannot be renamed after creation.** The old names (`cosmos-mywisprai`, `mywisprai-*`, etc.) still work perfectly — they're just cosmetic. Only the RG can be "renamed" by moving resources to a new one. + +--- + +## Az CLI Script (Copy-Paste-Ready) + +> Run this from a machine with `az` CLI installed and logged in (`az login`). +> This script is **idempotent** — safe to re-run if interrupted. + +```bash +#!/usr/bin/env bash +set -euo pipefail + +# ───────────────────────────────────────────────────────────── +# BytelystAI — Azure Resource Setup Script +# Creates MindLyst deltas inside an existing shared resource group +# ───────────────────────────────────────────────────────────── + +RG="rg-mywisprai" +COSMOS_ACCOUNT="cosmos-mywisprai" +COSMOS_DB="mindlyst" +STORAGE_ACCOUNT="bytelystblobs" + +echo "=== 1/3 Cosmos DB: Ensure '$COSMOS_DB' database ===" +if az cosmosdb sql database show --account-name "$COSMOS_ACCOUNT" --resource-group "$RG" --name "$COSMOS_DB" >/dev/null 2>&1; then + echo " [exists] $COSMOS_DB" +else + az cosmosdb sql database create --account-name "$COSMOS_ACCOUNT" --resource-group "$RG" --name "$COSMOS_DB" --query name -o tsv >/dev/null + echo " [created] $COSMOS_DB" +fi + +echo "" +echo "=== 2/3 Cosmos DB: Ensure 12 MindLyst containers ===" +MINDLYST_CONTAINERS=( + "users:/id" + "brains:/userId" + "brain_templates:/id" + "memory_items:/userId" + "actions:/memoryItemId" + "entities:/memoryItemId" + "reflections:/userId" + "share_cards:/userId" + "daily_briefs:/userId" + "streaks:/userId" + "notification_log:/userId" + "brain_insights:/userId" +) + +for spec in "${MINDLYST_CONTAINERS[@]}"; do + name="${spec%%:*}" + pk="${spec#*:}" + if az cosmosdb sql container show --account-name "$COSMOS_ACCOUNT" --resource-group "$RG" --database-name "$COSMOS_DB" --name "$name" >/dev/null 2>&1; then + echo " [exists] $COSMOS_DB/$name" + else + az cosmosdb sql container create --account-name "$COSMOS_ACCOUNT" --resource-group "$RG" --database-name "$COSMOS_DB" --name "$name" --partition-key-path "$pk" --query name -o tsv >/dev/null + echo " [created] $COSMOS_DB/$name ($pk)" + fi +done + +echo "" +echo "=== 3/3 Blob Storage: Ensure MindLyst containers ===" +SA_KEY=$(az storage account keys list --resource-group "$RG" --account-name "$STORAGE_ACCOUNT" --query '[0].value' -o tsv) +for c in mindlyst-voice mindlyst-images mindlyst-exports; do + created=$(az storage container create --name "$c" --account-name "$STORAGE_ACCOUNT" --account-key "$SA_KEY" --public-access off --query created -o tsv) + if [ "$created" = "true" ]; then + echo " [created] blob/$c" + else + echo " [exists] blob/$c" + fi +done + +echo "" +echo "=== Done ===" +``` + +### Optional: Key Vault secrets (run separately if needed) + +```bash +set -euo pipefail + +# Populate/refresh MindLyst secrets in Key Vault by reading current values from Azure resources. +# No rotation: this copies current keys/config into Key Vault. + +KV_NAME="kv-mywisprai" +RG="rg-mywisprai" + +COSMOS_ENDPOINT="$(az cosmosdb show -g "$RG" -n cosmos-mywisprai --query documentEndpoint -o tsv)" +COSMOS_KEY="$(az cosmosdb keys list -g "$RG" -n cosmos-mywisprai --query primaryMasterKey -o tsv)" +STORAGE_CONNECTION_STRING="$(az storage account show-connection-string -g "$RG" -n bytelystblobs --query connectionString -o tsv)" + +OPENAI_ENDPOINT="$(az cognitiveservices account show -g "$RG" -n mywisprai-openai-sweden --query '{name:name,kind:kind,location:location,endpoint:properties.endpoint}' -o json | python3 -c 'import json,sys; print(json.load(sys.stdin)["endpoint"])')" +OPENAI_KEY="$(az cognitiveservices account keys list -g "$RG" -n mywisprai-openai-sweden --query key1 -o tsv)" + +SPEECH_REGION="$(az cognitiveservices account show -g "$RG" -n mywisprai-speech --query '{name:name,location:location,kind:kind,sku:sku.name}' -o json | python3 -c 'import json,sys; print(json.load(sys.stdin)["location"])')" +SPEECH_KEY="$(az cognitiveservices account keys list -g "$RG" -n mywisprai-speech --query key1 -o tsv)" + +NOTIF_PRIMARY_CONNECTION_STRING="$(az notification-hub authorization-rule list-keys -g "$RG" --namespace-name lysnnai --notification-hub-name mindlyst-hub --name DefaultFullSharedAccessSignature -o json | python3 -c 'import json,sys; print(json.load(sys.stdin)["primaryConnectionString"])')" +APPINSIGHTS_CONNECTION_STRING="$(az monitor app-insights component show --app bytelyst-appinsights -g "$RG" -o json | python3 -c 'import json,sys; print(json.load(sys.stdin)["connectionString"])')" + +# Write secrets (suppress output to avoid printing values) +az keyvault secret set --vault-name "$KV_NAME" --name mindlyst-cosmos-endpoint --value "$COSMOS_ENDPOINT" -o none +az keyvault secret set --vault-name "$KV_NAME" --name mindlyst-cosmos-key --value "$COSMOS_KEY" -o none +az keyvault secret set --vault-name "$KV_NAME" --name mindlyst-cosmos-database --value mindlyst -o none + +az keyvault secret set --vault-name "$KV_NAME" --name mindlyst-blob-connection-string --value "$STORAGE_CONNECTION_STRING" -o none + +az keyvault secret set --vault-name "$KV_NAME" --name mindlyst-openai-endpoint --value "$OPENAI_ENDPOINT" -o none +az keyvault secret set --vault-name "$KV_NAME" --name mindlyst-openai-key --value "$OPENAI_KEY" -o none +az keyvault secret set --vault-name "$KV_NAME" --name mindlyst-openai-deployment --value gpt-4o-mini -o none +az keyvault secret set --vault-name "$KV_NAME" --name mindlyst-openai-api-version --value 2024-06-01 -o none + +az keyvault secret set --vault-name "$KV_NAME" --name mindlyst-speech-key --value "$SPEECH_KEY" -o none +az keyvault secret set --vault-name "$KV_NAME" --name mindlyst-speech-region --value "$SPEECH_REGION" -o none + +az keyvault secret set --vault-name "$KV_NAME" --name mindlyst-notification-hub-connection-string --value "$NOTIF_PRIMARY_CONNECTION_STRING" -o none +az keyvault secret set --vault-name "$KV_NAME" --name mindlyst-appinsights-connection-string --value "$APPINSIGHTS_CONNECTION_STRING" -o none + +echo "✓ MindLyst secrets populated in $KV_NAME" +``` + +### Optional: Notification Hub (run when mobile app is ready) + +```bash +# Create namespace (if needed) + MindLyst hub +NS="lysnnai" +RG="rg-mywisprai" + +az notification-hub namespace create \ + --resource-group "$RG" \ + --name "$NS" \ + --location eastus \ + --sku Free \ + 2>/dev/null && echo "✓ Namespace $NS created" \ + || echo "→ Namespace $NS already exists" + +az notification-hub create \ + --resource-group "$RG" \ + --namespace-name "$NS" \ + --name mindlyst-hub \ + 2>/dev/null && echo "✓ Hub mindlyst-hub created" \ + || echo "→ Hub mindlyst-hub already exists" +``` + +### Optional: Application Insights (shared telemetry) + +```bash +RG="rg-mywisprai" +APP="bytelyst-appinsights" +LOC="eastus" + +# Required once per subscription (App Insights may try to auto-register this provider) +az provider register --namespace Microsoft.OperationalInsights --wait + +# Create (classic) App Insights component via ARM +az resource create \ + --resource-group "$RG" \ + --name "$APP" \ + --resource-type Microsoft.Insights/components \ + --is-full-object \ + --properties "{\"location\":\"$LOC\",\"kind\":\"web\",\"properties\":{\"Application_Type\":\"web\"}}" + +# Get the connection string +APPINSIGHTS_CONNECTION_STRING="$(az resource show \ + --resource-group "$RG" \ + --name "$APP" \ + --resource-type Microsoft.Insights/components \ + --query properties.ConnectionString -o tsv)" + +# Store it in Key Vault (treat as sensitive) +az keyvault secret set --vault-name kv-mywisprai --name mindlyst-appinsights-connection-string --value "$APPINSIGHTS_CONNECTION_STRING" -o none +echo "✓ Stored App Insights connection string in Key Vault: mindlyst-appinsights-connection-string" +``` + +--- + +## Bicep IaC (Replicate To Another Account) + +If you want to replicate this shared infrastructure into a **different Azure subscription/account**, use the Bicep templates in: + +- `infra/azure/bytelyst-shared/` + +This folder also includes `infra/azure/bytelyst-shared/main.json` (compiled ARM template) which is tracked for easy diffing between runs: + +```bash +az bicep build --file infra/azure/bytelyst-shared/main.bicep +``` + +### What the Bicep creates + +- 1 Resource group (name is a parameter) +- Cosmos DB (serverless) + MindLyst `mindlyst` database + 12 containers +- (Optional) Cosmos DB empty database `mywisprai` (containers are app-specific; create separately) +- Storage account + `mindlyst-voice`, `mindlyst-images`, `mindlyst-exports` containers (private) +- Azure OpenAI (AI Foundry) account (optional: model deployment) +- Speech account +- Notification Hubs namespace + `mindlyst-hub` + auth rule +- Application Insights +- Key Vault (optional: auto-creates `mindlyst-*` secrets if `deployerObjectId` is provided) + +If Key Vault secret creation is enabled, the template populates: + +- `mindlyst-cosmos-endpoint` +- `mindlyst-cosmos-key` +- `mindlyst-cosmos-database` +- `mindlyst-openai-endpoint` +- `mindlyst-openai-key` +- `mindlyst-openai-deployment` +- `mindlyst-speech-key` +- `mindlyst-speech-region` +- `mindlyst-blob-connection-string` +- `mindlyst-notification-hub-connection-string` +- `mindlyst-appinsights-connection-string` + +### Deploy (subscription scope) + +1. `az login` and select the right subscription: + +```bash +az login +az account set --subscription "" +``` + +2. Register providers (first-time subscriptions): + +```bash +az provider register --namespace Microsoft.DocumentDB --wait +az provider register --namespace Microsoft.Storage --wait +az provider register --namespace Microsoft.CognitiveServices --wait +az provider register --namespace Microsoft.KeyVault --wait +az provider register --namespace Microsoft.NotificationHubs --wait +az provider register --namespace Microsoft.Insights --wait +``` + +3. Get your Azure AD user objectId (used to grant Key Vault secret permissions): + +```bash +az ad signed-in-user show --query id -o tsv +``` + +4. Edit the param file and pick **globally unique names**: + +- `infra/azure/bytelyst-shared/params/staging.example.json` + +5. Deploy: + +```bash +az deployment sub create \ + --name bytelyst-shared-staging \ + --location eastus \ + --template-file infra/azure/bytelyst-shared/main.bicep \ + --parameters @infra/azure/bytelyst-shared/params/staging.example.json \ + --parameters deployerObjectId="" +``` + +> **Note:** `openAiModelVersion` is region-dependent. If it’s empty, the template will create the OpenAI account but **skip** creating the `gpt-4o-mini` deployment (create it manually in Azure AI Foundry). + +### Parameter mapping (current staging) + +Use this mapping to understand how the current staging resource names relate to Bicep params. In a **different account**, you must choose new globally-unique names for most of these. + +| Bicep parameter | Current staging value | Notes | +|---|---|---| +| `resourceGroupName` | `rg-mywisprai` | In a new subscription, choose a new RG name (example: `rg-bytelyst-staging`) | +| `cosmosAccountName` | `cosmos-mywisprai` | Globally unique; must change in another subscription | +| `storageAccountName` | `bytelystblobs` | Globally unique; must change in another subscription | +| `openAiAccountName` | `mywisprai-openai-sweden` | Globally unique; must change in another subscription | +| `speechAccountName` | `mywisprai-speech` | Globally unique; must change in another subscription | +| `keyVaultName` | `kv-mywisprai` | Globally unique; must change in another subscription | +| `notificationHubNamespaceName` | `lysnnai` | Globally unique; must change in another subscription | +| `mindlystHubName` | `mindlyst-hub` | Not globally unique (scoped to the namespace) | +| `appInsightsName` | `bytelyst-appinsights` | Must be unique within the RG | +| `createMywispraiDatabase` | N/A | Creates an empty `mywisprai` database placeholder (no containers) | +| `deployerObjectId` | N/A | If set, Bicep also creates Key Vault secrets (`mindlyst-*`) | + +--- + +## What This Unlocks for MindLyst + +Once all steps are complete, these implementation plan tasks are done: + +``` +Phase 0.3 (Azure Infrastructure): +[x] Reuse existing Cosmos DB account — add mindlyst database with 12 containers +[x] Reuse existing Blob Storage — add 3 mindlyst-prefixed containers +[x] Reuse existing Azure OpenAI (GPT-4o-mini) for triage + brain invocation +[x] Reuse existing Azure Speech Service for STT/TTS +[x] Add MindLyst secrets to existing Key Vault +[x] Create MindLyst notification hub +[x] Set up Application Insights for telemetry +``` + +**Next steps after provisioning:** +1. (Done) MindLyst web: Enable Azure OpenAI triage + brain-chat (`OPENAI_PROVIDER=azure` + `AZURE_OPENAI_*`) +2. (Done) MindLyst web: Cosmos persistence for `/api/memory` and `/api/brains` (`COSMOS_*`) +3. Wire additional MindLyst API routes to Cosmos as needed (reflections, briefs, notifications, etc.) +4. Implement voice capture STT pipeline with Azure Speech SDK +5. Implement blob upload for voice/image captures to `mindlyst-voice` / `mindlyst-images` +6. Implement push notifications via `mindlyst-hub` (once mobile app is ready) + +--- + +## Quick Reference: Portal URLs + +| Resource | Direct Link | +|----------|-------------| +| Resource Group | `portal.azure.com → Resource groups → rg-mywisprai` | +| Cosmos DB Data Explorer | `portal.azure.com → cosmos-mywisprai → Data Explorer` | +| Blob Containers | `portal.azure.com → bytelystblobs → Containers` | +| OpenAI Studio | `oai.azure.com` → select `mywisprai-openai-sweden` | +| Speech Keys | `portal.azure.com → mywisprai-speech → Keys and Endpoint` | +| Key Vault Secrets | `portal.azure.com → kv-mywisprai → Secrets` | + +--- + +> **This document is shared between both repos.** When updating Azure infrastructure, update this doc in `learning_multimodal_memory_agents` and reference it from `learning_voice_ai_agent`.