InsightClickIQ on Azure

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

InsightClickIQ on Azure: Container Apps with a managed identity, PostgreSQL Flexible Server, Blob Storage, Key Vault, Azure OpenAIInsightClickIQ on AzureOne identity does the work: the container's managed identity, found by DefaultAzureCredential everywhere.Your websiteclickiq.js snippet, ~8 KB gzip+ lazy rrweb recorderMobile appsiOS · Android · React Nativesame /api/ingest/batchConsole + agentsbrowser console, MCPclients (bearer token)COMPUTEFront Door / App GatewayTLS, routing, WAFAPI on Container Appsbuilt withINSIGHT_PLATFORM=azure;detected fromCONTAINER_APP_NAMEmin replicas 1 for thebackground loopManaged identityRBAC on Blob, Key Vault,Azure OpenAIMANAGED DATAFlexible Serverzone-redundant HARLS on org, partitionsRead replicaconsole reads, set byCLICKIQ_READ_DATABASE_…Blob Storageaz://account/containercool / archive tiersClickHouse Cloudoptional large tier;ADX = a new dialectPLATFORM SERVICESKey Vaultazure-kv://vault/nameAzure OpenAIyour deployment, keylessPurple: authenticated by the managed identity; no stored credential. Dashed: large tier only.The database password is the one secret left. It lives in Key Vault and is read by reference.
The Azure shape. Purple arrows are calls authenticated by the container's managed identity through DefaultAzureCredential: Blob, Key Vault and Azure OpenAI. The only stored secret is the database password, and it lives in Key Vault.
PieceOn AzureConfiguration
The APIAzure 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 databaseAzure 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 storeA Blob Storage container for replay bodies and report PDFs.CLICKIQ_REPLAY_STORE=az://account/container. Access through the managed identity; no account key.
SecretsKey Vault.Refs, never values: azure-kv://my-vault/clickiq-pg. Cached five minutes after the first read.
The modelAzure OpenAI.INSIGHT_LLM_PROVIDER=azure_openai, INSIGHT_AZURE_OPENAI_ENDPOINT, and INSIGHT_CHAT_MODEL naming your deployment (default gpt-4o). Keyless by default.
Large tierClickHouse 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 identityOnWhy
Storage Blob Data Contributorthe replay containerWrite 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 Userthe vault (or the named secrets)Read the Postgres password and, later, the ClickHouse password.
Cognitive Services OpenAI Userthe Azure OpenAI resourceCall 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.

TierEvents a month13 months of eventsFlexible Server
Pilot, internal app, small site0.1–2 M1–25 GBStandard_D2ds_v5 (2 vCPU, 8 GB), General Purpose
Mid-size product or store2–30 M25–350 GB4–8 vCPU / 32 GB, zone-redundant HA, a read replica
Large, many tenants30–300 M0.3–3.5 TBPostgres 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

90,275events in 30 days on the Insurance 360 demo
~1 GB13 months of Postgres at that rate
2deployments for two jurisdictions
0account keys in configuration
People · guid-android-0001
A person page for an identified mobile user, guid-android-0001, showing one Android session, sixteen events, the role trait student, and an AI line noting three errors on /LessonList
A person is an id and traits, not a name. This learner is known only by an account guid and a role. Erasing them removes the person row, every anonymous id they used, their sessions and events, and the replay objects in the storage container.

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.

Reports · Insurance 360 executive usage
The Insurance 360 executive usage report: last 7 days compared with the previous 7, an AI-written executive summary citing 106 active users, the most active manager and top clients, and win and anomaly findings
The report reads Postgres; its PDF lands in Blob. The summary cites the other panels' real numbers — 106 active users, 93.4% returning, Northgate Logistics at 114 client views. “Download PDF” is server-side when Chromium is in the image and a browser print when it is not.

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.

AI Agents
The AI Agents page listing nine playbooks — conversion analyst, frustration hunter, traffic analyst, page audit, journey explorer, error triage, form doctor, weekly review and install health — each with a Run button and a weekly or daily schedule toggle
Schedules need a process that stays up. A scheduled playbook is started by the API's hourly background round. On Container Apps, scale to zero would silently skip it: keep the minimum replica count at one.
SCALE TO ZEROCheapest, and quietly wrong

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.

MINIMUM ONE REPLICAThe background loop keeps running

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.

OptionStatusWhy
ClickHouse Cloud on AzureSupportedThe built dialect. Store its password in Key Vault and point CLICKIQ_CLICKHOUSE_SECRET at it.
ClickHouse on AKSSupportedSame dialect; you operate it.
Azure Data Explorer (Kusto)Not builtFast, but a different query language: it would need a third dialect in the engine. We would rather keep one.
SynapseNot for the consoleBatch-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 an X-Country header your edge sets.

The AWS shape →  ·  What the snippet never collects →

← All posts   Request a demo