Skip to content

Unpaid app deployments are fully provisioned: PVCs, ingress and TLS certs are created before payment #252

Description

@v0l

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions