Reported by Kieran, 2026-07-27: "Apps are deployed by the operator even before paying? that's wrong it should only be created once paid."
Confirmed at 529de91. Payment gates the replica count and nothing else. Everything an unpaid order needs in the cluster is created on the operator's next reconcile.
What an unpaid order gets today
v1_create_app_deployment (lnvps_api/src/api/apps.rs:507) writes the app_deployment row immediately with is_setup: false on the subscription (:566). reconcile_one (lnvps_operator/src/app_deployments.rs:1082) then runs the whole provisioning sequence and only consults the billing gate at step 4:
| Step |
Line |
Gated on payment? |
| Namespace + NetworkPolicy |
:1097-1108 |
No |
| Generated secrets |
:1110-1113 |
No |
| PVCs |
:1166-1168 |
No |
| ConfigMap / Secret / Service |
:1169-1180 |
No |
| Deployment object |
:1181-1196 |
replicas 0 — this is the only gate |
| Ingress + TLS certificate |
:1201-1215 |
No |
let replicas = if gate == GateReason::Running { 1 } else { 0 }; (:1158) is the entire enforcement.
Why this is worse than wasted disk
1. Real storage is provisioned for free. PVCs are created at full size — Route96 25 GiB, HAVEN 30 GiB, the Buzz example 98 GiB — multiplied by resource_multiplier (:1168), which goes to 16.
2. It burns Let's Encrypt quota, and that degrades the paid product. Step 5 applies an Ingress with cluster_issuer: letsencrypt-prod, so an unpaid order triggers real certificate issuance for {name}.{ingress_domain}. Let's Encrypt caps certificates per registered domain per week. Enough unpaid orders exhaust the quota for apps.lnvps.cloud — and then paying customers cannot get certificates. This is the sharpest consequence: a free action denies service to people who paid.
3. Unpaid deployments consume cluster capacity and block paid orders. AppClusterCapacityService::used sums every non-deleted deployment (lnvps_api_common/src/capacity.rs:560-575), with no payment check. Since admission calls select_in_region against that number (apps.rs:533), unpaid rows can exhaust a cluster and make a paying customer's order fail with "No cluster with enough capacity".
The VM path already gets this right, which is the cleanest evidence that apps diverged rather than decided:
// lnvps_api_common/src/capacity.rs:244-256
// Only count VMs that have been paid for (subscription is_setup = true)
let is_paid = self.db.get_subscription_by_line_item_id(vm.subscription_line_item_id)
.await.map(|s| s.is_setup).unwrap_or(false);
if is_paid { vms.push(vm); }
4. Nothing limits how many an unauthenticated-in-practice caller can create. The endpoint takes NIP-98 auth and upsert_user mints a user for any pubkey (apps.rs:512). Nostr keys are free and unlimited. I found no per-user deployment cap and no rate limit — scoped to lnvps_api/src/api/apps.rs and lnvps_api_common/src/capacity.rs, which are the two files on this path; I have not audited middleware.
Fix
1. Return before provisioning anything when the subscription was never set up. The seam already exists: GateReason distinguishes Unpaid (:995, never paid, is_setup = 0) from Expired (:996, paid then lapsed). Move the gate to the top of reconcile_one and, on Unpaid, do the status write-back and return — no namespace, no secrets, no PVCs, no ingress.
Do not collapse Unpaid and Expired. An expired deployment must keep its PVCs and scale to 0, exactly as it does now — that is customer data and the current comment at :1124-1127 says so deliberately. Only the never-paid case gets nothing.
2. Exclude never-paid deployments from AppClusterCapacityService::used, matching the VM path above. Expired ones still count, since they still hold their PVCs.
3. Leave the name reservation alone — find_app_deployment_by_cluster_name (apps.rs:545-554) reserving the hostname at order time is defensible: it stops a customer paying and then finding the name taken. Worth revisiting if unpaid rows accumulate, but it costs nothing in the cluster once fix 1 lands, so it is not part of this issue.
Ordering
This goes ahead of the rest of the api queue. Everything else open on this repo is correctness or accounting; this one hands out storage and certificates for free and can deny service to paying customers. Overridable — say so on the issue and I will re-rank.
Bojan yours. api#193 part 2 moves behind it.
— Alejandra (PM), posting from Kieran's account
Reported by Kieran, 2026-07-27: "Apps are deployed by the operator even before paying? that's wrong it should only be created once paid."
Confirmed at
529de91. Payment gates the replica count and nothing else. Everything an unpaid order needs in the cluster is created on the operator's next reconcile.What an unpaid order gets today
v1_create_app_deployment(lnvps_api/src/api/apps.rs:507) writes theapp_deploymentrow immediately withis_setup: falseon the subscription (:566).reconcile_one(lnvps_operator/src/app_deployments.rs:1082) then runs the whole provisioning sequence and only consults the billing gate at step 4::1097-1108:1110-1113:1166-1168:1169-1180:1181-11960— this is the only gate:1201-1215let replicas = if gate == GateReason::Running { 1 } else { 0 };(:1158) is the entire enforcement.Why this is worse than wasted disk
1. Real storage is provisioned for free. PVCs are created at full size — Route96 25 GiB, HAVEN 30 GiB, the Buzz example 98 GiB — multiplied by
resource_multiplier(:1168), which goes to 16.2. It burns Let's Encrypt quota, and that degrades the paid product. Step 5 applies an Ingress with
cluster_issuer: letsencrypt-prod, so an unpaid order triggers real certificate issuance for{name}.{ingress_domain}. Let's Encrypt caps certificates per registered domain per week. Enough unpaid orders exhaust the quota forapps.lnvps.cloud— and then paying customers cannot get certificates. This is the sharpest consequence: a free action denies service to people who paid.3. Unpaid deployments consume cluster capacity and block paid orders.
AppClusterCapacityService::usedsums every non-deleted deployment (lnvps_api_common/src/capacity.rs:560-575), with no payment check. Since admission callsselect_in_regionagainst that number (apps.rs:533), unpaid rows can exhaust a cluster and make a paying customer's order fail with "No cluster with enough capacity".The VM path already gets this right, which is the cleanest evidence that apps diverged rather than decided:
4. Nothing limits how many an unauthenticated-in-practice caller can create. The endpoint takes NIP-98 auth and
upsert_usermints a user for any pubkey (apps.rs:512). Nostr keys are free and unlimited. I found no per-user deployment cap and no rate limit — scoped tolnvps_api/src/api/apps.rsandlnvps_api_common/src/capacity.rs, which are the two files on this path; I have not audited middleware.Fix
1. Return before provisioning anything when the subscription was never set up. The seam already exists:
GateReasondistinguishesUnpaid(:995, never paid,is_setup = 0) fromExpired(:996, paid then lapsed). Move the gate to the top ofreconcile_oneand, onUnpaid, do the status write-back and return — no namespace, no secrets, no PVCs, no ingress.Do not collapse
UnpaidandExpired. An expired deployment must keep its PVCs and scale to 0, exactly as it does now — that is customer data and the current comment at:1124-1127says so deliberately. Only the never-paid case gets nothing.2. Exclude never-paid deployments from
AppClusterCapacityService::used, matching the VM path above. Expired ones still count, since they still hold their PVCs.3. Leave the name reservation alone —
find_app_deployment_by_cluster_name(apps.rs:545-554) reserving the hostname at order time is defensible: it stops a customer paying and then finding the name taken. Worth revisiting if unpaid rows accumulate, but it costs nothing in the cluster once fix 1 lands, so it is not part of this issue.Ordering
This goes ahead of the rest of the api queue. Everything else open on this repo is correctness or accounting; this one hands out storage and certificates for free and can deny service to paying customers. Overridable — say so on the issue and I will re-rank.
Bojan yours. api#193 part 2 moves behind it.
— Alejandra (PM), posting from Kieran's account