Tech Stack
Every tool and service powering the Steeves & Associates BI platform — what each one does and why it was chosen.
Frontend
Function
Renders all dashboard pages (Overview, Market, Client Health, Allocation, Chat, Architecture, Tech Stack) using the App Router. Handles routing, server-side data fetching, and API proxying in development.
Why chosen
Chosen because it pairs React with file-based routing and built-in Vercel deployment. The App Router lets us mix server and client components, keeping data-heavy pages fast while keeping interactive charts and chat on the client side.
Function
Powers all interactive UI — filter dropdowns, chart tooltips, the chat message thread, allocation search, and dismissable chips. State is managed with useState and side-effects with useEffect.
Why chosen
Industry-standard for component-driven UIs. Rich ecosystem of chart and UI libraries built on top of it (Recharts, Lucide), which kept third-party dependencies minimal.
Function
Styles every element in the app. Custom brand tokens (steeves-navy, steeves-blue, steeves-gold, steeves-teal, steeves-muted) are defined in tailwind.config.js and used across all components. Shared utility classes like vz-card, vz-title, and vz-subtitle enforce visual consistency.
Why chosen
Eliminates the need for separate CSS files. Constraints like a fixed colour palette and spacing scale keep the dashboard visually consistent without a design system.
Function
Renders all charts in the dashboard — revenue trend line charts on the Overview page, bar charts for top customers and resources, pie/bar charts on the Market Position page, and score bar distributions on the Client Health page.
Why chosen
Built natively for React with a composable component API. Lighter than D3 for standard chart types, and the declarative syntax fits cleanly into Next.js page components.
Function
Converts the system architecture flowchart definition (written as plain text) into an SVG diagram rendered in the browser on the Architecture page.
Why chosen
Allows the architecture diagram to be version-controlled as text alongside the code, rather than as a binary image. Dynamically imported to avoid adding it to the initial page bundle.
Function
Provides all icons used in the sidebar navigation, action buttons, status indicators, and info tooltips across the UI.
Why chosen
Lightweight, tree-shakeable icon set with a consistent visual style. Each icon is an individual React component, so only the icons actually used end up in the bundle.
Function
Hosts the Next.js app and serves it globally via CDN. Automatically builds and deploys a new version every time code is pushed to the main branch on GitHub. Handles SSL certificates, edge caching, and preview URLs for branches.
Why chosen
Made by the same team as Next.js — zero configuration needed for deployment. The free Hobby plan covers the project's traffic with automatic deploys, removing any manual deployment step for the frontend.
Backend
Function
Runs all backend code — API route handlers, database queries, client health scoring (RFMT), resource allocation logic, NL-to-SQL generation, and LLM orchestration. The Docker image is built from python:3.11-slim.
Why chosen
Python has the best ecosystem for data science and LLM integration. Libraries like psycopg2, requests, and pandas are mature and well-documented. The slim Docker image keeps the container size small.
Function
Exposes all API endpoints consumed by the frontend. Routes are split into blueprints by feature: /api/overview, /api/competitors, /api/client-health, /api/allocation, /api/chat. Each blueprint maps HTTP requests to the corresponding service layer.
Why chosen
Lightweight and unopinionated — ideal for an API-only backend where the data logic lives in service modules rather than the framework. Faster to set up than Django for a project of this scope.
Function
Adds CORS headers to every API response so the Vercel-hosted frontend (a different domain) can call the Azure-hosted API. In production, allowed origins are restricted to the Vercel URL and localhost.
Why chosen
Browsers block cross-origin requests by default. Since the frontend and backend are on different domains (Vercel vs Azure), CORS headers are required for every API call to succeed.
Function
Sits in front of the Flask app inside the Docker container. Runs 2 sync worker processes, each handling HTTP requests independently. Configured with a 120-second timeout to accommodate slow LLM API calls.
Why chosen
Flask's built-in development server is single-threaded and not safe for production. Gunicorn handles concurrent requests reliably and integrates cleanly with Docker.
Function
Executes all SQL queries against the Azure PostgreSQL database. Uses a ThreadedConnectionPool (min 1, max 10 connections) so connections are reused across requests rather than opened and closed on every call.
Why chosen
The standard Python driver for PostgreSQL. The connection pool was added specifically to stay within the Azure B1ms tier's connection limit and avoid per-request connection overhead on a containerised app.
Function
Makes all HTTP calls to the OpenRouter API. The payload is serialised with json.dumps(ensure_ascii=True) and encoded as ASCII bytes before being sent, giving full control over encoding.
Why chosen
Replaced the OpenAI Python SDK after discovering that the SDK's internal httpx layer validated header values with .encode('latin-1'), which raised UnicodeEncodeError when the API key contained non-ASCII characters. Using requests directly avoids that layer entirely.
Database
Function
Stores all application data in three tables: time_entries (17,792 billable hour records from 2020–2025 used for all revenue and utilisation queries), competitors (50 Canadian Microsoft Azure partners used for market analysis), and chat_history (session-scoped conversation logs for the AI chat).
Why chosen
Relational model is the right fit for structured time-entry and competitor data. PostgreSQL's window functions (RANK, OVER), date extraction (EXTRACT QUARTER), and aggregation functions are used extensively in the dashboard queries.
Function
Runs the PostgreSQL 16 server on Azure infrastructure (Canada Central, B1ms tier). Handles automated backups, security patching, and SSL enforcement. Firewall rules allow connections from the Azure Container Apps environment and from local developer IPs.
Why chosen
Managed service eliminates database administration work. The Flexible Server tier allows pausing the instance during inactivity, reducing cost for a portfolio project. Hosting in Canada Central keeps data residency aligned with the company's Canadian operations.
AI & LLM
Function
Acts as a single API endpoint that routes requests to different LLM providers (DeepSeek, Meta, etc.). The backend sends all LLM calls to OpenRouter, which handles provider routing, rate limiting, and billing in one place.
Why chosen
Avoids maintaining separate API keys and SDKs for each model provider. Lets us mix a free model (Llama for classification) with a paid model (DeepSeek for generation) under one unified API, and makes swapping models trivial.
Function
Receives a natural language question plus the database schema and generates a valid read-only SQL query. Also narrates the query results into a business-friendly answer, and answers contextual questions about competitor data and operational metrics.
Why chosen
Benchmark results show DeepSeek V3 performs on par with GPT-4 class models for code and SQL generation at a fraction of the cost (~$0.0004 per conversation via OpenRouter). Chosen specifically for its strong SQL accuracy.
Function
Reads the user's chat message and classifies it into one of five intents: data_query, client_health, resource_recommend, document_qa, or general. The classified intent routes the request to the correct handler in gemini_chat.py.
Why chosen
Intent classification is a lightweight task that does not require the most capable model. Llama 3.3 70B is available on OpenRouter's free tier, so this step costs nothing. Keyword matching handles obvious cases first; the LLM only runs when keywords are ambiguous.
Function
Runs a local LLM (llama3.1:8b) on the developer's machine during development. Set via LLM_PROVIDER=ollama in the local .env file. The backend calls Ollama's /api/chat endpoint instead of OpenRouter.
Why chosen
Allows full end-to-end development and testing of all LLM-powered features without spending API credits or requiring an internet connection. The same code paths run; only the provider endpoint changes.
Infrastructure & DevOps
Function
Packages the Flask backend and all its dependencies into a single portable image. The Dockerfile installs Python requirements, copies the source code, exposes port 5000, and starts Gunicorn. The image is built for linux/amd64 to match the Azure Container Apps runtime.
Why chosen
Containerisation guarantees the app runs identically in development, CI, and production. It eliminates 'works on my machine' problems and makes the deployment unit self-contained — no need to install Python or dependencies on the server.
Function
Runs the Docker container in a managed serverless environment. Configured with min/max 1 replica (always on, no cold starts). Secrets (database URL, OpenRouter key) are stored as Container App secrets and injected as environment variables at container startup.
Why chosen
Serverless containers remove the need to manage VMs or Kubernetes clusters. Container Apps scales automatically and integrates natively with Azure Container Registry, making it the lowest-overhead way to run a containerised API on Azure.
Function
Stores every Docker image built by the CI pipeline. Each deploy pushes two tags: :latest (for human reference) and :<git-sha> (the tag actually deployed — immutable and traceable back to the exact commit). Azure Container Apps pulls the image directly from ACR.
Why chosen
Private registry co-located with the rest of the Azure infrastructure. The SHA-based tagging strategy means every running container can be traced back to the exact commit that produced it, which simplifies rollbacks and auditing.
Function
Automatically builds and deploys the backend whenever code touching backend/** is pushed to main. Pipeline steps: checkout → Azure login → ACR login → docker build (amd64) → docker push → update Container App secrets → deploy new image to Container App.
Why chosen
Eliminates manual deployments entirely. The pipeline runs on GitHub's infrastructure with no separate CI server to maintain. workflow_dispatch support also allows manual redeploys triggered directly from the GitHub UI.
Function
Encrypted storage for all sensitive values used by the pipeline: AZURE_CREDENTIALS, DATABASE_URL, OPENROUTER_API_KEY, ACR_NAME, ACR_LOGIN_SERVER, RESOURCE_GROUP, CONTAINERAPP_NAME, and CORS_ORIGINS. The CI workflow reads these at runtime and injects them into Azure.
Why chosen
Secrets never appear in code or logs. Separating secret storage from the deployment pipeline means rotating a key only requires updating one GitHub Secret and triggering a redeploy — no code changes needed.