Tech Series: How we built inveazy

How we built inveazy: a SQL Server dacpac as the schema contract, ASP.NET Core hubs and APIs on .NET 10, organization and workspace tenancy, Docker and Azure hosting, and paid provisioning to a customer site.

Part 0 of the inveazy tech series. This post is the map of how we built the platform under the product — the database contract, the web application, tenancy, where it runs, and how a customer site comes into existence.

Series: Next → The database project as source of truth


Why this map exists

inveazy is business software for teams that sell, stock, ship, and serve customers. CRM, inventory and warehouse, accounting, storefront, blog, files, and the rest share one signed-in hub and, when you want it, one public face for the company. The question underneath every screen is the same: what is the source of truth, and what is allowed to change it?

If that question stays implicit, each environment invents a slightly different answer. A laptop database migrates on first request. A shared LAN catalog picks up last week’s demo customers. Azure SQL rejects a type the local engine accepted. A paid site is “the demo folder, but live.” This series is the written split: schema is compiled and published; the web app consumes it; a customer install is a real site after checkout is paid.

Architecture at a glance: schema (SSDT dacpac) → web app (ASP.NET Core) → Azure SQL and containers. Paid checkout provisions a real site at https://{slug}.inveazy.com.

The database project owns the contract. The web app honors it. A customer install is a real site, not a folder on a shared demo.

inveazy database project → dacpac → publish → SQL
inveazy web app → Razor hubs + /api → SQL
Paid checkout → provision → https://{slug}.inveazy.com

In this post

  • Schema first — why the database project is the source of truth
  • The application shape — Razor pages, APIs, and services
  • Organization and workspace — multi-tenant isolation
  • Where it runs — demo, Docker, and Azure
  • How a customer site is born — from checkout to live site

Schema first

The database is not whatever an object-relational mapper invented during last week’s feature branch. Canonical DDL lives in the inveazy database project, an SSDT project portable to both Azure SQL and on-prem SQL Server. Schema ships as a dacpac — a Data-tier Application Package: the compiled catalog plus the scripts that ride with a publish. Tables are grouped the way the product is grouped: core tenancy and security, site and hub, CRM, inventory and warehouse, purchasing and sales, accounting, blog, and the neighboring modules. Most of that sits in the default schema. Reporting helpers and tenant isolation policy sit in a dedicated reporting area so support tooling can read without becoming a second write path.

Almost every business table carries a TenantId that points at the workspace, and an IsDeleted flag for soft delete. Inventory documents, blog posts, and security rows follow the same habit. We would rather retire a row in place than hard-delete it because a screen looked wrong or a republish needed a clean slate. Soft delete is not nostalgia. It is how you keep an audit trail when the pressure is to “just fix the data.”

The web application is a consumer of that published database. It does not run a second migration story that can drift away from the dacpac. When a feature needs a column, the column lands in the database project first. Services and pages follow. That order is slower the first time you feel it, and cheaper every time Azure SQL and a laptop Docker SQL stay on the same logical model. Part 1 is the contract. Part 2 is how that contract is applied to a live catalog.

The application shape

The inveazy web application is one host with deliberate surfaces. Razor Pages render the signed-in hubs and the public marketing, blog, and storefront routes. Controllers under /api return JSON for the JavaScript that powers compose screens, grids, the till, file manager, and billing webhooks. Business rules live in injected services so pages and controllers stay thin enough to reason about.

Staff sign in with cookies. The B2B storefront uses a separate customer cookie scheme so a clerk session and a buyer session do not collapse into one jar. Middleware decides which modules are enabled, which roles may call which APIs, how the store resolves its commerce tenant, and when a platform-billed site should soft-lock. Public pages stay public. Tenant operations stay behind authentication and module gates.

By design: when an API call fails, the response stays JSON so the hub can show an error in place. A full-page failure may render an HTML error page. Operators keep context to fix what went wrong, instead of a save swapping the composer for a generic “Error / Request ID” document. Part 3 of this series is that split in full: Razor for people, JSON under /api for hub scripts.

Public surfaces and routes

One process, four surfaces. The host decides which one you are on from the path — not from a second app.

/apps/…   signed-in Razor hubs
/api/…    JSON for scripts and webhooks
/blog/…   public content
/store/…  storefront when enabled

Organization and workspace

In inveazy: an organization is the company; a workspace is where the team operates day to day.

The organization owns branding, billing posture, and the public face. Training often starts in SandBox. Real operations move to a live workspace. In the database that workspace is the tenant row. Application code threads TenantId through queries and commands so a document cannot quietly belong to “wherever the connection landed.”

