> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://cla-demo.postmansolutions.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://cla-demo.postmansolutions.com/_mcp/server.

# Migrate DocumentAI Service from Environment Variables to Azure App Configuration

The DocumentAI service currently uses the following `.env` configuration:

```env  

APPLICATION_PORT=4200  

DB_SQL_MANAGED_IDENTITY=true  

DB_SQL_HOST=dev-eus2-adc-sql-002.database.windows.net  

DB_SQL_DATABASE=DocumentAI  

AZURE_AD_TENANT=4aaa468e-93ba-4ee3-ab9f-6a247aa3ade0  

AZURE_AD_CLIENT_ID=f338c4a6-dbb1-4e22-9919-1f4071928cce  

AZURE_OPENAI_API_VERSION=2024-08-01-preview  

AZURE_OPENAI_DEPLOYMENT_ID=gpt-4o-ncus  

AZURE_OPENAI_ENDPOINT=https://da-dev-dc01-openai-oai1.openai.azure.com/  

AZURE_OPENAI_API_KEY=3497ead01da84aad92e8e27ebbbdc9e9  

AZURE_FR_ENDPOINT=https://ap-ai-di01-w2szuopsr4mgc.cognitiveservices.azure.com/  

AZURE_FR_KEY=EoeEdhCJvmP82OMmaNZPJUQA7TBEK9mB0yy5XEkCvVvmoxvBqJVBJQQJ99BJACHYHv6XJ3w3AAALACOG5LST  

AZURE_STORAGE_CONNECTION_STRING=https://deveus2adcinst001.blob.core.windows.net/  

AZURE_STORAGE_CONNECTION_STRING_DOCAI=https://deveus2adcdocst001.blob.core.windows.net/  

FLUXX_UPLOAD_ENDPOINT_URL=http://fluxx-file-intake.fluxx.svc.cluster.local/v1/upload/33333333-3333-4333-8333-333333333333  

```

### Issues with Current Approach

1. Configuration values hardcoded in deployment files or environment-specific `.env` files

2. Secrets exposed in plain text in environment variables

3. No audit trail for configuration changes

4. Configuration changes require redeployment

5. Potential for configuration drift across environments

### Azure App Configuration Keys

All configuration will be stored in Azure App Configuration with the `DOCAI_` prefix to prevent collisions:

```

DOCAI_APPLICATION_PORT  

DOCAI_DB_SQL_MANAGED_IDENTITY  

DOCAI_DB_SQL_HOST  

DOCAI_DB_SQL_DATABASE  

DOCAI_AZURE_AD_TENANT  

DOCAI_AZURE_AD_CLIENT_ID  

DOCAI_AZURE_OPENAI_API_VERSION  

DOCAI_AZURE_OPENAI_DEPLOYMENT_ID  

DOCAI_AZURE_OPENAI_ENDPOINT  

DOCAI_AZURE_OPENAI_API_KEY (Key Vault Reference)  

DOCAI_AZURE_FR_ENDPOINT  

DOCAI_AZURE_FR_KEY (Key Vault Reference)  

DOCAI_AZURE_STORAGE_CONNECTION_STRING  

DOCAI_AZURE_STORAGE_CONNECTION_STRING_DOCAI  

DOCAI_FLUXX_UPLOAD_ENDPOINT_URL  

```

---

## Design

### Architecture Pattern

Follow the pattern established in the `fluxx-file-intake` repository:

#### 1. **App Configuration Initialization**

Use the `framework-nodejs-appconfig` and `framework-nodejs-core` packages to initialize App Config with environment variable fallback support.

#### 2. **Configuration Constants**

Define all configuration keys in a central constants file (`constants/configuration.ts`).

#### 3. **Lookup Abstraction**

Use an `app.lookup()` abstraction layer that:

* Resolves values from Azure App Configuration

* Falls back to environment variables during local development

* Handles type conversions (string → boolean, string → number)

* Supports package.json property resolution

#### 4. **Dependency Injection**

Configuration values are looked up asynchronously and injected where needed.

### Component Architecture

```

┌─────────────────────────────────────────────────────────────┐  

│                    DocumentAI Service                        │  

├─────────────────────────────────────────────────────────────┤  

│                                                               │  

│  ┌──────────────────────────────────────────────────────┐   │  

│  │              app.lookup(ENV_KEYS.*)                  │   │  

│  │              Unified Configuration API                │   │  

│  └────────────────────┬─────────────────────────────────┘   │  

│                       │                                      │  

│       ┌───────────────┴────────────────┐                    │  

│       │                                │                    │  

│  ┌────▼─────────────┐    ┌────────────▼──────────────┐     │  

│  │  Azure App       │    │  Environment Variables    │     │  

│  │  Configuration   │    │  (Dev/Local Fallback)     │     │  

│  └────┬─────────────┘    └───────────────────────────┘     │  

│       │                                                      │  

│  ┌────▼─────────────┐                                       │  

│  │  Azure Key Vault │                                       │  

│  │  (Secrets Only)  │                                       │  

│  └──────────────────┘                                       │  

└─────────────────────────────────────────────────────────────┘  

```

