Proof of concept · care-insights · Microsoft Fabric + Azure Data Lake

Find the budget, visit and invoice problems while there's still time to fix them.

An open lakehouse for home-care providers. Rostering, the worker mobile app and provider invoices land in Azure Data Lake Storage; Microsoft Fabric runs a bronze → silver → gold pipeline over them every morning; a code-first dashboard shows who will under-use or overspend their quarterly budget, which visits are being missed, and which invoices shouldn't be paid as-is.

Synthetic data. Every participant, worker and provider on this page is generated (fictitious IDs, no personal information). Problems were planted on purpose so the pipeline's findings can be checked against a known answer. Budgets, price caps and the carry-over rule are illustrative.
Isometric scene: four source systems feed a storage landing zone, a Fabric compute platform processes it, and results land in a lake that feeds a dashboard, a SQL endpoint and other engines.
  1. Sources: CRM, rostering, mobile app, provider invoices
  2. ADLS raw: files as received
  3. Fabric: Spark notebook, compute only
  4. ADLS lake: Delta bronze / silver / gold
  5. Consumers: dashboard, SQL endpoint, any Delta reader
$311k
at risk of being lost this quarter, flagged with 10 weeks to go
4 / 4
planted chronically-late workers found
182
invoice exceptions, $25k held back
13
SQL steps, one codebase, two engines
0
keys or passwords in the data path
~$0.36/h
Fabric F2, only while running (US$)

1. The problem: found too late

Under Support at Home, each participant has a quarterly budget. Unspent money above the carry-over limit is lost to the participant, and overspend has to be absorbed or clawed back. Both are usually discovered after the quarter closes, when nothing can be done. The same data also hides missed and late visits and errors in third-party provider invoices.