Multi-tenant does not mean one shared soup of rows with a filter you hope everyone remembered. It means every operational document knows its workspace, soft delete is the normal way to retire a mistake, and shared or customer catalogs are not re-seeded when schema republishes. Evaluation belongs on the demo host with sample data. Company work belongs on the company’s own site after provision. Part 5 returns to that line in daily work.

Where it runs

Three surfaces get confused often enough that they deserve full sentences — and a side-by-side so the overlap is visible.

Demo host Docker / LAN Paid Azure site
Purpose Evaluate with sample data Build, LAN, or self-host Customer system of record
Database Shared demo catalog SQL Server from dacpac Isolated Azure SQL
App Marketing / eval host Web container Container Apps + DNS
URL demo / public eval local or LAN https://{slug}.inveazy.com

The demo host is useful, and it is not your production system of record. Local and LAN engineering runs the same stack in Docker: SQL Server, a one-shot schema apply from the dacpac, and the web container, with optional sidecars depending on the compose file. Disposable volumes may carry sample CRM and inventory stories. Shared LAN databases publish under lockdown so those stories survive a schema update.

inveazy Azure is the managed customer path. The same dacpac publishes into Azure SQL. The web app runs on Container Apps. DNS points a slug hostname at that install so the company opens https://{slug}.inveazy.com. The web process does not hold cloud-admin credentials or a Docker socket. Provisioning is queued inside the app and executed by an out-of-process listener on the ops host. That boundary exists so a compromised site process is not automatically a compromised cloud control plane.

Self-host is the same product idea with different ownership of keys. You run SQL and containers where you choose. The architecture stories in this series still apply. Part 6 is environments and how app release and dacpac release stay in step.

How a customer site is born

When a customer completes checkout and payment is confirmed (or explicitly waived), we queue a provision job keyed to their chosen subdomain slug. Completing the Stripe session is not enough by itself. The job is idempotent on purpose. Webhook redelivery should not create a second database for the same company.

Checkout → payment confirmed → provision job
    → create Azure SQL → publish dacpac
    → deploy container → attach DNS
    → https://{slug}.inveazy.com

The listener creates the database, publishes the dacpac, deploys the Container App, and attaches DNS. A first install may seed SandBox so someone can complete first-organization setup without staring at an empty shell. Day-two fleet republish turns sample seed off and keeps lockdown on, so customer story data is not merged back over real work. Inside the new site, creating the live workspace is a tenant step in an already published database. It is not a second Azure provision. Part 7 of this series is the full review of that path and the design choices around it.

Key takeaway: inveazy is built on a hard split between schema (SSDT dacpac, source of truth) and behavior (ASP.NET Core web app). Every customer gets a real, isolated Azure SQL database and a containerized site — not a shared folder. Schema comes first; features follow.

What the rest of the series covers

This post is the map. The next seven parts zoom into each layer and the decisions that shaped it.

Part 1 stays on the database project: how the inveazy schema project is organized, why the dacpac is the publish unit, and why features are supposed to land in schema before they land in screens.

Part 2 covers dacpac publish and data lockdown: the publish scripts, pre-deploy and post-deploy behavior, when sample seed is allowed, and what a republish must never do to a shared or customer catalog.

Part 3 walks the web app and API: Razor versus JSON routes, services, cookies and roles, module middleware, and keeping API failures in JSON.

Part 4 is the front end we actually ship: progressive hub JavaScript against the API, the CSS and icon build pipeline, and rich text only where compose surfaces need it.

Part 5 returns to multi-tenant design in practice: organization versus workspace, TenantId and soft delete in daily work, and the line between public content and hub data.

Part 6 is DevOps and environments: Compose files, Azure versus LAN, configuration layers, and keeping app release and dacpac release in step.

Part 7 is the provisioning review: paywall, job table, listener boundary, Azure SQL and Container Apps and DNS, and the difference between first-install seed and day-two lockdown.

Questions we get about this map

Does the web app migrate the database when it starts?

No. Schema lives in the database project and ships as a dacpac. The web application consumes a published catalog. Part 1 is why. Part 2 is who is allowed to apply the package.

Is the demo host the same as a paid customer site?

No. The demo host is for evaluation with sample data. A paid site is an isolated Azure SQL database, a containerized app, and DNS at https://{slug}.inveazy.com after checkout is actually paid.

Can I run the same product on a LAN or on my own host?

Yes. Local and LAN engineering use Docker SQL and the same dacpac. Self-host is the same architecture with different ownership of keys. Lockdown still applies once a shared catalog has real work in it.

The database project owns the contract. The web app honors it. A customer install is a real site, not a folder on a shared demo.


What’s next

Next we start at the contract everything else depends on: the database project as source of truth. That is where schema ownership lives, how a dacpac becomes the unit you publish, and how the rest of the inveazy stack is meant to follow that contract instead of inventing a second one.