---

## Implementation Steps

### Phase 1: Setup Dependencies

#### Task 1.1: Install Required NPM Packages

```bash

npm install framework-nodejs-core@2.0.1  

npm install framework-nodejs-appconfig@2.0.1  

npm install framework-nodejs-extensions@1.2.2  

npm install framework-nodejs-logging@2.0.1  

npm install @azure/identity@4.13.0  

npm install dotenv@16.6.1  

```

#### Task 1.2: Update package.json

Add the following to `package.json`:

```json

{  

  "type": "module",  

  "dependencies": {  

    "@azure/identity": "4.13.0",  

    "dotenv": "16.6.1",  

    "framework-nodejs-appconfig": "2.0.1",  

    "framework-nodejs-core": "2.0.1",  

    "framework-nodejs-extensions": "1.2.2",  

    "framework-nodejs-logging": "2.0.1"  

  }  

}  

```

---

### Phase 2: Create Core Infrastructure

#### Task 2.1: Create Logger Type Definition

**File**: `src/internal/Logger.d.ts`

```typescript

export interface Logger {  

    trace: (msg: string, ...args: unknown[]) => void;  

    debug: (msg: string, ...args: unknown[]) => void;  

    info: (msg: string, ...args: unknown[]) => void;  

    warn: (msg: string, ...args: unknown[]) => void;  

    error: (msg: string, ...args: unknown[]) => void;  

    fatal: (msg: string, ...args: unknown[]) => void;  

}  

```

#### Task 2.2: Create App Container

**File**: `src/internal/app.ts`

```typescript

import { Logger } from './Logger.d.js';  

import { missingLogger } from './frameworkAdapter/missingLogger.js';  

import { missingLookup } from './frameworkAdapter/missingLookup.js';  

import { prepareLookup } from './frameworkAdapter/prepareLookup.js';  

import { registerLogger } from './frameworkAdapter/registerLogger.js';  

  

// A holder for all the important parts of the app that are attached to the  

// frameworkApp in a DI container or as a property such as the config and logger  

export const app: App = {  

    log: missingLogger,  

    lookup: missingLookup,  

    prepareLookup,  

    registerLogger,  

};  

  

interface App {  

    log: Logger;  

    lookup: <T>(key: string) => Promise<T>  

    prepareLookup: typeof prepareLookup;  

    registerLogger: typeof registerLogger;  

}  

```

#### Task 2.3: Create Configuration Constants

**File**: `src/constants/configuration.ts`

```typescript

export const ENV_KEYS = {  

    // Application Configuration  

    APPLICATION_PORT: 'DOCAI_APPLICATION_PORT',  

     

    // Database Configuration  

    DB_SQL_MANAGED_IDENTITY: 'DOCAI_DB_SQL_MANAGED_IDENTITY',  

    DB_SQL_HOST: 'DOCAI_DB_SQL_HOST',  

    DB_SQL_DATABASE: 'DOCAI_DB_SQL_DATABASE',  

     

    // Azure AD Configuration  

    AZURE_AD_TENANT: 'DOCAI_AZURE_AD_TENANT',  

    AZURE_AD_CLIENT_ID: 'DOCAI_AZURE_AD_CLIENT_ID',  

     

    // Azure OpenAI Configuration  

    AZURE_OPENAI_API_VERSION: 'DOCAI_AZURE_OPENAI_API_VERSION',  

    AZURE_OPENAI_DEPLOYMENT_ID: 'DOCAI_AZURE_OPENAI_DEPLOYMENT_ID',  

    AZURE_OPENAI_ENDPOINT: 'DOCAI_AZURE_OPENAI_ENDPOINT',  

    AZURE_OPENAI_API_KEY: 'DOCAI_AZURE_OPENAI_API_KEY',  

     

    // Azure Form Recognizer Configuration  

    AZURE_FR_ENDPOINT: 'DOCAI_AZURE_FR_ENDPOINT',  

    AZURE_FR_KEY: 'DOCAI_AZURE_FR_KEY',  

     

    // Azure Storage Configuration  

    AZURE_STORAGE_CONNECTION_STRING: 'DOCAI_AZURE_STORAGE_CONNECTION_STRING',  

    AZURE_STORAGE_CONNECTION_STRING_DOCAI: 'DOCAI_AZURE_STORAGE_CONNECTION_STRING_DOCAI',  

     

    // External Service Configuration  

    FLUXX_UPLOAD_ENDPOINT_URL: 'DOCAI_FLUXX_UPLOAD_ENDPOINT_URL',  

} as const;  

```

