Tech Series: Provisioning a customer site

Part 7 of the inveazy tech series. How the template becomes a dedicated install after paid checkout — provision job, out-of-process worker, six deploy artifacts, and Subscribers — without cloud admin secrets in the web app.

Part 7 of the inveazy tech series. Parts 1–6 built the pieces: database project, dacpac lockdown, web and API, front-end habits, workspace isolation, and DevOps environments. This post is where those pieces meet the commercial trigger — how a paid buyer becomes a live template install at https://{slug}.inveazy.com. For the environment handoff that leads here, read Part 6: DevOps and environments; to start the series from the map, see How we built inveazy.

Part 6 ended on a promise: the same product shape travels laptop → lab → Azure, and provisioning is where config layers and the dacpac meet the paywall. The product-facing map of that story is the Hivoltech / inveazy capabilities brief (PDF) — template, modules out of the gate, one-button provision, Subscribers, Azure today and private-network next. This chapter is the engineering review behind that brief: the job, the worker boundary, and the seed posture that separates day zero from day two.

Series:DevOps and environments · Earlier: Dacpac publish and data lockdown · Start: How we built inveazy


Golden rule: Money first — and stay there. Then one idempotent job. Then an out-of-process worker. The site URL is the customer’s home from day one — not a temporary staging host you rename later.

Roadmap (full journey)

  Checkout (paid) → provision job → listener → SQL + site → https://{slug}.inveazy.com
                         ↓
              first org / live workspace (tenant step inside the DB)

  Secrets boundary: web app posts intent; listener holds cloud/SQL admin

