# ByteLyst — 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-03-21 > > **Security note (staging):** This document does **not** include secret values. Store secrets in Azure Key Vault (`kv-mywisprai` — legacy resource name) and reference them by name (see **MindLyst Environment Variables** and **Key Vault** sections). Rotation is deferred; see `docs/devops/AZURE_KEY_VAULT_AND_SECRETS_ROTATION.md`. > > **Note on Azure resource names:** Many resources below still carry `mywisprai` in their names (e.g., `cosmos-mywisprai`, `kv-mywisprai`). Azure resources **cannot be renamed** after creation. The old names are purely cosmetic and work correctly. --- ## 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: LysnrAI — legacy name, 10 containers) │ └── mindlyst (database: MindLyst, 12 containers) │ ├── 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) │ ├── lysnr-* (LysnrAI secrets) │ ├── wispr-* (LysnrAI legacy secrets, pre-rebrand) │ └── mindlyst-* (MindLyst secrets) │ ├── 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` (LysnrAI, 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` (legacy name — this is the LysnrAI database) - `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 (legacy `wispr-*` prefix, pre-rebrand — superseded by `lysnr-*`): - `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` | Legacy (pre-rebrand) — superseded by `lysnr-*` | | `wispr-azure-openai-endpoint` | Legacy (pre-rebrand) | | `wispr-azure-openai-key` | Legacy (pre-rebrand) | | `wispr-azure-speech-key` | Legacy (pre-rebrand) | | `wispr-azure-speech-region` | Legacy (pre-rebrand) | --- ## 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'] = 'lysnrai-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/devops/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 # ───────────────────────────────────────────────────────────── # ByteLyst — 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 (LysnrAI, 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 lives in `learning_ai_common_plat/docs/devops/`.** It is the single source of truth for Azure infrastructure setup shared by all products.