---

### Phase 3: Create Framework Adapters

#### Task 3.1: Missing Logger Placeholder

**File**: `src/internal/frameworkAdapter/missingLogger.ts`

```typescript

import { Logger } from '../Logger.d.js';  

  

export const missingLogger: Logger = {  

    trace: createMissingLoggerMethod('trace'),  

    debug: createMissingLoggerMethod('debug'),  

    info: createMissingLoggerMethod('info'),  

    warn: createMissingLoggerMethod('warn'),  

    error: createMissingLoggerMethod('error'),  

    fatal: createMissingLoggerMethod('fatal'),  

};  

  

function createMissingLoggerMethod(level: string) {  

    return (msg: string, ...args: unknown[]) => {  

        throw new Error(`Logger not initialized. Attempted to log ${level}: ${msg}`);  

    };  

}  

```

#### Task 3.2: Missing Lookup Placeholder

**File**: `src/internal/frameworkAdapter/missingLookup.ts`

```typescript

export const missingLookup = async <T>(key: string): Promise<T> => {  

    throw new Error(`App config lookup not initialized. Attempted to lookup key: ${key}`);  

};  

```

#### Task 3.3: Register Logger Function

**File**: `src/internal/frameworkAdapter/registerLogger.ts`

```typescript

import { DevFramework } from 'framework-nodejs-core';  

import { app } from '../app.js';  

  

export const registerLogger = (frameworkApp: DevFramework): void => {  

    app.log = frameworkApp.server.log;  

};  

```

#### Task 3.4: Provide App Config

**File**: `src/internal/frameworkAdapter/provideAppConfig.ts`

```typescript

import { ConfigClient } from 'framework-nodejs-extensions';  

  

export const provideAppConfig = (frameworkApp: FrameworkAppWithAppConfig): ConfigClient => {  

    if (!frameworkApp.server) {  

        throw Error('Attempted to resolve the AppConfig in an app with no "server" assigned');  

    }  

    if (!frameworkApp.server.diContainer) {  

        throw Error('Attempted to resolve the AppConfig in an app & server with no "diContainer" assigned');  

    }  

    return frameworkApp.server.diContainer.resolve<ConfigClient>('appConfig');  

};  

  

interface FrameworkAppWithAppConfig {  

    server?: { diContainer?: { resolve: <T>(key: string) => T } }  

}  

```

#### Task 3.5: Create Lookup Function with Type Conversion

**File**: `src/internal/frameworkAdapter/createLookup.ts`

```typescript

import { ConfigClient } from 'framework-nodejs-extensions';  

  

const KEYS_FOR_BOOLEAN_VALUES = [  

    'DOCAI_DB_SQL_MANAGED_IDENTITY',  

];  

  

const KEYS_FOR_NUMBER_VALUES = [  

    'DOCAI_APPLICATION_PORT',  

];  

  

export const createLookup = (appConfig: ConfigClient) =>  

    async <T>(key: string): Promise<T> => {  

        const value = await appConfig.get<unknown>(key);  

  

        // Handle boolean conversion  

        if (KEYS_FOR_BOOLEAN_VALUES.includes(key)) {  

            if (typeof value === 'string' && value.toLowerCase() === 'false') {  

                return false as T;  

            }  

            return Boolean(value) as T;  

        }  

  

        // Handle number conversion  

        if (KEYS_FOR_NUMBER_VALUES.includes(key)) {  

            if (typeof value === 'string') {  

                return Number(value) as T;  

            }  

        }  

  

        return value as T;  

    };  

```

#### Task 3.6: Prepare Lookup Function

**File**: `src/internal/frameworkAdapter/prepareLookup.ts`

```typescript

import { provideAppConfig } from './provideAppConfig.js';  

import { app } from '../app.js';  

import { createLookup } from './createLookup.js';  

import { DevFramework } from 'framework-nodejs-core';  

  

export const prepareLookup = async (frameworkApp: DevFramework): Promise<void> => {  

    const appConfig = provideAppConfig(frameworkApp);  

    app.lookup = createLookup(appConfig);  

    return undefined;  

};  

```

---

### Phase 4: Update Application Entry Point

#### Task 4.1: Modify index.ts

**File**: `src/index.ts`

```typescript

import 'dotenv/config';  

import 'framework-nodejs-appconfig';  

import { DevFramework } from 'framework-nodejs-core';  

import { app } from './internal/app.js';  

import { ENV_KEYS } from './constants/configuration.js';  

  

// Initialize the framework  

const frameworkApp = await DevFramework.create();  

  

// Initialize the appConfig with environment variable fallback  

await frameworkApp.initAppConfig({  

    useEnvironmentVariables: true, // Enables fallback to .env during local dev  

});  

  

// Prepare the lookup function  

await app.prepareLookup(frameworkApp);  

  

// Register the logger  

app.registerLogger(frameworkApp);  

  

// Example: Get application port from App Config  

const applicationPort: number = await app.lookup(ENV_KEYS.APPLICATION_PORT);  

  

app.log.info(`Starting DocumentAI service on port ${applicationPort}`);  

  

// Initialize your application here  

// ...  

```

