/ Docs Guides / Onboard a customer Auth guide ← All guides
Tenants & tiers

Signup queue → admission check → one atomic onboard.

A customer reaches IG1 two ways: the public signup queue (a request an operator approves) or a direct admin onboard. Either way the provisioning call is the same — POST /v1/tenants builds a real Keystone project with the tier's quota set, the network substrate, a Zitadel machine user and its first credential, atomically, with rollback. There is no step two.

1 · The signup queue (public)

POST /v1/signup is the only unauthenticated write on the API. It creates one row in a bounded queue and nothing else — no Zitadel user, no Keystone project, no quota — so an abusive signup costs the platform nothing. The public response is identical whether or not the address is already queued: a 409 would turn signup into an account-existence oracle.

curl -s -X POST \
  https://api.cloud.ig1.com/v1/signup \
  -H "Content-Type: application/json" \
  -d '{"email": "jane.doe@ig-1.net", "org_name": "Acme Corp"}'   # → 202

Operators review the queue (admin-only):

GET  /v1/signups?state=pending      # the queue
GET  /v1/signups/capacity           # the sellable budget, before and after each tier
POST /v1/signups/{signup_id}/approve   # body: {"tier": "discovery"} — runs the onboard
POST /v1/signups/{signup_id}/reject
Approval is admission-checked. Approving a tenant spends finite platform capacity, so the tier's full footprint is tested against the measured sellable budget before anything is provisioned — and the response carries the numbers either way. A queue that lets you approve past the point of overselling is a queue that will oversell. The approved signup onboards with the requester as the tenant's human owner: they get an initialisation mail and choose their own password (nothing to reveal once).

2 · The onboard call (admin)

curl -s \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -X POST https://api.cloud.ig1.com/v1/tenants \
  -d '{"name": "Acme Corp", "tier": "discovery"}'   # tier omitted = discovery

The response carries the tenant record — project, tier, quotas, the network substrate it built — and the reveal-once client pair for the tenant's first machine user. Hand it to the customer through your secret channel; it is never stored and never retrievable again:

{
  "tenant": {
    "slug": "acme-corp",
    "project_id": "d7e2…",
    "project_name": "ig1-customer-acme-corp",
    "tier": "discovery",
    "quotas": { "nova": {…}, "cinder": {…}, "neutron": {…} },   // the tier's set
    "network": { "network_id": "…", "subnet_id": "…", "router_id": "…", "external_network_id": "…" }
  },
  "credential": {
    "id": "cred-…", "tier": 2,
    "client_id": "ig1-tenant-acme-corp",
    "client_secret": "…"          // this response only
  }
}

3 · The quota tiers

Tiers live in config/quota-tiers.yml, and every number there is justified against measured platform capacity — never a public cloud's defaults. The rule that matters is not the per-tier table but the aggregate invariant: the sum of granted resources across live tenants must stay inside the sellable budget, checked at approval time and asserted in CI, so an edit that oversells the fleet fails the build instead of production.

TierComputeBlock storageNetworkObjectKaaSEdge
discovery (default)8 cores · 16 GiB · 5 instances8 volumes · 80 GiB3 networks · 2 routers · 2 FIPs5 buckets · 20 GiB1 cluster · 3 workers1 exposure
standard32 cores · 64 GiB · 20 instances32 volumes · 300 GiB12 networks · 4 routers · 8 FIPs25 buckets · 100 GiB3 clusters · 10 workers4 exposures
extension128 cores · 256 GiB · 60 instances100 volumes · 1 200 GiB24 networks · 8 routers · 16 FIPs100 buckets · 500 GiB8 clusters · 30 workers10 exposures

Two more states complete the machine: pending (a signed-up account nobody has approved — no Keystone project at all, so there is nothing to apply numbers to) and suspended — an all-zero quota PUT over a live project, the object-storage quota on the tenant's S3 user included, so direct S3 writes stop with the same call. Running workloads keep running while nothing new can be created: reversible and non-destructive, the correct first response to abuse, ahead of deleting anything. One limit is real: Cinder refuses a block-storage quota below current usage and has no override, so when a tenant's volumes exceed the target tier the block keys are held at current usage (nothing new can be created, nothing is destroyed), everything else moves, and the response lists each held key under applied.cinder_held_at_usage — a re-run re-clamps to the then-current usage (Nova and Neutron are forced through and keep running workloads running).

Moving a tenant between tiers is an admin call, admission-checked exactly like approval:

curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -X POST https://api.cloud.ig1.com/v1/tenants/d7e2.../tier \
  -d '{"tier": "standard"}'
# or: ig1 tenant set-tier d7e2... --tier standard

The call is idempotent and ordered: Nova, Cinder and Neutron quota sets first, then the tenant's S3 user quota on the object gateway, and the ig1:tier= project tag last — a project never advertises a tier its limits do not match. If the object gateway refuses the quota write the call answers 502 with the OpenStack quotas already applied and the tag unwritten (the response says which, and whether the failure is transient — re-run — or structural — the gateway release or the service account's caps refuse the call as shaped, so a re-run cannot help). Re-running the same call converges a half-moved tenant; nothing is skipped silently. (api 0.11.1–0.11.4: the object-gateway quota write on this call had been refused by the gateway for every tenant — a request-shape bug on our side, fixed and pinned by test.)

4 · What the customer sees — GET /v1/quotas

Tier 0, self-service: the caller's own limits and usage, with the tier that set them. The project is derived from the credential — ?project= is an admin-only override, refused with a constant-shape 403 for everyone else regardless of whether the project exists.

curl -s -H "Authorization: Bearer $TOKEN" \
  https://api.cloud.ig1.com/v1/quotas
# or: ig1 quota show

The platform.object_storage rows carry the tier's limit and the object gateway's live ceiling — enforced and used per row, plus an enforcement verdict (source: rgw with matches_tier; source: tier before the tenant's first S3 key exists; unavailable if the gateway is not answering). Object-storage limits live only on the gateway — no OpenStack command lists them — so this is the one place a customer or operator can see the number that actually binds S3 writes, and whether it agrees with the tier.

5 · What the customer gets

Customer first steps

# mint
TOKEN=$(curl -s \
  -u "ig1-tenant-acme-corp:$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  --data-urlencode "scope=openid urn:zitadel:iam:org:project:roles urn:zitadel:iam:org:project:id:385253743135817916:aud" \
  https://zitadel.cloud.ig1.com/oauth/v2/token | jq -r .access_token)

# resolve — expect tenant_id = their project, all_projects = false
curl -s -H "Authorization: Bearer $TOKEN" \
  https://api.cloud.ig1.com/v1/whoami

Operations notes