A Repo That Mirrors Your Architecture - Source Control & Structure in Fabric
Your repo structure is your architecture, made versionable

This is Episode 3 of the From Chaos to Confidence series. In Episode 2 we designed the Fabric solition: layers, how granular to make them, and the fact that whatever you land on gets replicated across dev, test and prod. That architecture currently exists in the best possible place for an architecture to fail... in a diagram.
So let's make it real. Not automated yet, that's Episode 4, but versionable. The shift that matters is smaller than it sounds: your architecture stops being a drawing and becomes files. Folders you can open, text you can diff, names you can search. That's what turns it from a picture of intent into something that actually exists. Git is what you get once that's true. Branch it, review it, roll it back, hand it to a new colleague without a two-hour whiteboard session.
The thesis of this episode is a single sentence: your repo structure is your architecture, made versionable. If the two don't match, one of them is lying.
Why source control, before anything else
I've watched a lot of teams put off source control. Nobody ever decides against it. It just never becomes urgent. The platform works, people are shipping and nothing is on fire.
Then something is on fire. Someone changed a semantic model on Thursday afternoon. By Friday morning the numbers are wrong, and nobody can say what changed. With a Git repo, that's a five-minute diff and a revert. Without one, it's a support ticket and a memory contest.
Here's what you get the moment your platform lives in Git:
History and rollback: Every change becomes a commit with an author, a timestamp and a diff you can read. You can see what changed, who changed it and put it back.
Isolation: Branching lets you build something half-finished without anyone else seeing your half-finished thing. This is the difference between "development" and "everyone works in prod and hopes".
Collaboration without collisions: Two people, two branches, one pull request each. Nobody overwrites anybody. Nobody has to shout across the office to ask if it's safe to save.
Review before reality: A PR is the cheapest quality gate you will ever get.
Two-way sync: Fabric's Git integration isn't an export. You commit from the workspace and you update the workspace from Git. So the repo and the platform stay honest with each other.
"But Fabric already has version history"
Yes it does and this is the objection I hear often, so it deserves a fair answer.
Fabric notebooks have built-in version history: system checkpoints created automatically every five minutes as you edit, manual checkpoints for your own milestones, a diff view, and restore. Semantic models have it too. Neither is something you switch on. Fabric just configures it for you, which is part of why some teams may assume they're covered.
But it's an undo button, not source control, and the difference shows up in four places:
It's per item, not per solution: A notebook's history knows about that notebook. It has no idea the same change also needed a new field in a report or change to a query in a pipeline. A commit does, because a commit spans the whole solution.
It's shallow: Semantic models keep five versions, and restoring anything older than 14 days isn't supported. Notebook system checkpoints expire after a year. Git keeps every version for as long as the repo exists.
Most item types don't have it at all: Pipelines, lakehouses, reports, dataflows, environments, variable libraries... nothing. That list might improve, so check its current state rather than trusting me. But your platform will always be mostly items that aren't covered.
There's no branch, no PR, no review: Version history lets you go back. It doesn't let two people work in parallel, and it doesn't let anyone look at a change before it lands in the workspace.
Microsoft's own documentation lands in the same place. For full source control and support of more versions, use Git integration. And you can happily run both on the same semantic model. Item version history saves you from the last change you regret. Git is what gives your platform a memory.
Or, put less politely: without source control, your platform has no memory, no safety net, and no way to collaborate at scale.
How Fabric's Git integration actually works
The mechanic that shapes everything else is this: Git integration is configured per workspace, and a workspace connects to exactly one folder in one branch of one repository.
Four things to know before you design around it:
Providers. Azure DevOps, GitHub and GitHub Enterprise (cloud only in all three cases). Azure DevOps can live in a different tenant than your Fabric tenant, which comes up more often than you'd think in consultancies and post-merger estates.
Authentication. With Azure DevOps you can connect as yourself (the automatic credential, using your Entra identity) or through a configured connection that supports OAuth 2.0 or a service principal. And the service principal option is the one that matters, because it's what lets automation connect a workspace with no human signed in. GitHub's source control connector uses a personal access token. That asymmetry is worth knowing up front. If unattended automation is where you're heading, Azure DevOps currently gives you the cleaner identity story. (It's also the reason FabricOps supports both providers but treats the git credential as a connection rather than a login.)
What travels. Item definitions, not data. A lakehouse in Git is lakehouse.metadata.json and shortcuts.metadata.json. It's the shape of the thing, not a single row inside it. Semantic models arrive as TMDL, reports as PBIR, notebooks as source files. Coverage is broad these days. Very nearly every item type is supported, though some are still marked preview, including the two you'd least expect: reports and semantic models. And if a workspace does hold something unsupported, you can still connect it. Those items are simply invisible to Git. They aren't committed, synced or deleted, and they never appear in the source control pane at all. They're flagged as Unsupported in the workspace's Git status column, which is the only place you'll see them. Either way, check the current list rather than trusting this blogpost.
Folders are preserved. If you organise items into folders inside a workspace, those folders appear in the repo, and vice versa. Nested up to ten levels, empty folders aren't committed. And a small trap, connecting a workspace that already has folders to a branch that doesn't yet have them shows up as uncommitted changes. Commit first, update second.
There's more in the box these days than when I first started doing this: selective branching, related branches, comparing changes before you sync, branched workspaces, variable libraries for environment-specific values, and tooling in VS Code. I'm deliberately not going deep on any of it here. The branching workflow will be covered in a later episode. For now, all you need is the shape: workspace β repo β branch β folder.
One caveat worth saying out loud before we build on this: Git makes your platform versionable, not reproducible. A branch tells you what the items should look like. It doesn't create the workspaces, wire the connections or promise that dev and prod are the same shape. That's a different problem... and again something I will cover in a later episode.
The repo that mirrors your architecture
Here's the part I actually care about. You have seven layers (or three, or four. That is Episode 2's spectrum). A Git connected workspace connects to one folder. So:
One layer = one workspace = one repo folder.
That's it. That's the whole design. It sounds almost too simple to write down and it's the thing that makes everything downstream tractable.
This is the top of the FabricOps repo. So a real one and not just something idealised:
FabricOps/
βββ .azure-pipelines/ # Azure DevOps pipeline definitions
βββ .github/ # GitHub Actions workflows
βββ automation/ # Scripts, environment definitions, IaC
βββ documentation/ # Docs and design notes
βββ solution/ # The platform layers themself
βββ core/
βββ ingest/
βββ prepare/
βββ store/
βββ model/
βββ present/
βββ orchestrate/
The subfolders of the solution folder are the layers from Episode 2, one by one, same names. No mapping table, no translation layer, no tribal knowledge. Someone who has read the architecture diagram can navigate this repo and someone who has read the repo can draw the architecture diagram.
Go one level deeper and you get the items:
solution/
βββ store/
β βββ Landing.Lakehouse/
β βββ Base.Lakehouse/
β βββ Curated.Lakehouse/
β βββ .platform
β βββ lakehouse.metadata.json
β βββ shortcuts.metadata.json
βββ ingest/
β βββ FabricOps_API.Notebook/
β βββ FabricOps_SQL.DataPipeline/
βββ prepare/
β βββ FabricOps_functions.Notebook/
β βββ Engineering/
β β βββ FabricOps_enrich.Notebook/
β βββ Utils/
β βββ ManageWorkspaceIcons.Notebook/
βββ model/
β βββ FabricOps.SemanticModel/
β βββ definition.pbism
β βββ definition/ # TMDL
βββ present/
β βββ FabricOps.Report/
β βββ definition.pbir
β βββ definition/ # PBIR
β βββ StaticResources/
βββ orchestrate/
β βββ Load FabricOps.DataPipeline/
βββ core/
βββ Metadata.SQLDatabase/
A few things worth pointing out because they're the details people trip over:
Every item is a folder named
<DisplayName>.<ItemType>. That's Fabric's convention, not mine, and it's what makes a diff readable.Engineering/andUtils/underprepare/aren't layers β they're workspace folders, mirrored into the repo. Layers are the coarse structure so folders inside a layer are yours to organise however your team thinks.Every item carries a
.platformfile, which holds its type, display name, description and alogicalIdthat ties the item in the workspace to its representation in the branch:{ "metadata": { "type": "Lakehouse", "displayName": "Curated" }, "config": { "version": "2.0", "logicalId": "edcd54d9-β¦" } }Rename an item by changing
displayNamein that file and not by renaming the folder. And never touch thelogicalId. It's the thread connecting workspace and repo and cutting it produces exactly the kind of afternoon you were trying to avoid.The lakehouse folders contain no data. Only metadata and shortcut definitions. Say this out loud to your stakeholders early. "It's all in Git" and "We can restore the data from Git" are very different statements.
Mono-repo or multi-repo?
Start with a mono-repo. One repository, all layers, just as above.
The first reason is coordination. A change in Prepare that feeds a change in Model that surfaces in a Present report is one change to your platform, even though it touches three layers. In a mono-repo that's one branch and one PR, reviewed as a unit. Split across three repos, it's three PRs, in an order somebody has to remember, with no atomic way to roll back the middle one.
The second is documentation. Most teams keep their docs in Git too and docs belong next to the thing they describe: the architecture, the naming conventions, the runbook for whatever breaks at 2am. In a mono-repo that's one documentation folder and one place to look. Split the platform across five repos and you get five partial documentation sets, or a sixth repo that holds all the docs and starts drifting from every one of them the day it's created. Neither is a good outcome and the second is the one teams actually end up with.
You'd split when the people split, not when the folders get big:
Separate teams with genuinely separate release cadences (the Enterprise+ domain model from Episode 2 is the usual trigger)
Hard access boundaries where one team must not read another's source;
A repo that has grown so large that clone and CI times hurt. It's real, but rarer than people expect for item definitions, which are mostly small text files.
Even then, my preference is to split by domain (all of Finance's layers together) rather than by layer (everybody's Model workspaces in one repo). Splitting by layer maximises cross-repo chatter which is the thing you were trying to avoid.
Tip: this is a decision you can defer cheaply, and reverse expensively. Mono-repo until it hurts - and be specific about what "hurts" means when you claim it does.
The mapping, made concrete
Put it all together and every row of this table is a fact you can check, not a convention you have to remember:
| Layer (Episode 2) | Repo folder | Workspace (dev) |
|---|---|---|
| Core | solution/core |
Confidence - Core [dev] |
| Ingest | solution/ingest |
Confidence - Ingest [dev] |
| Prepare | solution/prepare |
Confidence - Prepare [dev] |
| Store | solution/store |
Confidence - Store [dev] |
| Model | solution/model |
Confidence - Model [dev] |
| Present | solution/present |
Confidence - Present [dev] |
| Orchestrate | solution/orchestrate |
Confidence - Orchestrate [dev] |
And now the environment axis from Episode 2 quietly pays for itself. Note what isn't in the repo: there's no store-dev sitting beside a store-test. One layer, one folder. Seven folders, twenty-one workspaces. What differs between environments isn't the folder, it's how the content arrives, and that is easier to draw than to describe.
That topology is a choice, not a rule, so let me explain my approach. I keep dev Git-connected because it mainly buys you two things nothing else does. Every merge to main gets proven against Fabric's own Git engine before it goes anywhere near a release, and the source control pane becomes a drift report anyone can read. No pipeline history to dig through to find out whether the workspace matches the branch.
I keep test and prod out of Git for two reasons. The first is that promotion should be a decision someone makes, not a side effect of a merge landing. The second is more mechanical and it's the one that settles the argument: item definitions carry environment-specific references. A semantic model points at a particular SQL endpoint, a notebook at a particular lakehouse, a pipeline at a particular connection. Sync prod straight from a branch and you've handed prod the dev GUIDs. Something has to rewrite those references in flight, which makes promotion a deployment concern rather than a sync concern. That's the job of fabric-cicd, Microsoft's own Python library for exactly this problem. It mutates the item definitions on the way into the target workspace, resolving references at deploy time so test gets test's GUIDs and prod gets prod's. Variable Libraries cover some of this ground and Microsoft recommends them where they fit. They're scoped to a single workspace though, which is awkward when your architecture is deliberately seven of them. Also semantic models aren't among the item types that can consume one. I'll walk through both in a later episode.
And Fabric happily supports the alternatives. You can give every stage its own branch and sync each workspace from it, which makes Git the literal source of truth per environment. You can go the other way and connect nothing but feature workspaces, deploying to dev, test and prod through the same pipeline. This is kind of what what Microsoft's own internal data engineering team does, and it has the tidy property that there's only one path into a workspace. Both are defensible. Mine is the middle and I'll make the full argument in a later episode, where the branching workflow belongs.
The mechanics of all of it: branching workflow and promotion are coming. The structure that makes them possible is what we're laying down here.
Conventions worth agreeing on before the first commit
None of these are clever. All of them are cheaper now than later.
Naming. Item display names become folder names, so the naming discipline you agreed in Episode 2 shows up in your directory listing whether you meant it to or not. Keep environment out of item names. The workspace already carries it. Curated, not Curated_dev_v2. Better still, don't rely on discipline for the workspace names at all. In FabricOps the name is a template, Confidence - {layer} [{environment}], substituted at provision time. Nobody can typo a workspace name into existence, because nobody types workspace names.
What lives outside solution/. Everything that operates the platform rather than being it: .github/ and .azure-pipelines/ for workflows, automation/ for scripts and environment definitions, and documentation/ for the things a new colleague needs: the architecture, the naming conventions and the reference for whatever configuration your automation reads. This separation matters more than it looks: solution/ is the only part Fabric syncs, so anything else you keep in the repo is yours to organise freely. It travels with the platform it deploys, reviewed in the same pull request. Documentation that lives next to the solution it documents actually has a chance of staying true.
What not to commit. Secrets, obviously, so NO PATs, no client secrets, no connection strings with a password in them and no credentials.json that "was only for testing". Also: local environment folders, editor junk (.DS_Store is a personal grudge) and anything a run produces rather than a human writes. Put the rules in .gitignore on day one and add a scan to CI when you get the chance. The interesting thing about committed secrets is that removing the file doesn't remove the history.
But wait! How do I get all this created and wired up?
This is exactly the right question and exactly where this episode stops.
Look at what we've just specified: seven workspaces per environment, each with the right capacity, the right permissions and the right items inside. Three environments, so twenty-one workspaces. The seven in dev each need a Git connection on top of that, pointing at its own folder in the right branch of the right repo, with the right git credential. And then you do it all again, identically, when someone adds a layer or a new environment or a fourth domain.
You can absolutely do that in the portal. I've done it. It takes an afternoon and it works. But it is quietly turning wrong because the one thing hand-wiring cannot give you is the guarantee that dev, test and prod are actually the same shape.
So in Episode 4 we stop clicking. We'll look at the options for setting this up as infrastructure-as-code covering the Fabric CLI, REST APIs, fabric-cicd and more. And where each of the tools and services fits in. Then we will go deep on how FabricOps does it all: a declarative definition of your platform that provisions the workspaces, creates the items, wires the git integration and can tear the whole thing down again. Including the bit nobody shows you: what happens when you run it twice.
Stay tuned!