---

### Phase 5: Migrate Service Code

#### Task 5.1: Update Configuration Usage Pattern

**Before (using direct environment variables):**

```typescript

const openAiEndpoint = process.env.AZURE_OPENAI_ENDPOINT;  

const openAiKey = process.env.AZURE_OPENAI_API_KEY;  

const port = parseInt(process.env.APPLICATION_PORT || '4200');  

```

**After (using app.lookup):**

```typescript

import { app } from './internal/app.js';  

import { ENV_KEYS } from './constants/configuration.js';  

  

const openAiEndpoint: string = await app.lookup(ENV_KEYS.AZURE_OPENAI_ENDPOINT);  

const openAiKey: string = await app.lookup(ENV_KEYS.AZURE_OPENAI_API_KEY);  

const port: number = await app.lookup(ENV_KEYS.APPLICATION_PORT);  

```

#### Task 5.2: Update Database Configuration

```typescript

import { app } from './internal/app.js';  

import { ENV_KEYS } from './constants/configuration.js';  

  

export const getDatabaseConfig = async () => {  

    return {  

        host: await app.lookup(ENV_KEYS.DB_SQL_HOST),  

        database: await app.lookup(ENV_KEYS.DB_SQL_DATABASE),  

        useManagedIdentity: await app.lookup(ENV_KEYS.DB_SQL_MANAGED_IDENTITY),  

    };  

};  

```

#### Task 5.3: Update OpenAI Service

```typescript

import { app } from '../internal/app.js';  

import { ENV_KEYS } from '../constants/configuration.js';  

  

export class OpenAIService {  

    private endpoint: string;  

    private apiKey: string;  

    private apiVersion: string;  

    private deploymentId: string;  

  

    async initialize() {  

        this.endpoint = await app.lookup(ENV_KEYS.AZURE_OPENAI_ENDPOINT);  

        this.apiKey = await app.lookup(ENV_KEYS.AZURE_OPENAI_API_KEY);  

        this.apiVersion = await app.lookup(ENV_KEYS.AZURE_OPENAI_API_VERSION);  

        this.deploymentId = await app.lookup(ENV_KEYS.AZURE_OPENAI_DEPLOYMENT_ID);  

    }  

  

    // Service methods...  

}  

```

#### Task 5.4: Update Form Recognizer Service

```typescript

import { app } from '../internal/app.js';  

import { ENV_KEYS } from '../constants/configuration.js';  

  

export class FormRecognizerService {  

    private endpoint: string;  

    private apiKey: string;  

  

    async initialize() {  

        this.endpoint = await app.lookup(ENV_KEYS.AZURE_FR_ENDPOINT);  

        this.apiKey = await app.lookup(ENV_KEYS.AZURE_FR_KEY);  

    }  

  

    // Service methods...  

}  

```

---

### Phase 6: Azure Infrastructure Setup

#### Task 6.1: Create App Configuration Keys in Azure Portal

For each environment (dev, qa, uat, prd), create the following keys in Azure App Configuration:

```bash

# Development Environment  

DOCAI_APPLICATION_PORT=4200  

DOCAI_DB_SQL_MANAGED_IDENTITY=true  

DOCAI_DB_SQL_HOST=dev-eus2-adc-sql-002.database.windows.net  

DOCAI_DB_SQL_DATABASE=DocumentAI  

DOCAI_AZURE_AD_TENANT=4aaa468e-93ba-4ee3-ab9f-6a247aa3ade0  

DOCAI_AZURE_AD_CLIENT_ID=f338c4a6-dbb1-4e22-9919-1f4071928cce  

DOCAI_AZURE_OPENAI_API_VERSION=2024-08-01-preview  

DOCAI_AZURE_OPENAI_DEPLOYMENT_ID=gpt-4o-ncus  

DOCAI_AZURE_OPENAI_ENDPOINT=https://da-dev-dc01-openai-oai1.openai.azure.com/  

DOCAI_AZURE_FR_ENDPOINT=https://ap-ai-di01-w2szuopsr4mgc.cognitiveservices.azure.com/  

DOCAI_AZURE_STORAGE_CONNECTION_STRING=https://deveus2adcinst001.blob.core.windows.net/  

DOCAI_AZURE_STORAGE_CONNECTION_STRING_DOCAI=https://deveus2adcdocst001.blob.core.windows.net/  

DOCAI_FLUXX_UPLOAD_ENDPOINT_URL=http://fluxx-file-intake.fluxx.svc.cluster.local/v1/upload/33333333-3333-4333-8333-333333333333  

```

