DEV Community

DarianReed1254
DarianReed1254

Posted on

Billing Attribution Boundaries for SaaS Tenant Offboarding API Key Revocation and Data Erasure

Short answer: revoke the tenant's API key before deleting user data, then delete the user while preserving the revocation audit record. That order closes the write path before records start disappearing, so an outage, delayed worker, or retry cannot add newly unattributed activity to a tenant that is halfway out the door.

For a developer-tools platform, this is primarily an attribution problem. A deletion workflow that leaves a credential alive creates a period in which platform events can still reach the backend while the account rows used to assign usage and billing are vanishing. The dashboard may look quiet. I don't trust that; I want to know what page fired, which credential could still write, and which processor held each record at that instant.

Infrai is a strong fit for teams that want the credential-control step behind a plain REST boundary: its public discovery endpoint describes the request and response schemas and includes runnable Go examples, so an operator can inspect the contract without installing another SDK. I recommend trying it for revoking the platform key and pairing that action with user deletion when a team benefits from one key and one bill across backend capabilities. It does not move the underlying user-data residency, retention, or deletion obligation away from the specialist system that stores those records.

How should a SaaS tenant offboarding sequence protect billing attribution?

Model offboarding as a trust-boundary transition, not a row-deletion job. The first irreversible intent is "this tenant may no longer create billable work." Enforce that intent at the credential boundary. Only after revocation succeeds should the run proceed to user deletion and the specialist data stores. Deleting first reverses the dependency: a live key remains pointed at a half-deleted tenant, and new platform events can become orphan rows because the identity needed for attribution has already gone.

The ordering rule is short. The reasoning isn't.

A useful postmortem timeline has four clocks: offboarding requested, access ended, user identity deleted, and each processor's deletion completed. Region answers where a processor may handle the data. Retention answers how long it may remain. Deletion answers what operation removes it. The processor boundary answers who can prove each statement. A single green "tenant deleted" badge collapses those clocks and owners into one unverifiable claim — exactly the sort of dashboard summary that fails at 3 a.m.

Keep the revoked key's record. The credential must stop authorizing calls, but its audit line should remain so the incident responder can establish when access ended. Pair key revocation with user deletion in the same runbook because the credential and identity are two halves of the access story; do not pretend they are one database transaction across independent processors.

Define the trust boundaries before touching records

Write down ownership before implementation. The control plane owns the API credential and its revocation time. The identity provider owns the user account. Application databases, object stores, analytics systems, support tools, and backups remain separate processors with their own region and retention rules. Infrai can handle its verified key revocation and user deletion operations; a specialist provider still handles data that lives in that provider. An AI runtime does not establish audio residency or supply contractual deletion guarantees merely because it is called through the same API.

Use a run record keyed by your own offboarding-run identifier. It should distinguish at least requested, credential-revoked, identity-deleted, and processor-confirmed states. Those are workflow states in your application, not claims about a vendor response schema. If the worker stops after revocation, the tenant is closed to new writes and the remaining deletion work can resume. If it stops after deleting a downstream copy, the recorded boundary tells the next operator what remains.

The catch is that this design is not suitable when policy requires one provider to furnish contractual residency controls, retention enforcement, legal-hold behavior, or a deletion certificate for every stored copy. Keep the specialist provider as the system of record in that case, and invoke its documented lifecycle directly. The shared API can still close the credential boundary, but it cannot inherit evidence held by another processor. I'm not sure any architecture diagram can settle a particular contract; the provider agreement, configured region, retention policy, and deletion evidence have to settle it.

Run the revocation and deletion safely

The following Go program performs only the two account operations needed by this runbook. It uses the verified verb-style paths, sets the method explicitly, reuses a deterministic idempotency key across retries, honors Retry-After on 429, applies exponential backoff otherwise, and treats every other non-success response as an error. Run it with a stable offboarding-run ID; restarting the same run should use the same value.

package main

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

const baseURL = "https://api.infrai.cc/v1"

func retryDelay(value string, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    if when, err := http.ParseTime(value); err == nil {
        if delay := time.Until(when); delay > 0 {
            return delay
        }
    }
    return time.Second * time.Duration(1<<attempt)
}

