Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5318246
docs: Add comprehensive Azure deployment guides
Aparnap2 Feb 28, 2026
c995331
docs: Add Azure deployment to README quick start
Aparnap2 Feb 28, 2026
94b7a4d
feat: Add complete Azure deployment infrastructure
Aparnap2 Feb 28, 2026
0fbc332
feat(azure-migration): Replace Cloudflare primitives with Azure equiv…
Aparnap2 Feb 28, 2026
e1778ec
feat: Wire Azure Queue consumer into FastAPI + update worker deps
Aparnap2 Feb 28, 2026
e747171
docs: Update README and DEPLOY with current architecture
Aparnap2 Feb 28, 2026
d1ef0ad
docs: Update PRD and add ARCHITECTURE.md
Aparnap2 Feb 28, 2026
6d3d059
docs: Remove outdated documentation
Aparnap2 Feb 28, 2026
5386a7e
feat: implement AP workflow state machine with LangGraph
Aparnap2 Mar 2, 2026
1c9655a
fix: correct test imports and syntax errors
Aparnap2 Mar 2, 2026
c8a0736
docs: Add comprehensive implementation summary
Aparnap2 Mar 4, 2026
45ba30d
feat: Add production E2E test with real Azure + Docker
Aparnap2 Mar 5, 2026
8129e46
chore: PHASE 1+2 - Remove voice-agent + migrate to direct Postgres
Aparnap2 Mar 6, 2026
2c11c7f
chore: PHASE 4 - remove legacy edge-api + azure-functions + edge_call…
Aparnap2 Mar 6, 2026
4220811
chore: remove orphaned salesforce test file (SF replaced by HubSpot)
Aparnap2 Mar 6, 2026
217905a
chore: delete orphaned salesforce_mcp.py (replaced by hubspot_mcp.py)
Aparnap2 Mar 6, 2026
362f9bd
fix: MCP server tool registration (Server → FastMCP) + HubSpot integr…
Aparnap2 Mar 6, 2026
d2090c8
security: Remove all .secrets/ from git tracking (qb_tokens.json)
Aparnap2 Mar 6, 2026
8af5e33
docs: Update all documentation with HubSpot integration + better Merm…
Aparnap2 Mar 6, 2026
1fc45af
security: Remove qb_tokens.json from git (accidentally added)
Aparnap2 Mar 6, 2026
21e612c
fix: Address all code review issues
Aparnap2 Mar 7, 2026
25ecc98
security: Remove qb_tokens.json from git (accidentally added again)
Aparnap2 Mar 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .env.azure.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# .env.azure.example
# Copy to .env and fill in values for local development against Azure services.
# In production, these are injected as Azure Container Apps secrets (not .env files).
#
# NEVER commit real values. This file is for documentation only.

# ── LLM (OpenRouter free tier) ────────────────────────────────────────────────
# Get key at: https://openrouter.ai/keys
OPENAI_API_KEY=sk-or-v1-...
OPENAI_BASE_URL=https://openrouter.ai/api/v1
LLM_MODEL=z-ai/glm-4.5-air:free

# Groq (for fast JSON extraction step)
# Get key at: https://console.groq.com
GROQ_API_KEY=gsk_...

# ── Extractor mode ────────────────────────────────────────────────────────────
# Options: fixture | azure_di | ollama | sarvam
# Use 'fixture' for local dev without Azure keys
EXTRACTOR_MODE=fixture

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The environment variable EXTRACTOR_MODE is defined here and again on line 91. This redundancy can be confusing as the last one will take precedence. To improve clarity and avoid potential configuration errors, please remove one of the definitions to have a single source of truth for this setting.


# ── Azure Document Intelligence (F0 free tier: 500 pages/month) ───────────────
# Create at: https://portal.azure.com → Document Intelligence
AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT=https://your-instance.cognitiveservices.azure.com/
AZURE_DOCUMENT_INTELLIGENCE_KEY=your-key-here

# ── Azure Storage (5 GB free LRS) ─────────────────────────────────────────────
# Create at: https://portal.azure.com → Storage Accounts
AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net
AZURE_STORAGE_CONTAINER=invoices

# ── Azure Storage Queues (free, no limits) ────────────────────────────────────
# Same storage account — no extra resource needed
AZURE_QUEUE_NAME=invoice-processing
AZURE_DLQ_NAME=invoice-dlq