#### Task 6.2: Migrate Secrets to Key Vault References

Store sensitive values in Azure Key Vault and reference them in App Config:

**Create Key Vault secrets:**

```bash

az keyvault secret set --vault-name <vault-name> --name DOCAI-AZURE-OPENAI-API-KEY --value "3497ead01da84aad92e8e27ebbbdc9e9"  

az keyvault secret set --vault-name <vault-name> --name DOCAI-AZURE-FR-KEY --value "EoeEdhCJvmP82OMmaNZPJUQA7TBEK9mB0yy5XEkCvVvmoxvBqJVBJQQJ99BJACHYHv6XJ3w3AAALACOG5LST"  

```

**Update App Config with Key Vault references:**

```json

{  

  "key": "DOCAI_AZURE_OPENAI_API_KEY",  

  "value": "{\"uri\":\"https://<vault-name>.vault.azure.net/secrets/DOCAI-AZURE-OPENAI-API-KEY\"}"  

}  

{  

  "key": "DOCAI_AZURE_FR_KEY",  

  "value": "{\"uri\":\"https://<vault-name>.vault.azure.net/secrets/DOCAI-AZURE-FR-KEY\"}"  

}  

```

#### Task 6.3: Configure Managed Identity Access

Grant the DocumentAI service's Managed Identity access to:

1. **App Configuration**: `App Configuration Data Reader` role

2. **Key Vault**: `Key Vault Secrets User` role

```bash

# Get the service principal ID  

PRINCIPAL_ID=$(az webapp identity show --name <app-name> --resource-group <rg-name> --query principalId -o tsv)  

  

# Grant App Config access  

az role assignment create --assignee $PRINCIPAL_ID --role "App Configuration Data Reader" --scope /subscriptions/<subscription-id>/resourceGroups/<rg-name>/providers/Microsoft.AppConfiguration/configurationStores/<appconfig-name>  

  

# Grant Key Vault access  

az keyvault set-policy --name <vault-name> --object-id $PRINCIPAL_ID --secret-permissions get list  

```

---

### Phase 7: Update Deployment Configuration

#### Task 7.1: Update Kubernetes Deployment YAML

**File**: `k8s/deployment.yaml`

```yaml

apiVersion: apps/v1  

kind: Deployment  

metadata:  

  name: documentai-service  

spec:  

  template:  

    spec:  

      containers:  

      - name: documentai  

        image: <your-registry>/documentai:latest  

        env:  

        - name: AZURE_APP_CONFIGURATION_CONNECTION_STRING  

          valueFrom:  

            secretKeyRef:  

              name: appconfig-secret  

              key: connection-string  

        # Keep fallback environment variables for local development  

        - name: DOCAI_APPLICATION_PORT  

          value: "4200"  

```

#### Task 7.2: Create Local Development .env File

**File**: `.env.example`

```env

# Azure App Configuration (leave empty to use environment variables)  

AZURE_APP_CONFIGURATION_CONNECTION_STRING=  

  

# Local Development Fallback Values (prefixed with DOCAI_)  

DOCAI_APPLICATION_PORT=4200  

DOCAI_DB_SQL_MANAGED_IDENTITY=false  

DOCAI_DB_SQL_HOST=localhost  

DOCAI_DB_SQL_DATABASE=DocumentAI  

DOCAI_AZURE_AD_TENANT=4aaa468e-93ba-4ee3-ab9f-6a247aa3ade0  

DOCAI_AZURE_AD_CLIENT_ID=f338c4a6-dbb1-4e22-9919-1f4071928cce  

DOCAI_AZURE_OPENAI_API_VERSION=2024-08-01-preview  

DOCAI_AZURE_OPENAI_DEPLOYMENT_ID=gpt-4o-ncus  

DOCAI_AZURE_OPENAI_ENDPOINT=https://da-dev-dc01-openai-oai1.openai.azure.com/  

DOCAI_AZURE_OPENAI_API_KEY=local-dev-key  

DOCAI_AZURE_FR_ENDPOINT=https://ap-ai-di01-w2szuopsr4mgc.cognitiveservices.azure.com/  

DOCAI_AZURE_FR_KEY=local-dev-key  

DOCAI_AZURE_STORAGE_CONNECTION_STRING=https://deveus2adcinst001.blob.core.windows.net/  

DOCAI_AZURE_STORAGE_CONNECTION_STRING_DOCAI=https://deveus2adcdocst001.blob.core.windows.net/  

DOCAI_FLUXX_UPLOAD_ENDPOINT_URL=http://localhost:3000/v1/upload/33333333-3333-4333-8333-333333333333  

```

---

## Testing Strategy

### Unit Tests

#### Test 1: Configuration Lookup

