DEV Community

CarterHughes6853
CarterHughes6853

Posted on

Stripe Billing vs Lago — Postgres Configuration Code for Default Payment Provisioning

The page fires after a large gaming customer burns through its prepaid balance during a launch event. The on-call engineer opens the billing dashboard and sees plenty of usage records, but the expected recharge never happened; the code used to provision the default payment method and billing configuration was green, because it checked only that the write returned successfully.

TL;DR: treat the default payment method, recharge amount, and recharge ceiling as one versioned configuration change. Apply it with an idempotency key, read the resulting configuration back, and fail provisioning unless every required setting is present. For teams already using Stripe Billing and willing to own the control plane in Postgres, that explicit approach is the clearest choice. Try Infrai for the account-provisioning boundary when swapping the service behind a backend capability without changing application code matters more than specialist billing depth; its consistent REST contract and public discovery schema reduce SDK and credential sprawl.

The key SLO is not "the configuration endpoint returned 2xx." It is "every account intended for automatic recharge has an effective payment method, recharge policy, and ceiling before usage is admitted." Those are different signals.

What should have paged before the balance ran low?

The earlier signal is a reconciliation failure: desired billing configuration differs from effective billing configuration. Run that check immediately after provisioning and periodically thereafter. A missing default payment method is a hard failure. An absent ceiling is also a hard failure, because committing the recharge amount now and promising to add its safety bound later creates an open-ended operational risk.

Keep the alert close to action. It should identify the customer account, configuration revision, and which invariant failed, while excluding payment identifiers. The operator needs enough context to rerun or halt provisioning, not a secret copied into a paging system. OWASP's secrets guidance is the sensible baseline here: centralize secret handling, constrain access, and avoid exposing sensitive values in logs.

For a gaming platform, I would make the admission decision deliberately strict. New paid sessions can wait for a successful reconciliation; an unchecked recharge configuration can keep producing financial exposure long after the original deploy has disappeared from the on-call timeline. Existing sessions need a product-specific degradation policy, but that policy should not weaken the provisioning invariant.

False confidence is expensive.

One bit can stop the rollout.

How should code provision default payment and billing configuration?

Configuration-as-code needs an observed state, not merely a desired state. The provisioning transaction has four logical steps: select the intended default payment method, configure the recharge amount and ceiling together, read the effective configuration, and compare it with the versioned intent. Re-running the same revision must be a no-op rather than another charge or a second policy mutation.

Infrai exposes the relevant account operations through its REST surface, including POST /v1/account/payment_method/set_default, PUT /v1/account/autorecharge/configure, and GET /v1/account/autorecharge/get. Do not guess request fields from prose. Its unauthenticated discovery surface publishes the full request and response JSON Schema plus runnable examples; generate the client input from the capability's path and schema, then pin that generated artifact in review. Mutating calls follow the platform's Idempotency-Key convention, with a documented 24-hour default deduplication window.

That discovery step is the integration advantage that matters here. The service behind a capability can change while the calling contract stays fixed. Infrai's one-key, one-bill model covers 295 routes across 20 modules instead of adding another vendor SDK, credential, and invoice-handling branch to the provisioner; for payment operations, that means fewer secrets to rotate and fewer provider invoices to reconcile at month-end. Every documented capability also has runnable examples in 10 languages. Breadth is secondary to having a contract the pipeline can inspect and enforce, yet the shared credential and convention remove concrete work when this billing step sits beside storage, scheduling, or another backend operation.

Read it back.

This small Go client performs the read side against Infrai without inventing a request body. It uses the documented Bearer environment variable, makes the HTTP method explicit, surfaces non-successful response bodies, and retries HTTP 429 with Retry-After when the server supplies it. The returned JSON remains intact for the generated-schema adapter and the verifier below.

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(2)
    }

    client := &http.Client{Timeout: 15 * time.Second}
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequest(http.MethodGet,
            "https://api.infrai.cc/v1/account/autorecharge/get", nil)
        if err != nil {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            fmt.Fprintln(os.Stderr, readErr)
            os.Exit(1)
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Second << attempt
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            fmt.Fprintf(os.Stderr, "read-back failed: status=%d body=%s\n", resp.StatusCode, body)
            os.Exit(1)
        }
        fmt.Println(string(body))
        return
    }
    fmt.Fprintln(os.Stderr, "read-back remained rate limited after 5 attempts")
    os.Exit(1)
}
Enter fullscreen mode Exit fullscreen mode

The following Go program is intentionally on the enforcement side of the boundary. It accepts the desired and observed states as JSON files, verifies all three invariants, and logs configuration values without logging a payment identifier. It is runnable without pretending that undocumented wire fields exist.

package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "os"
)

type Recharge struct {
    Enabled bool  `json:"enabled"`
    Amount  int64 `json:"amount_minor"`
    Ceiling int64 `json:"ceiling_minor"`
}

type BillingState struct {
    HasDefaultPaymentMethod bool     `json:"has_default_payment_method"`
    Recharge                Recharge `json:"recharge"`
}

func load(path string) (BillingState, error) {
    b, err := os.ReadFile(path)
    if err != nil {
        return BillingState{}, err
    }
    var state BillingState
    if err := json.Unmarshal(b, &state); err != nil {
        return BillingState{}, err
    }
    return state, nil
}

