
Most Azure estates already have the services InsightClickIQ needs: a PostgreSQL Flexible Server pattern, a storage account policy, a Key Vault per environment and an Azure OpenAI resource someone fought procurement for. The product uses all four as they are. What it adds is one container, and the useful thing about that container on Azure is that it needs exactly one identity and almost no secrets.
This is the Azure shape, the reason it is shaped that way, and a worked example of the case Azure customers raise most often: analytics on an internal application whose users sit in two jurisdictions.
The shape
| Piece | On Azure | Configuration |
|---|---|---|
| The API | Azure Container Apps (or AKS), image built with INSIGHT_PLATFORM=azure. The platform library also detects Azure on its own from CONTAINER_APP_NAME or the identity endpoint. | Keep at least one replica running: the background rounds (alerts every minute, signals every 30 minutes, retention and partitions daily, scheduled agent runs hourly) live in the API process. |
| The database | Azure Database for PostgreSQL Flexible Server, zone-redundant HA, with a read replica for the console. | INSIGHT_DATABASE_URL without a password, INSIGHT_DATABASE_SECRET=azure-kv://vault/name, INSIGHT_DATABASE_SSLMODE=require, CLICKIQ_READ_DATABASE_URL for the replica. |
| Object store | A Blob Storage container for replay bodies and report PDFs. | CLICKIQ_REPLAY_STORE=az://account/container. Access through the managed identity; no account key. |
| Secrets | Key Vault. | Refs, never values: azure-kv://my-vault/clickiq-pg. Cached five minutes after the first read. |
| The model | Azure OpenAI. | INSIGHT_LLM_PROVIDER=azure_openai, INSIGHT_AZURE_OPENAI_ENDPOINT, and INSIGHT_CHAT_MODEL naming your deployment (default gpt-4o). Keyless by default. |
| Large tier | ClickHouse Cloud on Azure, or self-managed on AKS. | CLICKIQ_EVENT_STORE=clickhouse. Only the event table moves. |
One identity, everywhere
The Azure parts of the platform library all authenticate the same way: the storage backend
opens Blob with DefaultAzureCredential, Key Vault secrets resolve with it, and
the Azure OpenAI client asks it for a Microsoft Entra token scoped to Cognitive Services
whenever no API key reference is configured. On Container Apps that credential is the
app's managed identity. So the role assignments are the whole access design:
| Grant the managed identity | On | Why |
|---|---|---|
| Storage Blob Data Contributor | the replay container | Write chunks as sessions are recorded, read them for the player and heatmap snapshots, delete them when retention or an erasure says so. |
| Key Vault Secrets User | the vault (or the named secrets) | Read the Postgres password and, later, the ClickHouse password. |
| Cognitive Services OpenAI User | the Azure OpenAI resource | Call your chat deployment. No key to rotate. |
| Nothing else | — | The API never manages resources, lists accounts, or reads other vaults. |
The database password is the one secret left, and that is a limit of how Flexible Server is
usually wired, not of the product. It sits in Key Vault and the API reads it by reference.
If you set INSIGHT_DATABASE_PASSWORD directly, GET /api/health/config
says so in its warnings, which is a useful line in a deployment checklist.
Row-level security still binds
Create the application role as an ordinary login: NOSUPERUSER NOBYPASSRLS. The Flexible Server admin you get at provisioning time is the wrong account for the API. Every product table carries the organization and a policy on it, and RLS only protects you if the connecting role is subject to it.
Flexible Server, sized from measurements
One table grows: event. The storage review measured it at about 0.9 KB per event
with indexes (271,000 events took 246 MB); after monthly partitioning and an index diet, the
running demo's October partition is 228,798 events in 167 MB. Sessions and people add about
a tenth. Everything else in the schema is console content and stays small.
| Tier | Events a month | 13 months of events | Flexible Server |
|---|---|---|---|
| Pilot, internal app, small site | 0.1–2 M | 1–25 GB | Standard_D2ds_v5 (2 vCPU, 8 GB), General Purpose |
| Mid-size product or store | 2–30 M | 25–350 GB | 4–8 vCPU / 32 GB, zone-redundant HA, a read replica |
| Large, many tenants | 30–300 M | 0.3–3.5 TB | Postgres to about 100 M a month, then ClickHouse for events |
A few server parameters are worth setting through the portal or Bicep rather than leaving at
defaults. The application role should carry statement_timeout = 30s and
work_mem = 64MB; the database random_page_cost = 1.1, since Flexible
Server storage is SSD. The event partitions set their own autovacuum scale factor (0.02),
and the API creates next months' partitions itself every day, so you do not need
pg_partman or a cron job. If you put PgBouncer in front — Flexible Server
has a built-in one — use transaction mode; the tenant is set per transaction, which is
what that mode requires.
The residency case