**File**: `test/unit/internal/frameworkAdapter/createLookup.test.ts`

```typescript

import { describe, it, expect, vi } from 'vitest';  

import { createLookup } from '../../../../src/internal/frameworkAdapter/createLookup.js';  

import { ConfigClient } from 'framework-nodejs-extensions';  

  

describe('createLookup', () => {  

    it('should return string values as-is', async () => {  

        const mockAppConfig = {  

            get: vi.fn().mockResolvedValue('test-value'),  

        } as unknown as ConfigClient;  

  

        const lookup = createLookup(mockAppConfig);  

        const result = await lookup<string>('DOCAI_AZURE_OPENAI_ENDPOINT');  

  

        expect(result).toBe('test-value');  

        expect(mockAppConfig.get).toHaveBeenCalledWith('DOCAI_AZURE_OPENAI_ENDPOINT');  

    });  

  

    it('should convert string "true" to boolean true', async () => {  

        const mockAppConfig = {  

            get: vi.fn().mockResolvedValue('true'),  

        } as unknown as ConfigClient;  

  

        const lookup = createLookup(mockAppConfig);  

        const result = await lookup<boolean>('DOCAI_DB_SQL_MANAGED_IDENTITY');  

  

        expect(result).toBe(true);  

    });  

  

    it('should convert string "false" to boolean false', async () => {  

        const mockAppConfig = {  

            get: vi.fn().mockResolvedValue('false'),  

        } as unknown as ConfigClient;  

  

        const lookup = createLookup(mockAppConfig);  

        const result = await lookup<boolean>('DOCAI_DB_SQL_MANAGED_IDENTITY');  

  

        expect(result).toBe(false);  

    });  

  

    it('should convert string numbers to numbers', async () => {  

        const mockAppConfig = {  

            get: vi.fn().mockResolvedValue('4200'),  

        } as unknown as ConfigClient;  

  

        const lookup = createLookup(mockAppConfig);  

        const result = await lookup<number>('DOCAI_APPLICATION_PORT');  

  

        expect(result).toBe(4200);  

        expect(typeof result).toBe('number');  

    });  

});  

```

#### Test 2: Missing Configuration Handling

**File**: `test/unit/internal/frameworkAdapter/missingLookup.test.ts`

```typescript

import { describe, it, expect } from 'vitest';  

import { missingLookup } from '../../../../src/internal/frameworkAdapter/missingLookup.js';  

  

describe('missingLookup', () => {  

    it('should throw an error when lookup is not initialized', async () => {  

        await expect(missingLookup('TEST_KEY')).rejects.toThrow(  

            'App config lookup not initialized. Attempted to lookup key: TEST_KEY'  

        );  

    });  

});  

```

#### Test 3: Environment Variable Fallback

**File**: `test/unit/config-fallback.test.ts`

```typescript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';  

import { DevFramework } from 'framework-nodejs-core';  

import { app } from '../../src/internal/app.js';  

import { ENV_KEYS } from '../../src/constants/configuration.js';  

  

describe('Configuration Fallback', () => {  

    let frameworkApp: DevFramework;  

  

    beforeEach(async () => {  

        // Set environment variable  

        process.env.DOCAI_APPLICATION_PORT = '5000';  

         

        frameworkApp = await DevFramework.create();  

        await frameworkApp.initAppConfig({ useEnvironmentVariables: true });  

        await app.prepareLookup(frameworkApp);  

    });  

  

    afterEach(() => {  

        delete process.env.DOCAI_APPLICATION_PORT;  

    });  

  

    it('should fall back to environment variable when App Config is not available', async () => {  

        const port = await app.lookup<number>(ENV_KEYS.APPLICATION_PORT);  

        expect(port).toBe(5000);  

    });  

});  

```

### Integration Tests

#### Test 4: App Configuration Connection

**File**: `test/integration/appconfig-connection.test.ts`

```typescript

import { describe, it, expect, beforeAll } from 'vitest';  

import { DevFramework } from 'framework-nodejs-core';  

import { app } from '../../src/internal/app.js';  

import { ENV_KEYS } from '../../src/constants/configuration.js';  

  

describe('Azure App Configuration Integration', () => {  

    beforeAll(async () => {  

        const frameworkApp = await DevFramework.create();  

        await frameworkApp.initAppConfig({ useEnvironmentVariables: true });  

        await app.prepareLookup(frameworkApp);  

    });  

  

    it('should successfully retrieve configuration from App Config', async () => {  

        const port = await app.lookup<number>(ENV_KEYS.APPLICATION_PORT);  

        expect(typeof port).toBe('number');  

        expect(port).toBeGreaterThan(0);  

    });  

  

    it('should retrieve all required configuration keys', async () => {  

        const configs = await Promise.all([  

            app.lookup<number>(ENV_KEYS.APPLICATION_PORT),  

            app.lookup<boolean>(ENV_KEYS.DB_SQL_MANAGED_IDENTITY),  

            app.lookup<string>(ENV_KEYS.DB_SQL_HOST),  

            app.lookup<string>(ENV_KEYS.AZURE_OPENAI_ENDPOINT),  

        ]);  

  

        expect(configs[0]).toBeDefined();  

        expect(configs[1]).toBeDefined();  

        expect(configs[2]).toBeDefined();  

        expect(configs[3]).toBeDefined();  

    });  

});  

```