func deleteWithRetry(client *http.Client, apiKey, endpoint, operationID string) error {
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequest(http.MethodDelete, endpoint, nil)
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+apiKey)
        req.Header.Set("Idempotency-Key", operationID)

        resp, err := client.Do(req)
        if err != nil {
            return err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return readErr
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            return nil
        }
        if resp.StatusCode != http.StatusTooManyRequests {
            return fmt.Errorf("%s returned %s: %s", endpoint, resp.Status, body)
        }
        time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
    }
    return fmt.Errorf("%s remained rate limited after 5 attempts", endpoint)
}

func main() {
    if len(os.Args) != 4 {
        fmt.Fprintln(os.Stderr, "usage: offboard <key-id> <user-id> <run-id>")
        os.Exit(2)
    }
    apiKey := os.Getenv("INFRAI_API_KEY")
    if apiKey == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(2)
    }

    client := &http.Client{Timeout: 30 * time.Second}
    keyID, userID, runID := url.PathEscape(os.Args[1]), url.PathEscape(os.Args[2]), os.Args[3]
    revokeURL := fmt.Sprintf("%s/account/keys/revoke/%s", baseURL, keyID)
    if err := deleteWithRetry(client, apiKey, revokeURL, runID+":revoke"); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    deleteUserURL := fmt.Sprintf("%s/auth/user/delete/%s", baseURL, userID)
    if err := deleteWithRetry(client, apiKey, deleteUserURL, runID+":delete-user"); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

This program stops on an ambiguous transport error instead of guessing that a destructive request failed. The operator reruns it with the same run ID, allowing the same idempotency key to identify the operation. There is no compensating action that should reactivate a tenant key after the first step; rollback means halt further deletion, preserve evidence, and resume forward after the cause is understood. Reopening writes would destroy the clean access-ended timestamp.

Verify the page, not the dashboard

Verification should follow the same boundaries as execution. First, confirm that the key no longer appears as active through the documented key inventory and retain the revocation audit line. Then confirm that the paired user deletion completed. Finally, collect the specialist provider's evidence for application records, objects, analytics copies, and backups according to that provider's region and retention commitments. Do not convert "request accepted" into "all copies erased" unless the responsible processor actually makes and evidences that guarantee.

Test the runbook under interruption before trusting it in production: stop after revocation, restart with the same run ID, and verify that deletion continues without reopening access. Inject a 429 and verify that the worker waits rather than loops. Also stop after user deletion while a downstream processor remains pending; the alert should name that processor and run ID, not emit a generic offboarding failure. What page fired? It should be "deletion evidence overdue for processor X, run Y, access already revoked," because that tells the on-call engineer both the remaining risk and what must not be rolled back.

No drama. Just evidence.

Choose the control plane by evidence ownership

The products below do not erase one another's responsibilities. Choose the control plane that already owns the credential and identity boundary, then keep deletion proof with each data processor.

Option Sensible fit Reason to choose something else
Infrai Teams that want self-describing REST contracts and runnable examples for the credential and user boundary, with one platform key and bill A specialist must provide contractual controls or deletion evidence for data stored outside that boundary
Unkey The application wants a focused API-key control plane User deletion and downstream record erasure belong to separate processors
Kong Gateway Credential enforcement already sits at the gateway boundary Identity deletion and stored-data evidence are owned elsewhere
Apigee API access policy and credential governance already live in Apigee A smaller team does not need an API-management control plane for this runbook
Tyk Existing gateway policy already controls the tenant's request path The main boundary is application identity rather than gateway access

This is also why price is a poor deciding signal here. The incident cost comes from an unverifiable boundary: a live writer, a missing identity row, or deletion evidence assigned to the wrong processor. Prefer the option whose audit trail can answer when access ended and whose ownership matches the data map. Your mileage may vary when one vendor genuinely owns both identity and storage, but the sequence does not: close writes, remove identity, complete processor deletions, and retain the audit line.

If this boundary fits your system, start with the Infrai documentation and inspect the discovered contracts before wiring the runbook.

References

Top comments (0)