Plans and tenant limits
EIDGuard is sold on one of three plans. Each plan includes a number of protected External ID tenants.
Two marketplace items, one product. EIDGuard deploys as an Azure Marketplace solution template into a resource group you own, and is billed through the EIDGuard SaaS subscription sold alongside it — a solution template offer cannot transact. Your plan lives on that subscription; the deployment learns it from the licence the subscription issues (see "Where the limit comes from"). Nothing about your plan is decided inside the deployment.
This page describes what that number counts, what happens when you reach it, and — the part worth reading first — what does not happen.
The guarantee#
A plan limit only ever blocks onboarding a new tenant. It never stops backups.
Tenants that are already onboarded keep running on their schedule, keep their recovery points, and stay fully restorable, comparable and exportable — including when a plan change leaves you over the limit. Nothing is deleted, no snapshot is withheld, and no feature switches off. A deployment sitting at "5 of 3" after a downgrade backs up all five tenants exactly as before; the dashboard says so, and offboarding stays available if you want to come back under the cap.
The three plans#
| Plan | Price | Protected tenants |
|---|---|---|
| Starter | $199 / month | up to 3 |
| Standard | $299 / month | up to 5 |
| Enterprise | private offer — sales@vambris.com | unlimited |
The fee is flat and per deployment: one EIDGuard subscription, one monthly charge, billed by Azure on your existing subscription invoice. There is no metering — no per-user, per-object or per-backup component — so the bill does not move when a tenant grows or a schedule runs more often. Azure infrastructure in the resource group (App Service plan, storage, Key Vault, Automation minutes) is charged separately at normal Azure rates.
Enterprise is a private plan: pricing and the audience are set per deal, and the
tenant cap is 0, which the product reads as unlimited.
What counts as a tenant#
One protected External ID tenant, as it appears in the deployment's tenant list. A tenant enters that list when you complete the onboarding wizard for it in the dashboard, and leaves it when you offboard.
Nothing else counts. Users, groups, applications, snapshots, restores, runbook
jobs and dashboard sign-ins are all unmetered and uncapped on every plan. The
dashboard shows the current position under Configuration → Plan — 3 of 5 tenants protected, or 4 tenants protected · unlimited on Enterprise.
Where the limit comes from#
The plan you bought (your EIDGuard SaaS subscription: Starter · Standard · Enterprise)
│
└─► the licensing service issues your deployment a signed licence
naming the plan and its tenant limit — at setup, then refreshed
on every six-hourly check-in
│
├─► read when you onboard a tenant ──► allowed, or refused (403)
│
└─► reported to the dashboard ──────► "3 of 5 tenants protected"
The licence is the source of truth. It is issued to the deployment when the
Setup Wizard registers it and re-fetched every six hours, so a plan change on
the subscription takes effect within hours with nothing redeployed. Two
settings on the deployment's Function App — EIDB_PLAN_NAME and
EIDB_TENANT_LIMIT, stamped into the package at build time — are the
fallback for a deployment that has never reached the licensing service (a
fresh deployment, or one whose outbound HTTPS is blocked): they describe the
plan the package was deployed from and can never grant more than was bought.
So the place to confirm what a deployment currently enforces is the Configuration → Plan domain in the dashboard, which shows the plan and limit in force. The two Function App settings (Settings → Environment variables) show only the plan the package was deployed from; after an upgrade or downgrade on the subscription they stay as they were while the licence carries the new limit, so they are not the value to diagnose a mismatch from.
What happens at the limit#
Onboarding is the only thing the plan limit gates. The check runs on the final
commit of the onboarding wizard (POST /api/tenants/commit), inside the same
lock the commit itself takes — so two administrators racing for the last slot
cannot both succeed.
Onboarding can also be refused for a reason that is not your plan. A deployment configured with a licensing endpoint it cannot reach pauses new onboarding after 30 days without contact, and stops scheduled backups after 60. The refusal says so and looks nothing like the plan message below; the cause is almost always blocked outbound HTTPS rather than anything about your subscription. See outbound connectivity §7. Deployments with no licensing endpoint configured are unaffected, and restore, compare and export are never gated by it at all.
POST /api/tenants/commit (Admin role · inside the estate-wide action lock)
│
├─ limit is 0, blank, missing or unreadable? ────► commit (fails OPEN)
│
├─ tenant already in the tenant list? ───────────► commit (re-onboarding is an upsert)
│
├─ tenants currently protected < limit? ─────────► commit
│
└─ otherwise ────────────────────────────────────► 403 tenant_limit_reached
"Your <plan> plan protects up to N
tenants. Offboard a tenant, or
upgrade your plan in the Azure
portal, to onboard another."
Two of those branches are deliberate and worth naming (Assert-TenantQuota,
web/api/Modules/WebApi/WebApi.psm1):
Already onboarded → always allowed. Re-running the onboarding wizard for a tenant you already protect is an upsert: it replaces that tenant's entry and the count does not change, so it is never refused — not at the limit, and not over it. This matters because re-onboarding is the repair path. If a tenant's app registration was deleted in the External ID tenant, or its certificate needs re-issuing, you fix it by running onboarding again. A cap that blocked that would turn a plan limit into an outage.
The test is whether the tenant is in the list, not whether you have tried before. An onboarding that never reached its final step never added the tenant, so at the cap that retry is treated as a new tenant and refused. If the tenant does not appear on your dashboard, free a slot or change plan first. (A run refused at the cap cleans up after itself — it deletes the app registrations it created and revokes the roles it granted, and tells you about anything it could not remove.)
An unreadable limit fails open. If the app setting is missing, empty, negative or garbled, the deployment treats itself as unlimited rather than refusing everything. A mangled setting must never brick onboarding, and it must never take the dashboard down either — the parse is written so that it cannot throw.
The dashboard mirrors all of this before you start: the onboarding form disables the submit button at the limit and explains why, but keeps it enabled when the tenant you typed is one you already protect.
Everything else is untouched at the limit. Scheduled and on-demand backups, restores, differentials, certificate rotation, recovery-point export, alerting and offboarding all behave identically whether you are at 1 of 5 or 6 of 3.
A run refused for the licensing reason above cleans up after itself in exactly the same way. Both refusals are decided before the server persists the tenant, so no tenant record is saved — but by that point the browser has already created app registrations and issued certificates in your External ID tenant. Cleanup is therefore a reversal, not an absence, and it is best-effort by design.
The dashboard undoes what it can, on the spot, and then tells you exactly what it could not:
- App registrations it created are deleted. A delete that fails is reported by name rather than retried silently.
- Apps that already existed are never deleted. Those are your objects — onboarding adopted them, so they are left in place and reported for review, with any Graph roles this run granted revoked.
- Key Vault certificates always remain. They are issued server-side and cannot be revoked from the browser. Each is listed by exact name: inert if its app was deleted, or flagged as an active credential needing prompt revocation if its app was retained.
Nothing is summarised away — an empty list genuinely means nothing was left behind.
Changing plan#
Change the plan on your EIDGuard SaaS subscription in the Azure portal. That is the whole procedure. Your deployment asks the licensing service how it stands every six hours and takes back a fresh licence, so a new plan applies within six hours — usually much sooner — and nothing in your subscription is touched.
No redeploy. No downtime. No settings to re-apply, nothing to note down first, and no tenant to re-onboard.
This is a change from earlier versions, which stamped the tenant limit into the deployed application at package time. Back then the only way to move it was to redeploy the template, and redeploying carried the caveats below. If you are following older instructions that tell you to deploy a different plan into the same resource group: don't. It will not change your limit any more, and a mistyped resource name prefix builds a second parallel estate you then pay for twice.
Bought through a partner (CSP)? You cannot change the subscription yourself — Microsoft makes it read-only to you, and the portal will refuse the change without explaining why. Ask your partner to move you to a larger plan. The dashboard detects this and says so rather than sending you to a portal that will turn you away.
For Enterprise, which is unlimited and priced privately, contact sales@vambris.com.
Upgrading to a newer EIDGuard version — which settings survive#
A plan change no longer redeploys anything, so nothing here applies to it.
A version upgrade does redeploy the template. Before it changes anything,
the upgrade reads the settings you have set in the dashboard back from your
deployment (the read-dashboard-settings step) and re-deploys those, so the
values below are kept exactly as you left them:
| Dashboard setting | After a version upgrade |
|---|---|
| Backup frequency and hour (RPO) | Kept — your schedule is read back and re-deployed |
| Time zone | Kept — each schedule's name carries its zone, so it is re-deployed in that zone, under the same name, at the same local hour |
| Snapshot retention days | Kept — your retention is read back and re-deployed |
| Alert email address | Kept — the action group keeps your address |
| Alerts you switched off | Kept off — the three operational rules stay disabled |
| Certificate-expiry warning days | Kept — the schedule carries your value |
| Scheduled verification you switched off | Kept off — the schedule is not re-created |
| A tenant's own RPO | Kept — overrides stay in the tenant list, and the schedule is re-deployed at the cadence it already ran at |
| A tenant's own retention | Kept — the read-back compiles every tenant's window into the lifecycle policy it re-deploys |
| A tenant's own alert recipient | Kept — its alert rule and action group are not part of the template, so the upgrade leaves them as they are |
The deployment's original values (what you entered when you first deployed) matter only for a fresh deployment. Supplying different ones on an upgrade changes nothing; change them in the dashboard instead.
Your data is not touched either way: recovery points stay where they are (the template declares the storage account and its containers, never the blobs inside), Key Vault certificates are not re-issued or revoked, and your onboarded tenants survive.
One safety rule applies, in the upward direction only: a retention can never sit
below the soft-delete window (the dashboard refuses that combination), so if an
upgrade raises the soft-delete window above your retention, retention is raised
to match and the dashboard shows the new value. A tenant's own window below it
is applied at the soft-delete window instead — by the upgrade
(the readback script in marketplace/bicep/modules/settingsreadback.bicep) and
by every later retention save in the dashboard (Set-RetentionPolicy in
web/api/Modules/WebApi/WebApi.psm1),
until you raise or clear it; each of those saves, onboarding and offboarding
included, warns, naming the tenant. The Tenants page keeps showing the value
you saved — the raise never rewrites it; only an immutable window that rises
during a retention save records a corrected value, and that only ever
lengthens it. A tenant window that is below only the immutable window is
applied as saved and loses nothing, because Azure refuses every delete inside
the immutable window. Snapshots are never deleted sooner than you asked.
Nothing breaks and nothing is lost — backups keep running throughout, on your schedule, and there is nothing to note down or re-apply afterwards.
Private networking? The read-back step needs to reach your data storage account, and after lockdown it cannot. It stops the upgrade rather than guess — guessing would put the original schedule and retention back over yours. Re-enable public network access on the data storage account for the duration of the upgrade, then disable it again: see private-endpoints.md.
Scheduled verification is on by default, and there is exactly one daily verification schedule: the one your deployment created. Configuration → Protection reads and writes that schedule directly, so it shows verification as Enabled out of the box, switching it off stops it, and switching it back on restarts the same schedule — you will never see two compare jobs a day from it.
A version upgrade respects the toggle: if you switched verification off, the upgrade does not re-create the schedule, and it stays off until you switch it on again. (Under the hood the upgrade removes the disabled schedule and the Configuration → Protection creates a fresh one when you enable it — the same as a deployment that started with verification off.) A plan change does not redeploy, so it leaves the toggle alone either way.
The hour it runs at follows your backup schedule — one hour after the backup window — and is not adjustable from the dashboard. With a time zone set, it runs in that zone too, at the same local hour all year. There is nothing to note down or re-apply for it.
Deployed before August 2026 and switched the verification toggle on at some point? You may have had a second, redundant verification schedule (
Daily-ExternalID-Verify) and two compare jobs a day. Your next version upgrade removes it automatically — a plan change will not, because it no longer redeploys anything. Nothing to do either way, and no backup or report is affected.
An upgrade takes effect for onboarding as soon as this deployment picks up the new licence — within six hours, and usually sooner. A downgrade takes effect the same way, and only for new onboarding:
downgrade Standard ──► Starter
│
tenants 1-5 ══════════════════╪══════════════════════════► still backing up
│ (all five, unchanged)
│
onboarding tenant 6 ───────────┤────────────────────────────► 403 at the commit
│
re-onboarding tenant 3 ────────┼────────────────────────────► allowed (upsert)
│
offboarding tenant 5 ──────────┴────────────────────────────► allowed (5 → 4)
For unlimited tenants, Enterprise is a private offer — contact sales@vambris.com and the plan is made available to your subscription, after which it appears alongside the public plans in the portal.
Before you delete the deployment#
Unrelated to plans, but on the same billing page in most people's heads: deleting the resource group removes EIDGuard and everything it holds — including the recovery points. It is not how you cancel billing: that is a separate subscription (see the note at the top of this page), so cancelling one without the other leaves you either paying for nothing or running unlicensed.
Uninstalling is "delete the resource group", but three things about this product mean it is not only that. In order:
Export the recovery points you want to keep from Configuration → Export recovery points, which copies every snapshot to a storage account you own. See web-ui.md for the flow and permissions.md for the one role assignment it needs.
Offboard every tenant from Tenants → the tenant's card → offboard. That deletes the backup and restore app registrations EIDGuard created in each External ID tenant and takes the tenant off the backup schedule. Deleting the resource group cannot reach those applications — they live in your External ID tenants, not in Azure — and the restore application holds write permissions, so do not leave it behind. With no tenant onboarded, no new recovery point is written.
Wait out the immutable window, then purge the recovery points. Every recovery point is written into storage that Azure itself refuses to delete — for anyone, a subscription Owner included — until that point's immutable window has passed (the window is shown on Configuration → Protection as the immutable floor). An Admin can raise that window there but nobody can lower it, so every raise lengthens this wait — see web-ui.md. And Azure will not delete a storage account while its
backupscontainer still holds anything, expired or not: a resource-group delete started with recovery points still in it removes everything else, fails on the storage account withAccountProtectedFromDeletion("the container(s) backups have object level immutability enabled"), and leaves the group behind with the account in it. Nothing is damaged by that, but nothing is finished either. So once the newest recovery point is older than the window, empty the container yourself. Two details, both measured on a real teardown (2026-09-13):- the purge needs Storage Blob Data Owner on the account. Storage Blob
Data Contributor deletes the current blobs but is refused on their expired
previous versions with
OperationNotAllowedOnAutomaticSnapshot; - deleting a blob leaves its previous version behind, so the purge is two passes: current blobs, then every remaining version, until the listing is empty.
acct=<data storage account> # the <prefix>sa… account holding backups and webconfig az role assignment create --role "Storage Blob Data Owner" --assignee <your upn> \ --scope "$(az storage account show -n $acct --query id -o tsv)" # wait a minute or two for the grant to propagate, then repeat until the list is empty: az storage blob list --account-name $acct --container-name backups --include v \ --auth-mode login --query "[].[name,versionId]" -o tsv | while IFS=$'\t' read -r name ver; do az storage blob delete --account-name $acct --container-name backups --name "$name" \ ${ver:+--version-id "$ver"} --auth-mode login >/dev/null done az storage blob list --account-name $acct --container-name backups --include v --auth-mode login -o tsv | wc -l # 0A version whose window has not passed yet is refused with a 409 — that is the protection working; come back when it has expired.
- the purge needs Storage Blob Data Owner on the account. Storage Blob
Data Contributor deletes the current blobs but is refused on their expired
previous versions with
Delete the resource group. With the container empty, the account, and therefore the group, delete normally.
Tidy what sits outside the group. Three things the deployment creates are not inside it and so may not go with it:
- the dashboard's own sign-in application,
EIDGuard-WebUI (<web app name>), an app registration in the workforce tenant that hosts the subscription. Delete it from Entra ID → App registrations (Entra keeps a deleted registration recoverable for 30 days); - the five custom role definitions. These are subscription-level objects, and on every teardown measured so far they were removed with the group anyway — but the RBAC read plane can go on listing them for a while afterwards, and a straggler is possible. If one is still listed a day later, see deployment-troubleshooting.md for how to tell and the commands to clear it;
- if you enabled private networking against a VNet you already owned, the three subnets it used in that VNet, since the VNet is yours and not ours to delete. They cost nothing left alone. Before removing any of them by hand, read permissions.md — the deployment adopts a subnet that already existed under the configured name rather than creating its own, and does not record which case applied, so a subnet carrying that name may well be yours rather than ours.
- the dashboard's own sign-in application,
Related#
- Backing up multiple External ID tenants — how multi-tenant deployments are shaped.
- Web UI — the dashboard the plan surfaces in.
- Deployment troubleshooting — including the
403
tenant_limit_reachedresponse.