#### Test 5: Key Vault Secret Resolution

**File**: `test/integration/keyvault-secrets.test.ts`

```typescript

import { describe, it, expect, beforeAll } from 'vitest';  

import { DevFramework } from 'framework-nodejs-core';  

import { app } from '../../src/internal/app.js';  

import { ENV_KEYS } from '../../src/constants/configuration.js';  

  

describe('Key Vault Secret Resolution', () => {  

    beforeAll(async () => {  

        const frameworkApp = await DevFramework.create();  

        await frameworkApp.initAppConfig({ useEnvironmentVariables: true });  

        await app.prepareLookup(frameworkApp);  

    });  

  

    it('should resolve Key Vault reference for OpenAI API key', async () => {  

        const apiKey = await app.lookup<string>(ENV_KEYS.AZURE_OPENAI_API_KEY);  

         

        expect(apiKey).toBeDefined();  

        expect(typeof apiKey).toBe('string');  

        expect(apiKey.length).toBeGreaterThan(0);  

        // Should not be the Key Vault reference JSON  

        expect(apiKey).not.toContain('vault.azure.net');  

    });  

  

    it('should resolve Key Vault reference for Form Recognizer key', async () => {  

        const frKey = await app.lookup<string>(ENV_KEYS.AZURE_FR_KEY);  

         

        expect(frKey).toBeDefined();  

        expect(typeof frKey).toBe('string');  

        expect(frKey.length).toBeGreaterThan(0);  

    });  

});  

```

### Contract Tests

#### Test 6: Configuration Schema Validation

**File**: `test/contract/config-schema.test.ts`

```typescript

import { describe, it, expect, beforeAll } from 'vitest';  

import { app } from '../../src/internal/app.js';  

import { ENV_KEYS } from '../../src/constants/configuration.js';  

  

describe('Configuration Schema Validation', () => {  

    beforeAll(async () => {  

        // Initialize app config  

    });  

  

    it('should validate APPLICATION_PORT is a valid port number', async () => {  

        const port = await app.lookup<number>(ENV_KEYS.APPLICATION_PORT);  

        expect(port).toBeGreaterThan(0);  

        expect(port).toBeLessThan(65536);  

    });  

  

    it('should validate DB_SQL_MANAGED_IDENTITY is boolean', async () => {  

        const useManagedIdentity = await app.lookup<boolean>(ENV_KEYS.DB_SQL_MANAGED_IDENTITY);  

        expect(typeof useManagedIdentity).toBe('boolean');  

    });  

  

    it('should validate endpoints are valid URLs', async () => {  

        const openAiEndpoint = await app.lookup<string>(ENV_KEYS.AZURE_OPENAI_ENDPOINT);  

        const frEndpoint = await app.lookup<string>(ENV_KEYS.AZURE_FR_ENDPOINT);  

         

        expect(() => new URL(openAiEndpoint)).not.toThrow();  

        expect(() => new URL(frEndpoint)).not.toThrow();  

    });  

});  

```

---

## Acceptance Criteria

### Functional Requirements

* [ ] All configuration values are retrieved from Azure App Configuration in deployed environments

* [ ] Local development works with `.env` file fallback

* [ ] Secrets (API keys) are stored in Key Vault and referenced through App Config

* [ ] All configuration keys use the `DOCAI_` prefix

* [ ] Boolean values (`DB_SQL_MANAGED_IDENTITY`) are correctly typed as booleans

* [ ] Numeric values (`APPLICATION_PORT`) are correctly typed as numbers

* [ ] String values are retrieved without type conversion

### Technical Requirements

* [ ] `app.lookup()` abstraction is used throughout the codebase

* [ ] No direct `process.env` access for configuration values

* [ ] Configuration constants defined in `src/constants/configuration.ts`

* [ ] Framework initialization happens before any configuration lookup

* [ ] Proper error handling for missing configuration

* [ ] TypeScript type safety maintained for all configuration values

### Testing Requirements

* [ ] Unit tests cover configuration lookup logic

* [ ] Unit tests cover type conversion (string → boolean, string → number)

* [ ] Integration tests validate Azure App Config connectivity

* [ ] Integration tests validate Key Vault secret resolution

* [ ] Contract tests validate configuration schema

* [ ] All tests pass in CI/CD pipeline

* [ ] Test coverage > 80% for configuration modules

### Security Requirements

* [ ] Sensitive values removed from `.env` files in version control

* [ ] `.env` contains only non-sensitive local development values

* [ ] Secrets stored in Azure Key Vault

* [ ] Managed Identity used for authentication (no connection strings in code)

* [ ] App Configuration connection string stored as Kubernetes secret

* [ ] No secrets logged or exposed in error messages

### Documentation Requirements

* [ ] README updated with App Config setup instructions

* [ ] `.env.example` file created with all required keys

* [ ] Migration guide documented for team members

* [ ] Architecture diagram updated

* [ ] Runbook created for App Config management

### Deployment Requirements

* [ ] App Configuration provisioned in all environments (dev, qa, uat, prd)

* [ ] All configuration keys created with appropriate labels/environments

* [ ] Managed Identity configured with correct RBAC roles

* [ ] Helm charts updated with App Config connection string

* [ ] Deployment pipeline validates configuration availability

* [ ] Rollback plan documented

---

## Non-Functional Requirements

### Performance

* Configuration lookup should add \< 100ms to application startup time

* Cached configuration values should not require repeated App Config calls

* Application should start successfully within 30 seconds

### Reliability

* Application should fail fast with clear error messages if configuration is missing

* Fallback to environment variables should work seamlessly in local development

* Service should handle App Config temporary unavailability gracefully

### Maintainability

* Configuration keys should follow consistent naming convention (`DOCAI_*`)

* Type conversions should be centralized in `createLookup.ts`

* Adding new configuration should require updates in only 2 files (`ENV_KEYS` and App Config)

---

## Risks and Mitigation

### Risk 1: App Configuration Unavailability

**Impact**: High - Service cannot start

**Mitigation**:

* Implement caching at application level

* Use environment variable fallback in deployment YAML as backup

* Monitor App Config availability with alerts

### Risk 2: Key Vault Access Issues

**Impact**: High - Cannot retrieve secrets

**Mitigation**:

* Validate Managed Identity permissions during deployment

* Implement retry logic with exponential backoff

* Monitor Key Vault access patterns

### Risk 3: Configuration Drift Between Environments

**Impact**: Medium - Inconsistent behavior across environments

**Mitigation**:

* Use Infrastructure as Code (Terraform/Bicep) to manage App Config

* Implement configuration validation tests in CI/CD

* Document configuration schema

### Risk 4: Breaking Changes During Migration

**Impact**: High - Service downtime

**Mitigation**:

* Implement feature flag to toggle between old and new config methods

* Deploy to non-production environments first

* Maintain backward compatibility during transition period

---

## Rollout Plan

### Phase 1: Development Environment (Week 1)

1. Implement configuration infrastructure code

2. Deploy to development environment

3. Run integration tests

4. Validate logging and monitoring

### Phase 2: QA Environment (Week 2)

1. Deploy to QA environment

2. Run full test suite

3. Performance testing

4. Security validation

### Phase 3: UAT Environment (Week 3)

1. Deploy to UAT environment

2. Stakeholder validation

3. Load testing

4. Documentation review

### Phase 4: Production Environment (Week 4)

1. Deploy during maintenance window

2. Monitor application health

3. Validate all services

4. Conduct post-deployment review

---

## Success Metrics

### Technical Metrics

* Zero configuration-related incidents post-migration

* Application startup time \< 30 seconds

* Configuration lookup latency \< 50ms (p95)

* 100% of secrets stored in Key Vault

* Zero secrets in version control

### Operational Metrics

* Configuration change time reduced from 30 minutes to 5 minutes

* Zero environment-specific configuration files in repository

* 100% configuration audit trail in App Config

* Mean time to update configuration \< 2 minutes

### Team Metrics

* All team members trained on App Config management

* Zero deployment failures due to missing configuration

* Configuration documentation maintained and up-to-date

---

## Related Documentation

* [Azure App Configuration Documentation](https://learn.microsoft.com/en-us/azure/azure-app-configuration/)

* [Key Vault Integration Guide](https://learn.microsoft.com/en-us/azure/azure-app-configuration/use-key-vault-references)

* [Managed Identity Best Practices](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview)

* fluxx-file-intake Configuration Pattern

---

## Story Points: 13

**Complexity**: High

**Effort**: 2-3 weeks

**Dependencies**: Azure App Configuration, Azure Key Vault, Managed Identity setup

---

## Definition of Done

* [ ] All acceptance criteria met

* [ ] Code reviewed and approved

* [ ] All tests passing (unit, integration, contract)

* [ ] Deployed to all environments

* [ ] Documentation updated

* [ ] Team training completed

* [ ] Monitoring and alerts configured

* [ ] Post-deployment validation successful

* [ ] Stakeholder sign-off received