# 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/devops/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/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 <<EOF
# ── Azure Cosmos DB ─────────────────────────────────────────
COSMOS_ENDPOINT=${COSMOS_ENDPOINT}
COSMOS_KEY=${COSMOS_KEY}
COSMOS_DATABASE=${COSMOS_DATABASE}

# ── Azure OpenAI (AI Foundry) ───────────────────────────────
OPENAI_PROVIDER=azure
AZURE_OPENAI_ENDPOINT=${AZURE_OPENAI_ENDPOINT}
AZURE_OPENAI_KEY=${AZURE_OPENAI_KEY}
AZURE_OPENAI_DEPLOYMENT=${AZURE_OPENAI_DEPLOYMENT}
AZURE_OPENAI_API_VERSION=${AZURE_OPENAI_API_VERSION}

# ── Azure Speech ────────────────────────────────────────────
AZURE_SPEECH_KEY=${AZURE_SPEECH_KEY}
AZURE_SPEECH_REGION=${AZURE_SPEECH_REGION}

# ── Azure Blob Storage ──────────────────────────────────────
AZURE_BLOB_CONNECTION_STRING=${AZURE_BLOB_CONNECTION_STRING}

# ── Push Notifications ──────────────────────────────────────
ANH_CONNECTION_STRING=${ANH_CONNECTION_STRING}
ANH_HUB_NAME=mindlyst-hub

# ── App Insights ────────────────────────────────────────────
APPLICATIONINSIGHTS_CONNECTION_STRING=${APPLICATIONINSIGHTS_CONNECTION_STRING}

# ── App Config ──────────────────────────────────────────────
NEXT_PUBLIC_APP_NAME=MindLyst
NEXT_PUBLIC_APP_VERSION=0.2.0
MINDLYST_USER_ID=staging_user

# ── Abuse Controls (Web API) ────────────────────────────────
# Basic, per-instance rate limiting + payload guards for Azure OpenAI routes.
RATE_LIMIT_ENABLED=true
LLM_RATE_LIMIT_WINDOW_MS=60000
TRIAGE_RATE_LIMIT_LIMIT=30
BRAIN_CHAT_RATE_LIMIT_LIMIT=20
TRIAGE_MAX_CONTENT_CHARS=8000
BRAIN_CHAT_MAX_MESSAGE_CHARS=2000
BRAIN_CHAT_MAX_HISTORY_MESSAGES=20
BRAIN_CHAT_MAX_HISTORY_TOTAL_CHARS=10000
EOF
```

---

## Verification Checklist

After completing all steps, verify:

| # | Check | How |
|---|-------|-----|
| 1 | **Cosmos DB: `mindlyst` database** | Data Explorer → expand `cosmos-mywisprai` → `mindlyst` → 12 containers visible |
| 2 | **Cosmos DB: `mywisprai` untouched** | Data Explorer → `mywisprai` database still has its existing containers |
| 3 | **Blob: 3 new containers** | Storage → `bytelystblobs` → Containers → `mindlyst-voice`, `mindlyst-images`, `mindlyst-exports` |
| 4 | **Blob: other containers intact (if any)** | Same page → any pre-existing containers should still be present |
| 5 | **OpenAI deployment** | Azure OpenAI Studio → Deployments → `gpt-4o-mini` listed |
| 6 | **Speech Service keys** | Speech resource → Keys and Endpoint → keys visible |
| 7 | **Key Vault: MindLyst secrets** | Key Vault → Secrets → `mindlyst-*` entries visible |
| 8 | **MindLyst web connects** | Run `cd mindlyst-native/web && npm run dev` → triage/brain-chat should use Azure OpenAI (not mock) |

---

## Cost Estimates

### Combined Cost (LysnrAI + MindLyst, MVP / Low Usage)

| Service | Pricing | LysnrAI | MindLyst | Combined |
|---------|---------|---------|----------|----------|
| Cosmos DB (Serverless) | $0.25/M RU + $0.25/GB | ~$2 | ~$2 | ~$4 |
| Blob Storage (LRS) | $0.018/GB | ~$0.10 | ~$0.10 | ~$0.20 |
| Azure OpenAI (GPT-4o-mini) | $0.15/M input + $0.60/M output | ~$5 | ~$5 | ~$10 |
| Speech (F0 free) | 5h STT free | $0 | $0 | $0 |
| Key Vault | $0.03/10K ops | ~$0.03 | ~$0.03 | ~$0.06 |
| App Insights | 5GB/month free | $0 | $0 | $0 |
| Notification Hub (Free) | 1M pushes/month free | $0 | $0 | $0 |
| **Total** | | | | **~$5–15/month** |

### Cost Savings from Sharing

- **Cosmos DB:** 1 account instead of 2 = no extra overhead (Serverless has no idle cost)
- **OpenAI:** 1 resource instead of 2 = no extra base cost (pay per token only)
- **Speech:** 1 resource instead of 2 = shared 5h free tier (may need S0 upgrade sooner)
- **Blob Storage:** 1 account instead of 2 = negligible savings but simpler key management
- **Estimated savings:** ~$3–8/month at MVP scale

---

## Resource Naming Convention

| Azure Resource | Current Name | Ideal Name | Notes |
|---------------|-------------|------------|-------|
| Resource Group | `rg-mywisprai` | `rg-bytelyst` | Move resources to new RG (Step 1) |
| Cosmos DB Account | `cosmos-mywisprai` | `cosmos-bytelyst` | Can't rename — keep as-is |
| Blob Storage | `bytelystblobs` | `bytelystblobs` | Already brand-neutral |
| Azure OpenAI | `mywisprai-openai-sweden` | — | Can't rename — keep as-is |
| Speech Service | `mywisprai-speech` | — | Can't rename — keep as-is |
| Key Vault | `kv-mywisprai` | `kv-bytelyst` | Can't rename — keep as-is |
| Notification Hub NS | `lysnnai` | `bytelyst-hub-ns` | Consider creating new NS later if desired |
| App Insights | `bytelyst-appinsights` | — | Can recreate if you want workspace-based mode |

> **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 "<SUBSCRIPTION_ID>"
```

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="<YOUR_OBJECT_ID>"
```

> **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 lives in `learning_ai_common_plat/docs/devops/`.** It is the single source of truth for Azure infrastructure setup shared by all products.