Vocabulary: Slug = the site hostname label (the {slug} in https://{slug}.inveazy.com). Provision job = the durable row that tracks install status for that slug. Listener / worker = out-of-process host that holds cloud/SQL admin capability the web app must not. Webhook = Stripe’s event delivery to your platform — it can fire more than once for the same payment, which is why the job must be idempotent on slug. Install = one dedicated database + site runtime + hostname. Lockdown = publish without re-seeding disposable sample story data (Part 2).

Think of the whole path this way: money first, then one idempotent job, then an out-of-process worker — not because microservices are trendy, but because secrets do not belong in the web app.

Why provision sits on a template

The capabilities brief opens with a commercial thesis: every new SaaS idea tends to rebuild the same expensive layer — users and customers, roles, multi-workspace tenancy, subscriptions, payments, admin visibility, hosting, dedicated databases, domains, and TLS — before the product idea gets interesting. inveazy is the reusable application template that already carries that SaaS core plus a deep module set (CRM, projects, catalog and supply chain, three sell channels, finance, assets, public site, workspace assistant). You customize for a vertical. You incorporate the next module into the same application. You deploy with one-button provisioning. You do not recreate tenancy and billing for every concept.

Automation is the unlock. Without a path from paywall to dedicated install, the template is only a codebase. With it, the same tested product shape — schema from the dacpac family in Parts 12, app from Parts 34, workspace wall from Part 5, environments from Part 6 — can be redistributed without standing up Azure by hand for each buyer.

Buyer path: one-button provision

“One button” means the buyer does not open an Azure subscription, wire containers, invent DNS, or mint certificates for each new install. On the managed Azure product line, the customer completes Stripe Checkout on Features & pricing with organization name, site slug, and workspace intent. Checkout success drives provisioning automatically. When the job finishes, welcome setup opens on a private site — typically https://{slug}.inveazy.com — and first-organization setup continues inside the application.

Authorized operations staff can also trigger or retry provision from Admin / Subscribers for smoke tests and recovery. That is an ops path, not a requirement for ordinary paid signup. Provisioning runs in the cloud so new installs do not depend on a home-office machine being online.

Checkout is still not “create database.” Completing a Stripe session is a commercial event. Stripe webhooks can retry, so we must never create infrastructure before payment is genuinely settled — typically a paid status, or the rare no_payment_required path the product treats as settled. Treating “session completed” as “spin Azure” builds databases for abandoned carts and lets webhook retries invent a second site for the same slug.

// Gate: commercial settlement before infrastructure
// Checkout "completed" ≠ provision
// Prevents databases for abandoned carts — a critical safeguard
if (paymentStatus is paid or no_payment_required)
  enqueue provision job for slug
else
  wait — do not create SQL or DNS

Buyers feel a dedicated site. Engineers feel a status machine that refused to start until the money event was honest.

The job row and the Subscribers hub

After payment succeeds, the commerce layer records a durable provision job for that customer slug, updates the Subscribers catalog, and notifies a dedicated cloud provision service over an authenticated channel. A provision job is the durable fact of “we are installing this slug.” Ops needs a grid they can read without SSH: pending, provisioning, provisioned, or failed.

Idempotency is not a slogan here. The job is keyed so the same slug does not quietly become two Azure sites when Stripe redelivers a webhook or an operator clicks twice. If a job for that slug is already provisioning or provisioned, a second enqueue skips or no-ops — once a job is live, a retry for the same slug either resumes or ignores the new request; it never forks a second install. If it failed, retry is a deliberate action with a status trail — not a second blind create.

// Gate: one job per slug (Stripe redelivery safe)
// status in (provisioning, provisioned) ⇒ resume or ignore — never fork
if job exists for slug and status in (provisioning, provisioned)
  skip — do not start a second install
else
  create or resume job → pending → provisioning → provisioned | failed

Admin / Subscribers (and related billing surfaces) keep product subscriptions and provision jobs visible so operators can see which slugs are paid, which installs are in flight, and which jobs need retry or recovery. The capabilities brief puts it cleanly: without that hub, one-button provision is a demo; with it, provision is an operating practice — paywall → job → install → account record → welcome.

Out-of-process listener: the web app must not hold cloud admin

This is where the security model lives. The signed-in marketing and hub process already holds customer session cookies, Stripe platform keys for the paywall, and connection strings for the marketing database. It must not also hold the Azure subscription credentials that create SQL databases, the SQL admin password that runs sqlpackage against a brand-new catalog, or a path to docker.sock on a host that builds containers. If those secrets lived in the web process, a stolen session cookie or a compromised container would become a cloud-admin incident — not just a site outage.

inveazy draws the same line the brief describes: a Python provision listener orchestrates the run — validate the request, drive automation for that slug, and report status. We name the role, not a public endpoint or repo path. It does not render the customer website. It invokes provisioning automation that talks to the cloud control plane. The web app POSTs a provision intent to that allowlisted, authenticated host and learns progress by reading the job status — not by embedding the script. That is the same spirit as Part 3’s split: Razor and /api for people and contracts; privileged work stays where secrets are meant to live.

App (paywall / Subscribers)
  POST provision intent for slug
       ↓
Listener / worker (out-of-process)  ← cloud/SQL admin lives here
  runs sqlpackage + site deploy + DNS
       ↓
Job status → provisioned
  customer opens https://{slug}.inveazy.com

This post describes architecture for evaluation. It does not publish credentials, API tokens, listener endpoints, or copy-paste runbooks — the same honesty line as the capabilities brief.

What lands: six artifacts

The worker’s job is boring on purpose. The brief lists six artifacts that matter for every dedicated install. Conceptually, the script creates or targets them in order; resume after a mid-fail continues from the last honest checkpoint rather than inventing a parallel slug.

# Artifact Why it matters
1 Dedicated SQL database Customer data isolated per install
2 Schema publish (dacpac) Same tested product shape every time — no hand-built schema drift
3 Web application / site runtime Dedicated managed container app bound to that database
4 Cloudflare domain Customer hostname DNS for the install
5 TLS certificates HTTPS on that hostname without a side project
6 Admin / Subscribers records Subscription + provision job visibility for account management

All six land together as one install. Image and dacpac remain parallel build artifacts from Part 6. Provision assumes both are already buildable. Day zero applies them to a new catalog. Day two republishes schema under lockdown without rewriting the customer’s story.

The customer’s home is https://{slug}.inveazy.com from the moment the install is meant to exist. That is not a temporary hostname you rename after onboarding. Slug uniqueness is a product constraint on checkout; changing a live hostname later is an ops event, not a casual PATCH.

First login vs fleet republish

A brand-new customer database has no invoices to protect. A first install may run with sample seed on so SandBox is not an empty shell while someone completes first-organization setup. That is day zero: the catalog is disposable until a real company lives there.

SeedSandboxSampleData = 1   // first customer install (day 0) — SandBox story OK
SeedSandboxSampleData = 0   // day-2 / fleet republish — lockdown; lookups only

Day two is different. Schema still lands through the same dacpac path. Sample CRM narratives, fake warehouses, and training blog posts must not MERGE back over a week of real work. Lockdown sets sample seed off. Lookups may still MERGE. Stories do not. Soft-deleting users across workspaces to “fix” a list screen is never a publish step — that is the data lockdown habit from Part 2, enforced because provision and republish share one package family.

Azure provision creates the install: site + database + hostname. The live company and its first real workspace are a tenant step inside that database — first-organization setup after login — not a second Azure provision and not a second excuse to re-seed sample companies into the books. Subscribers see the provisioned site as a commercial fact. Inside the hub, the customer finishes organization profile, invites users, and turns on the modules they will actually run.

Azure today · private-network next

Scalability is built into the install model. On the managed Azure path available today, each customer gets a dedicated SQL database and a dedicated web / Container App runtime, Cloudflare hostname and TLS on the product domain pattern, workspaces for more locations inside an install, and the same one-button paywall provision. Grow database and site resources as load grows; provision additional dedicated installs when a new brand or entity needs its own footprint.

Private-network / Self-hosted is the next host target on the roadmap in the capabilities brief — same template, same schema baselines, same incorporate-module → click-deploy philosophy, when a customer’s hosting policy requires it. Specialist lab and Docker-style stacks already support private-style testing and promotion. Contact Hivoltech to scope those programs ahead of general Self-hosted list pricing. Soft packaging for the managed Azure line remains on Features & pricing; the constant is dedicated installs, workspaces, and automated provision when the next footprint is needed.

New modules compound the same way: engineer them into the existing application beside CRM, SCM, ecommerce, Stripe, accounting, assets, and AI, then ride the same provision path out to the next dedicated install. That is the customization ladder in the brief — stock template, industry fit, custom module work, redeploy — not a second greenfield SaaS stack.

What this contract keeps in place

Pay before infrastructure. One job per slug with status you can read in Subscribers. Web app posts; worker builds. Six artifacts land as one install. Public URL from day one. Day-zero seed may teach; day-two lockdown must not overwrite. First organization is a tenant step inside an already published database. Azure hosts the managed path today; private-network follows the same delivery idea when policy requires it.

That is the unlock of the whole series in one path: the database project defines shape, the dacpac carries it, the web and API host the product, isolation keeps workspaces honest, DevOps keeps environments from casualizing data, and provisioning is the commercial gate that refuses secret sprawl and double-create. Optional later chapters (files, integrations spine) can deepen a corner; they do not replace this climax.


Questions we get

Is checkout enough to create my site?

No. Settlement is. The product waits for a paid (or explicitly no-payment-required) signal before the provision job may start. That protects you from half-paid ghosts and protects us from building Azure for abandoned sessions.

What if Stripe sends the webhook twice?

The job is unique on the site slug. A second delivery that finds provisioning or provisioned already in progress does not invent a second database. Status is the lock; the worker is not a second opinion.

Why not let the web app run sqlpackage itself?

Because the process that serves the public blog and signed-in hubs should not hold Azure/SQL admin capability. An out-of-process listener keeps privileged credentials off the request path. The app stays a client of the job; the worker stays the place cloud admin belongs.

Will a later schema update wipe my CRM?

Not under lockdown. Day-two republish applies schema with sample seed off. Lookups can refresh. Sample story MERGEs do not. Soft-delete “cleanup” across tenants is not a publish feature. See Part 2.

Where do operators learn the product after the site is up?

This series was for architecture. Day-to-day walkthroughs live in the separate how-to series and in Help on the install. Provisioning ends when the URL answers; onboarding begins when a human signs in.


Bottom line

A customer site is born when money is settled, a single job claims the slug, and an out-of-process worker applies the dacpac and stands up the site at https://{slug}.inveazy.com — with Subscribers as the operating record of that path. First login may meet SandBox; fleet republish must not. The live company is created inside the database after the install exists — not by a second cloud provision.

The contract: Paid → idempotent job → isolated site. Secrets stay with the worker. Sample seed is a day-zero kindness, not a day-two habit.

Series complete — what’s next for readers

You have the map from schema to paywall. If you run the product day to day, switch to the how-to series for subscribe, admin, CRM, inventory, and the rest of the hub. If you evaluate architecture, keep the capabilities brief (PDF) beside these posts. Features & pricing and a free demo remain the product doors.

Series:DevOps and environments · Start over: How we built inveazy