← Back to list

Building the Power BI Writeback Platform: Architecture and Infrastructure as Code

This is the technical companion to From Report Documentation to a Reusable Power BI Writeback Platform. That post covers the why: the two…

Mandla Sibanda · 2026-05-29 21:32 · 0 claps · 5.4 min read
#power-bi #aws-cloudfront #s3 #aws-lambda-functions #rds-postgres
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud 🏛️ · Architecture

Building the Power BI Writeback Platform: Architecture and Infrastructure as Code

This is the technical companion to **From Report Documentation to a Reusable Power BI Writeback Platform**. That post covers the why: the two problems I was solving (sales forecasting and report documentation), why the existing options didn’t fit, and where native Fabric writeback is heading. If you want the problem context, start there.

This post is about the how.

The whole platform comes down to one shape: collect structured input from inside a Power BI report, write it to a central store, and read it back into Power BI. Below is how each piece actually works, and how I turned it into something I can extend with config instead of clicks.

The realization that shaped everything

Before the architecture, here’s the insight the whole thing is built around.

When the first documentation form worked, it clicked that the form wasn’t the interesting part. The skeleton was:

  • embed a form in Power BI
  • pass report context into it
  • collect structured input
  • send JSON to an API
  • validate against a schema
  • store in a shared table
  • read back into Power BI

That skeleton doesn’t care whether the form documents a report, captures a forecast, records an approval, or asks someone to explain a variance. The fields change; the plumbing doesn’t. So I built the architecture around the skeleton, not around documentation. Everything below follows from that.

The initial AWS architecture

The first build was deliberately small. I didn’t want a full web app stack, just a static form, a simple API, and a database.

The form is React + Vite, bundled into a static artifact so it can be hosted cheaply. It’s served from S3 through CloudFront and embedded in Power BI via the HTML Content visual and an iframe. The iframe URL carries the context: api_url, user, source_report, source_page, plus any form-specific prefill values.

On save, the form POSTs JSON to a Lambda Function URL. I used a Function URL instead of API Gateway because the first version didn’t need a real API surface. It needed one POST endpoint. The Lambda parses the payload, validates it against the form’s config, normalizes single-record or batch submissions, and inserts rows into Postgres (RDS). Power BI then connects back to the writebacks table with DirectQuery, filters by form_id, and expands whatever payload fields it needs.

End to end:

  1. User opens a Power BI report.
  2. Power BI loads the iframe form.
  3. CloudFront + S3 serve the static React form.
  4. The form reads URL context from the report.
  5. The user enters documentation or forecast values.
  6. The form POSTs JSON to the Lambda Function URL.
  7. Lambda validates the submission.
  8. Lambda inserts the row(s) into RDS Postgres.
  9. Power BI reads the rows back through DirectQuery.

One table, many forms

The decision I’m most glad I made early: don’t create a new table per form. If every form needed its own table, every new use case becomes a migration, an infra change, a modeling change, and a maintenance headache.

Instead, everything goes in one shared writebacks table. The stable metadata lives in columns:

form_id
record_id
submitted_at
submitted_by
source_report
source_page
request_id
environment
payload_version

The form-specific data lives in a single JSONB column:

payload

So a documentation submission and a forecast submission share a table even though their fields have nothing in common. A documentation payload might be:

{
"reportName": "Sales Performance",
"businessOwner": "Sales Operations",
"technicalOwner": "BI Team",
"whyCreated": "Track monthly sales performance by region",
"manualStepsRequired": "Update yearly target value every January",
"hardcodedValuesTechDebt": "Current fiscal year is manually set"
}

A forecast payload might be:

{
"customer_id": "12345",
"segment": "Residential",
"forecast_year": 2026,
"forecast_month": 7,
"forecast": 125000,
"prior_year": 118000,
"shipped_actual": 121500
}

Both are writebacks. They just represent different workflows.

The table is append-only. Every save is a new row, which gives me history and auditability for free. When Power BI needs the current value, it uses “latest per logical key” based on form_id, record_id, and submitted_at. This worked especially well for forecasting: one save writes a batch of monthly rows, each with a composite record_id (customer, segment, year, month), and Power BI picks the latest submission per logical row.

From PoC to infrastructure as code

A one-off can survive on manual setup. A reusable platform can’t. The goal was that adding a new form should feel like adding source files and config, not hand-wiring cloud infrastructure.

I used AWS CDK to define the platform. The repo discovers form manifests, builds the bundles, deploys static files to S3, updates the CloudFront-hosted form paths, and passes per-form validation config into Lambda via environment configuration. Every new form needs the same basics anyway: static hosting, the write API, validation config, database storage, integration docs, and a local dev path.

Going to code also made the thing safe to evolve: versioned, reviewable, and repeatable, instead of a sequence of clicks in the AWS console I’d have to remember. I also wanted contributors to work without AWS access, so there’s a local-only mode with form scaffolding, mock API behavior, and preview commands. You can create or edit a form without deploying anything.

A generic form template

Most writeback forms don’t need a custom React app. They need a few text inputs, dropdowns, numbers, booleans, and textareas. So I made a schema-driven pattern: a ui/schema.json file declares the fields, and a generic renderer draws the form. For simple forms, the schema is enough. For complex ones (a 12-month forecast matrix is not a five-field feedback form), you can still drop down to custom React.

Either way, the backend contract stays the same. Every form submits:

{
"form_id": "some-form",
"record_id": "some-logical-key",
"user": "user@company.com",
"source_report": "Report Name",
"source_page": "Page Name"
}

…and then adds its own fields. For batch workflows, it includes records:

{
"form_id": "sales-forecast",
"user": "user@company.com",
"records": [
{ "record_id": "customer-segment-2026–01", "forecast_month": 1, "forecast": 100000 },
{ "record_id": "customer-segment-2026–02", "forecast_month": 2, "forecast": 110000 }
]
}

So the forecast form writes multiple related rows in one transaction, while the documentation form stays a simple single-record submission. New forms start from a standard pattern instead of a blank project.

What I’d harden next

This started as a PoC, so there’s plenty I’d shore up before calling it a mature internal product. The big one is authentication. The early version uses a Lambda Function URL, which kept things simple, but a production version needs real identity and authorization. The user value passed from Power BI is fine for audit context. It should not be treated as a trusted identity boundary on its own.

After that:

  • a read-only database user for Power BI
  • stronger validation rules
  • automated tests around the Lambda handler
  • stable Power BI report identifiers
  • better record-ID conventions
  • monitoring and alerting
  • an admin UI for creating and managing forms
  • a documentation completeness score
  • a report maintenance calendar

Native Fabric writeback is worth watching too. If we move deeper into Fabric, Translytical task flows may be the better long-term path for some of these workflows. But building the custom version taught me a lot about the actual shape of the problem.

Closing

The thing I keep coming back to is how little code the core actually needed: an embedded static form, one POST endpoint, a single shared table, and DirectQuery read-back. The architecture is built around the skeleton, so a new use case is mostly a schema and some config, not a new system.


메타데이터
post_id
0807cc45d564
slug
building-the-power-bi-writeback-platform-architecture-and-infrastructure-as-code-0807cc45d564
url
https://medium.com/@mandlahsibanda/building-the-power-bi-writeback-platform-architecture-and-infrastructure-as-code-0807cc45d564
canonical_url
https://medium.com/@mandlahsibanda/building-the-power-bi-writeback-platform-architecture-and-infrastructure-as-code-0807cc45d564
author_url
https://medium.com/@mandlahsibanda
status
ok
fetched_at
2026-06-09 15:37:30