# ── Azure AI Search (F (free) tier: 50 MB, 3 indexes) ────────────────────────
# Create at: https://portal.azure.com → AI Search
AZURE_SEARCH_ENDPOINT=https://your-search.search.windows.net
AZURE_SEARCH_KEY=your-admin-key
AZURE_SEARCH_INDEX=invoices

# ── PostgreSQL (local dev) ────────────────────────────────────────────────────
# In production: Azure Flexible Server B1MS
DATABASE_URL=postgresql://invoicify:password@localhost:5432/invoicify
CHECKPOINTER_URL=postgresql://invoicify:password@localhost:5432/invoicify

# ── Internal service URLs ─────────────────────────────────────────────────────
# worker calls agent-core for AI processing
AGENT_CORE_URL=http://localhost:8001
# agent-core calls worker for status callbacks
EDGE_API_BASE_URL=http://localhost:8787

# ── Observability (Langfuse cloud free: 50k events/month) ────────────────────
# Get keys at: https://cloud.langfuse.com
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_HOST=https://cloud.langfuse.com

# ── Integrations ─────────────────────────────────────────────────────────────
SLACK_BOT_TOKEN=xoxb-...
SLACK_SIGNING_SECRET=...

# ═══════════════════════════════════════════════════════════════
# ERP Integrations (QuickBooks + HubSpot CRM)
# ═══════════════════════════════════════════════════════════════

# QuickBooks Online (Sandbox)
# Get tokens: https://developer.intuit.com/app/developer/playground
QB_CLIENT_ID=your_client_id
QB_CLIENT_SECRET=your_client_secret
QB_REALM_ID=4620816365162546440 # sandbox company ID
QB_REFRESH_TOKEN=your_refresh_token # from one-time OAuth flow
QB_SANDBOX=true

# ═══════════════════════════════════════════════════════════════
# HubSpot CRM (Free - replaces Salesforce)
# ═══════════════════════════════════════════════════════════════

# HubSpot Private App API token (replaces Salesforce JWT)
# Get token: app.hubspot.com → Settings → Integrations → Private Apps
# 1. Create private app named "Invoicify"
# 2. Select scopes: crm.objects.deals.*, crm.objects.companies.*
# 3. Copy token (starts with pat-na1-...)
# 4. Token never expires unless manually revoked
HUBSPOT_API_KEY=pat-na1-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

# ── Environment ───────────────────────────────────────────────────────────────
ENVIRONMENT=development
LOG_LEVEL=INFO
STRATEGY_MODE=OPTIMIZE

# ── Redis (Azure Cache for Redis) ─────────────────────────────────────────────
# Used for OAuth token store in stateless containerized environments
# Create at: https://portal.azure.com → Cache for Redis (C0 free tier available)
# Connection string format: rediss://:access-key@hostname:port
REDIS_URL=rediss://:your-redis-key@your-redis-host:6380
170 changes: 170 additions & 0 deletions .github/workflows/azure-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# .github/workflows/azure-deploy.yml
# Deploys the Invoicify monorepo to Azure free tier.
#
# Services deployed:
# invoicify-worker → Azure Container Apps (Node 20, replaces Cloudflare Worker)
# invoicify-api → Azure Container Apps (Python 3.11 agent-core)
# apps/web → Azure Static Web Apps (Next.js)
#
# Triggers: push to feat/azure-native-migration or main
# Required GitHub Secrets:
# AZURE_CLIENT_ID (federated identity, no password needed)
# AZURE_TENANT_ID
# AZURE_SUBSCRIPTION_ID
# AZURE_STATIC_WEB_APPS_TOKEN
# REGISTRY_NAME (e.g. invoicifyregistry)
# RESOURCE_GROUP (e.g. invoicify-rg)

name: Deploy to Azure

on:
push:
branches:
- main
- feat/azure-native-migration
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main

permissions:
id-token: write # OIDC federated auth — no password secrets needed
contents: read

jobs:
# ─────────────────────────────────────────────────────────────────────────
# Job 1: Build and deploy Hono worker (replaces Cloudflare Worker)
# ─────────────────────────────────────────────────────────────────────────
deploy-worker:
name: Deploy invoicify-worker → Container Apps
runs-on: ubuntu-latest
if: github.event_name == 'push'

steps:
- uses: actions/checkout@v4