Approach. Join the three systems that each hold part of the picture (rostering, the worker app's check-ins and provider invoices) into one lakehouse, and answer four questions every morning. The budget forecast isn't a straight line: it's spend to date + in-house visits already booked for the rest of the quarter + provider invoices at their current run rate.

QuestionGold tableWhat the POC found (synthetic data)
Who will under-use or overspend their budget this quarter?gold_budget_utilisation76 participants under-used, ~$311k at risk of being lost; 122 forecast over budget
Are visits happening, and on time?gold_visit_reliability87.5% on time over the last 30 days; 218 missed
Which workers or rosters are consistently late?gold_worker_punctualityAll 4 planted chronically-late workers flagged
Which provider invoices should not be paid as-is?gold_invoice_exceptions52 duplicates, 124 above price cap (top offender PRV004), 6 unknown participants

For scale: in the quarter that just closed, the same rule estimates $366k was lost to participants. The difference this time is that the $311k is flagged in October, with ten weeks left to act.

2. An open lakehouse: ADLS stores, Fabric computes

Approach. Azure Data Lake Storage Gen2 is the system of record. Every layer (raw files, then bronze, silver and gold Delta tables) lives in your own storage account. Fabric reaches it through OneLake shortcuts and does the compute. Pause or remove Fabric and nothing is lost, and Databricks, Synapse, DuckDB or Polars can read the same tables.

What's in it

Solution architecture diagram: source systems, ADLS raw, Fabric workspace with lakehouse and notebook, ADLS lake with medallion layers, and consumers, plus the Entra identity and capacity.
Solution architecture. Click to open full size. It's an editable draw.io SVG: open it in draw.io or the VS Code draw.io extension.

Demonstrable artifacts

3. Medallion pipeline, checked against a known answer

Approach. The transform logic is plain Spark SQL files in transform/sql, run in order. Bronze keeps raw data as Delta. Silver cleans it: AU dd/MM/yyyy dates, de-duplicated app events, check-ins matched to bookings. Gold answers the four questions. The synthetic generator plants specific problems, and the gold tables are checked against them.

Isometric scene: a raw storage block feeds a bronze platform with seven tables, a silver platform with nine and a gold platform with four, which feeds four answer blocks: budgets, visits, workers and invoices.
  1. Raw: 124 CSV / JSONL files
  2. Bronze: 7 tables, as received
  3. Silver: 9 cleaned, conformed tables
  4. Gold: 4 tables, one per question
  5. Answers: budgets, visits, workers, invoices

Planted problem → did the pipeline find it?

Planted in the source dataFound in goldResult
4 chronically-late workers (W0016, W0018, W0033, W0043)Top 4 by late rate, all flagged needs reviewFound
~4% of visits missedMissed rate per worker and per day ≈ 4%Found
Provider PRV004 billing above the price capTop above-cap offender by $ at riskFound
1.5% duplicate invoices, 6 invoices for unknown participants52 duplicates and 6 unknown participants in invoice exceptionsFound
1% duplicate app events, 2% missing check-outs, AU date formatsDe-duplicated and parsed in silver; missing check-outs counted per visitHandled

The forecast, in the SQL itself

-- transform/sql/gold/01_budget_utilisation.sql (excerpt)
visit_spend_aud + invoice_spend_aud + booked_aud
  + invoice_spend_aud / days_elapsed * (days_in_quarter - days_elapsed)  AS forecast_spend_aud
...
CASE
  WHEN forecast_spend_aud > budget_aud       THEN 'Over budget'
  WHEN forecast_spend_aud < 0.7 * budget_aud THEN 'Under-used'
  ELSE 'On track'
END AS budget_status,
-- illustrative carry-over: unspent funds up to the greater of $1,000 or 10% of budget
GREATEST(budget_aud - forecast_spend_aud - GREATEST(1000.0, 0.1 * budget_aud), 0.0) AS est_funds_lost_aud
Medallion lineage diagram: seven bronze tables, nine silver SQL steps and four gold tables, with what each step does.
Medallion lineage: every bronze table, silver step and gold output. Editable draw.io SVG.

Demonstrable artifacts

4. Interactive demo: the gold tables

Approach. These charts are drawn in your browser from the pipeline's actual gold output (exported to a small JSON file, as of 18 Oct 2026). Pick a region; hover any mark for its numbers. The production dashboard is the Streamlit app further down, which queries the same tables live with DuckDB.

Where participants will land this quarter

Participants by forecast spend, % of budget (2026-Q4)

Funds at risk of being lost, by region

Estimated, above the carry-over limit

Action list

Under-used: call to review the care plan before funds are lost. Over budget: review bookings before the quarter ends.

Source: gold_* tables from the DuckDB test run of the same SQL the Fabric notebook runs. Synthetic data.

The production dashboard: Streamlit + DuckDB

A code-first app (app/dashboard.py) with KPI tiles, the budget distribution, funds at risk by region and a ranked action list, plus Visits, Workers and Invoices tabs. With CARE_SOURCE=adls it reads gold Delta straight from ADLS (delta_scan('abfss://lake@…/gold/…')) using the viewer's Entra login, with no gateway, extract or refresh schedule.

Screenshot of the Streamlit dashboard showing quarter KPIs, a histogram of forecast spend as a percent of budget, funds at risk by region and an action list.
The Streamlit dashboard, Budgets tab, 2026-Q4.

5. Same SQL, two engines: test on a laptop, run on Fabric

Approach. The Spark SQL files are the single source of transform logic. In Fabric, the notebook reads them from lake/_pipeline/sql and runs them on Spark. On a laptop, transform/run_local.py transpiles the same files to DuckDB with sqlglot and runs them in seconds, with no cloud cost, before anything ships.

Isometric scene: one SQL file block feeds two lanes: a laptop that writes local tables and a Fabric compute platform that writes the cloud lake.
  1. transform/sql: 13 Spark SQL files
  2. Laptop: sqlglot → DuckDB, all 13 steps in ~1 second
  3. Local Parquet: for testing and review
  4. Fabric Spark: notebook, scheduled daily
  5. ADLS lake: Delta, the system of record
# transform/run_local.py (core loop)
for f in sorted((SQL / layer).glob("*.sql")):
    spark_sql = f.read_text().replace("{{AS_OF_DATE}}", a.as_of)
    duck_sql = sqlglot.transpile(spark_sql, read="spark", write="duckdb")[0]
    ...
    con.execute(f"COPY {table} TO '{out.as_posix()}' (FORMAT parquet)")

Two portability lessons are now in the SQL: use TIMESTAMPDIFF(SECOND, …) rather than UNIX_TIMESTAMP arithmetic, and wrap date-plus-interval in CAST(… AS DATE), because DuckDB returns a timestamp there.

Diagram: the same SQL files run locally on DuckDB and in Fabric on Spark.
Same SQL, two engines. Editable draw.io SVG.

Demonstrable artifacts

6. Keyless identity, and an honest status

Approach. Everything authenticates with Entra ID. People upload data and read the dashboard with their own login (az login, then DuckDB's credential_chain). Fabric reaches ADLS with its workspace identity, granted Storage Blob Data Contributor through RBAC. Account keys exist only as a fallback flag (-AuthMode Key).

Isometric scene: a person signs in through an Entra ID shield, which grants access to storage and the dashboard; the path to Fabric is grey and dashed with a warning sign, meaning it is blocked.
  1. You: Entra sign-in, no keys
  2. Entra ID: issues tokens, RBAC on storage
  3. ADLS: upload and read working
  4. Dashboard: DuckDB → ADLS verified
  5. Fabric: token refused pending

Where it's up to

PieceStatus
Resource group, ADLS Gen2 account (Australia East)Deployed
Synthetic data in raw/ (124 files), pipeline SQL in lake/_pipeline/Uploaded
All 13 SQL steps + dashboard; DuckDB → ADLS Entra authTested
Fabric capacity, workspace, notebook runScripted; waiting on tenant enablement

The Fabric run is waiting on one tenant-level switch. Fabric depends on the Power BI Service enterprise app (even though this design uses no Power BI reports), and in the test tenant that app is disabled. An Entra administrator enables it, someone signs in once at app.fabric.microsoft.com, and fabric/deploy-fabric.ps1 -Schedule does the rest. Workspace creation can stay locked down to named people in Fabric's tenant settings.

Deployment and identity diagram: scripts, what they create, how each signs in, and where the tenant block sits.
Deployment and identity: what each script creates and how it signs in. Editable draw.io SVG.

Demonstrable artifacts

7. What it can be used for

The POC answers four questions for one program, but its parts are general: land files from systems you don't control, keep them in your own storage, clean them with SQL you can test on a laptop, and put a ranked action list in front of the people who can act on it.

Care managers

Budget conversations before quarter end

A weekly list of participants heading for under-use (funds at risk) or overspend, ranked by dollars, with the bookings already in the diary.

Rostering & operations

Missed and late visits

On-time rate by region and service, plus workers or rosters that are consistently late, so the fix is a roster change rather than a complaint.

Finance / accounts payable

Invoice exceptions before payment

Duplicates, rates above the price cap and invoices for unknown participants, held back with the dollar amount at risk.

Quality & compliance

Evidence for reporting

Delivered-versus-scheduled care and a full lineage from raw file to figure, to support quality reporting and audits.

Other funded programs

Same pattern, different rules

NDIS plan management, home care packages, or any "budget vs booked vs actual" domain such as grants, projects or field services. Change the gold SQL, not the platform.

Data teams

A Fabric on-ramp that isn't lock-in

Adopt Fabric for compute and scheduling while data stays open Delta in ADLS. Other engines read the same tables, and Fabric can be paused or swapped.

From POC to production

Capability scorecard

CapabilityStatusNotes
Infrastructure as codeDemonstratedBicep for storage, RBAC and capacity; capacity paused on creation; idempotent re-runs
Open lakehouse on ADLSDemonstratedRaw and lake containers deployed and loaded; every layer open Delta/Parquet
Medallion pipelineDemonstrated13 SQL steps; every planted problem found in gold
Same SQL on two enginesDemonstratedsqlglot Spark → DuckDB locally; Fabric notebook runs the files as-is
Code-first dashboardDemonstratedStreamlit + DuckDB + Plotly, local or straight from ADLS with Entra auth
Keyless identityDemonstratedEntra login for people; workspace identity + RBAC scripted for Fabric
Fabric run + scheduleScripted, pendingREST deployment ready; waits on the tenant enabling Fabric

Cost

ItemApproximate cost
ADLS Gen2, ~15 MBCents per month
Fabric F2 capacity~US$0.36/hour, only while running (~US$263/month if left on); paused by default
DashboardFree locally; ~US$10–20/month on Azure Container Apps