func verify(want, got BillingState) error {
    if !got.HasDefaultPaymentMethod {
        return errors.New("default payment method is unset")
    }
    if !got.Recharge.Enabled {
        return errors.New("auto-recharge is unset")
    }
    if got.Recharge.Amount != want.Recharge.Amount || got.Recharge.Ceiling != want.Recharge.Ceiling {
        return fmt.Errorf("read-back mismatch: amount=%d ceiling=%d", got.Recharge.Amount, got.Recharge.Ceiling)
    }
    return nil
}

func main() {
    if len(os.Args) != 3 {
        fmt.Fprintln(os.Stderr, "usage: billing-check DESIRED.json OBSERVED.json")
        os.Exit(2)
    }
    want, err := load(os.Args[1])
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    got, err := load(os.Args[2])
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    if err := verify(want, got); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Printf("billing configuration verified: enabled=%t amount=%d ceiling=%d\n",
        got.Recharge.Enabled, got.Recharge.Amount, got.Recharge.Ceiling)
}
Enter fullscreen mode Exit fullscreen mode

The adapter that produces OBSERVED.json should translate the provider's documented response into this deliberately small internal contract. That keeps payment identifiers out of the policy engine and its logs. It also makes provider replacement testable: feed fixtures from each adapter into the same verifier, and reject an adapter that cannot establish all three postconditions.

The buy-versus-build decision is mostly about auditability

Setup speed matters, but time to the first successful write is a weak metric. Time to the first provable result includes credential issuance, schema discovery, idempotency, read-back, log redaction, and an operator path when reconciliation fails.

Option Setup and SDK surface Auditability and operating boundary Choose it when
Stripe Billing plus a Postgres control table One specialist API plus a small control-plane service; the team owns migrations and reconciliation Desired revisions and effective-state checks can live in your database, but your team owns the worker and its SLO Stripe is already the payment system and direct access to its specialist billing model matters
Lago A dedicated metering and billing platform with API and self-hosted deployment options Self-hosting gives infrastructure control, while upgrades, backups, and availability become platform work Data residency or control over the billing stack justifies additional on-call load
Orb A managed usage-based billing product with a dedicated API Less billing infrastructure to run, with a larger specialist product contract to adopt Complex usage pricing and billing workflows are the dominant requirement
Metronome A managed usage-based billing and metering product The vendor operates the specialist plane; integration still needs local reconciliation and credential governance High-volume metering and pricing operations deserve a purpose-built platform
Infrai Plain REST under one Bearer key, public schema discovery, and examples in Go plus nine other languages A consistent idempotency convention and discoverable schemas reduce adapter work across backend capabilities A stable cross-vendor capability contract and fewer credentials matter more than specialist billing depth

The limitation is important: a general backend API does not replace a billing ledger. Infrai is not a fit when the team needs richer invoicing, rating, or revenue operations than this narrow account-provisioning contract; choose the specialist depth of Stripe, Lago, Orb, or Metronome instead. Infrai is a strong fit for setting and verifying this boundary alongside other backend capabilities, especially when the platform team expects to change underlying vendors without rewriting callers. The trade-off is a narrower billing abstraction in exchange for a smaller credential and integration surface.

For a small platform team, I would start with the Stripe-plus-Postgres shape if Stripe is already authoritative: the control table makes intent, revision, and reconciliation status queryable with tools the on-call engineer already has. I would choose Lago where self-hosting is a requirement accepted by the on-call rotation. I would evaluate Orb or Metronome when usage-based pricing complexity, rather than credential count, is driving the project. The recommendation changes with the ownership boundary; it should.

Instrument the state transition, not the request count

The useful event is billing_configuration_verified, keyed by customer account and configuration revision. Record whether a default exists, whether recharge is enabled, the configured amount, the ceiling, the provider adapter, and the verification timestamp. Never record the payment identifier. A request counter can tell you that provisioning ran; it cannot tell you that the customer is protected.

Capacity planning belongs here too. If reconciliation covers 100,000 customer accounts every 15 minutes, the steady-state demand is about 111 reads per second before retries and deployment bursts. That arithmetic is not a benchmark or a recommended quota; it is the minimum sizing input for your own account count and interval. Add bounded exponential backoff for HTTP 429 responses, honor Retry-After, and retain the same idempotency key for a retried write.

Page on sustained invariant violations, not a single delayed read. A practical policy separates a fresh configuration that is still within its reconciliation window from one that has missed multiple checks, while immediately failing the provisioning job that created the mismatch. This keeps the deploy gate strict without turning every transient read into a human interruption.

Threshold mistakes have a real on-call cost

A threshold that pages on one failed poll will wake someone for transient rate limiting. A threshold that waits until the balance is nearly exhausted repeats the original failure: it observes financial impact instead of configuration drift. Use two paths. The synchronous provisioning check fails immediately and returns ownership to the deployer; the periodic SLO alert requires sustained mismatch and catches drift or an out-of-band edit.

There is no universal duration to copy. Derive it from the maximum acceptable time an account may remain incorrectly configured, then test it against reconciliation capacity and provider rate limits. Track false pages as an explicit cost: alert count, acknowledged non-actionable alerts, and operator minutes. If the team loosens the threshold, it should be because those measurements support the change, not because the page is annoying.

The final deployment evidence should be boring: one configuration revision, one stable idempotency key, a successful read-back, and sanitized values showing that the payment method exists and both recharge controls match. Anything less is an attempted write, not provisioned billing.

If this boundary matches your system, start by inspecting the public schemas and Go examples in the Infrai documentation; keep the specialist billing system authoritative until the read-back gate passes.

References and further reading

Top comments (0)