Blob Storage: replays, and the PDF question
Replays are rrweb chunks, gzipped, typically 50–300 KB per recorded session. They are the largest bytes the product writes and they never go into Postgres. A lifecycle policy that moves blobs to the cool tier after 30 days and archive after 90 suits how people use them: replays are watched within days of a signal or an investigation, rarely after.
Report PDFs need a word of care on Azure, as anywhere. GET /api/reports/{id}/pdf
prints the console's print route with headless Chromium through Playwright, and the stock
API image does not include a browser. Without one, the endpoint answers 501 with a reason and
the console opens the print view and calls the browser's own print dialog, which saves an
identical A4 PDF. If you want server-side PDFs — for kept snapshots, or for sending
without a person in the loop — add Playwright and Chromium to the image and set
CLICKIQ_CONSOLE_URL to where the console runs.

Azure OpenAI, and running without it
Every AI surface goes through one chat-model function. On Azure that is your Azure OpenAI
deployment, named in INSIGHT_CHAT_MODEL, at API version
2024-10-21 unless you set another. The model plans queries in a validated
format against the project's real event names and writes the narrative around numbers the
engine has already computed.
If the deployment is throttled, unreachable from your network, or not approved yet, nothing
breaks. Insight strips, chart explanations, signals, the weekly digest, investigations and
agent playbooks all have a rules path, labelled “Rules” in the console. That makes
a sensible rollout order for a cautious organisation: deploy with
CLICKIQ_AI=false, let people use the analytics, then switch the model on once
the data-protection review has seen what it receives.

Container Apps stops the API when no requests arrive. Ingest still works when traffic returns, but signals stop refreshing, retention skips its 03:00 run, the daily partition round does not run, and weekly playbooks never start.
One replica always on. With more than one, each round takes a Postgres advisory lock, so exactly one replica does it and the others skip — no separate worker to deploy.
The large tier: ClickHouse, not Data Explorer
Above about 100 million events a month, 30-day scans of the event table get slow on any Postgres. The product's answer is to move that one table to ClickHouse, which the query engine already speaks as a second dialect: the same query specification produces the same answer on both engines, measured 1.3–17× faster per statement on ClickHouse in our test. On Azure that means ClickHouse Cloud on Azure, or a self-managed cluster on AKS.
| Option | Status | Why |
|---|---|---|
| ClickHouse Cloud on Azure | Supported | The built dialect. Store its password in Key Vault and point CLICKIQ_CLICKHOUSE_SECRET at it. |
| ClickHouse on AKS | Supported | Same dialect; you operate it. |
| Azure Data Explorer (Kusto) | Not built | Fast, but a different query language: it would need a third dialect in the engine. We would rather keep one. |
| Synapse | Not for the console | Batch-oriented: it reads the warehouse export’s Parquet in Blob Storage (az://) with OPENROWSET, not interactive console reads. |
The switch is configuration and a backfill: turn on dual-write while Postgres is still the
store, copy history with the backfill script (re-runnable, it skips ids already copied),
then set CLICKIQ_EVENT_STORE=clickhouse and leave dual-write on. Sessions,
people, dashboards, reports and replays stay exactly where they were.
On Azure the access design fits in four role assignments. If a deployment needs more than that, something is configured the hard way.
Checklist for the platform team
- Flexible Server in a delegated subnet, TLS required, automated backups with point-in-time restore, and a restore tested every quarter.
- An ordinary application role subject to RLS; the admin login kept for migrations by hand only.
- Storage account with public access disabled, a lifecycle policy for replays, and the managed identity as the only data-plane principal.
- Container App with a minimum of one replica and a health probe on
/api/health; readiness on/api/health/readiness, which checks Postgres, the replay store and the event store. - Country from the browser's time zone by default, or from a MaxMind database mounted into
the container (
CLICKIQ_GEOIP_DB), or from anX-Countryheader your edge sets.