- name: Azure login (OIDC)
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Build worker image
working-directory: invoicify-worker
run: |
az acr login --name ${{ secrets.REGISTRY_NAME }}
docker build \
-t ${{ secrets.REGISTRY_NAME }}.azurecr.io/invoicify-worker:${{ github.sha }} \
-t ${{ secrets.REGISTRY_NAME }}.azurecr.io/invoicify-worker:latest \
.
docker push ${{ secrets.REGISTRY_NAME }}.azurecr.io/invoicify-worker:${{ github.sha }}
docker push ${{ secrets.REGISTRY_NAME }}.azurecr.io/invoicify-worker:latest

- name: Deploy worker to Container Apps
run: |
az containerapp update \
--name invoicify-worker \
--resource-group ${{ secrets.RESOURCE_GROUP }} \
--image ${{ secrets.REGISTRY_NAME }}.azurecr.io/invoicify-worker:${{ github.sha }} \
--set-env-vars \
ENVIRONMENT=production \
PORT=8787 \
DATABASE_URL=secretref:database-url \
AZURE_STORAGE_CONNECTION_STRING=secretref:storage-connection-string \
OPENAI_API_KEY=secretref:openai-api-key \
GROQ_API_KEY=secretref:groq-api-key \
AGENT_CORE_URL=secretref:agent-core-url

# ─────────────────────────────────────────────────────────────────────────
# Job 2: Build and deploy Python agent-core
# ─────────────────────────────────────────────────────────────────────────
deploy-agent-core:
name: Deploy agent-core → Container Apps
runs-on: ubuntu-latest
if: github.event_name == 'push'

steps:
- uses: actions/checkout@v4

- name: Azure login (OIDC)
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Build agent-core image
working-directory: apps/agent-core
run: |
az acr login --name ${{ secrets.REGISTRY_NAME }}
docker build \
-t ${{ secrets.REGISTRY_NAME }}.azurecr.io/agent-core:${{ github.sha }} \
-t ${{ secrets.REGISTRY_NAME }}.azurecr.io/agent-core:latest \
.
docker push ${{ secrets.REGISTRY_NAME }}.azurecr.io/agent-core:${{ github.sha }}
docker push ${{ secrets.REGISTRY_NAME }}.azurecr.io/agent-core:latest

- name: Deploy agent-core to Container Apps
run: |
az containerapp update \
--name invoicify-api \
--resource-group ${{ secrets.RESOURCE_GROUP }} \
--image ${{ secrets.REGISTRY_NAME }}.azurecr.io/agent-core:${{ github.sha }} \
--set-env-vars \
ENVIRONMENT=production \
EXTRACTOR_MODE=azure_di \
DATABASE_URL=secretref:database-url \
CHECKPOINTER_URL=secretref:database-url \
AZURE_STORAGE_CONNECTION_STRING=secretref:storage-connection-string \
AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT=secretref:adi-endpoint \
AZURE_DOCUMENT_INTELLIGENCE_KEY=secretref:adi-key \
AZURE_SEARCH_ENDPOINT=secretref:search-endpoint \
AZURE_SEARCH_KEY=secretref:search-key \
OPENAI_API_KEY=secretref:openai-api-key \
GROQ_API_KEY=secretref:groq-api-key \
LANGFUSE_PUBLIC_KEY=secretref:langfuse-public-key \
LANGFUSE_SECRET_KEY=secretref:langfuse-secret-key

# ─────────────────────────────────────────────────────────────────────────
# Job 3: Deploy Next.js frontend to Static Web Apps
# ─────────────────────────────────────────────────────────────────────────
deploy-web:
name: Deploy apps/web → Static Web Apps
runs-on: ubuntu-latest
if: |
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.action != 'closed')

steps:
- uses: actions/checkout@v4
with:
submodules: true

- name: Deploy to Azure Static Web Apps
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
action: upload
app_location: /apps/web
api_location: ''
output_location: .next
env:
NEXT_PUBLIC_API_URL: ${{ vars.API_URL }}
NEXT_PUBLIC_WORKER_URL: ${{ vars.WORKER_URL }}

# ─────────────────────────────────────────────────────────────────────────
# Job 4: Close preview environment on PR close
# ─────────────────────────────────────────────────────────────────────────
close-preview:
name: Close Static Web Apps preview
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.action == 'closed'

steps:
- uses: actions/checkout@v4
- uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_TOKEN }}
action: close
Loading
Loading