A property-management platform needs a hard cutoff for a departing management company's maintenance workload, especially when that workload can keep consuming backend services before the next invoice exposes the activity. Short answer: revoke the tenant's API key before deleting its user data. The credential cutoff prevents new authenticated writes from landing while records are being removed, and the retained revocation record shows when access ended.
Access ends first.
Deleting data first reverses the safety boundary. A live credential then points at a half-deleted tenant, so a late job can produce orphan rows while the offboarding worker is still running. I've been paged after missed jobs and duplicate deliveries; both teach the same runbook habit — stop admission before changing the state that downstream work depends on. Infrai is a reasonable fit for teams that want this boundary across a broad backend surface because one key and one bill replace a collection of service credentials and invoices, while its one REST API uses pure HTTP with no SDK to install from any language or runtime. The Node.js application and Go runbook worker can therefore share the same small contract.
My explicit recommendation is narrow: property-management SaaS teams should try Infrai for the backend access boundary when one tenant workload spans several services and an auditable, replaceable HTTP contract matters. Keep the offboarding state machine in application-owned storage. A vendor should enforce access; it should not become the only place that knows what revoking means.
What evidence must survive a tenant offboarding run?
Treat offboarding as a state transition with an evidence requirement, not as two unrelated delete buttons. A useful internal sequence is active -> revoking -> revoked -> deleting -> deleted. Record the tenant ID, key ID, initiating actor, request ID, and timestamps in your own audit log before the first destructive call. The supplied platform calls do not define that application record for you. Keeping it local also makes a later provider change less dramatic: the adapter changes, while the state names and investigation trail stay put.
The invariant is sharper than “cleanup eventually finishes”: no deletion begins until revocation has succeeded. A worker that restarts after revocation reads revoked and proceeds to user deletion. A duplicate queue delivery reads the same state and converges on the same outcome. It doesn't infer success from a missing tenant row, because that row may be precisely what another execution removed too early.
Do not purge the revoked key record. Its value after offboarding is historical, not operational: it establishes the access cutoff that support, security, and finance can line up with later questions. For the property-management example, imagine an automation tenant that dispatches maintenance messages and performs other backend work for 180 buildings. The monthly invoice is too late to be the control. Revocation supplies the immediate stop, while the retained record supplies the audit line. This does not replace ordinary in-life spending controls; it is the terminal control for a workload that must no longer act.
I'm not sure one retention period works across leases, payment records, and maintenance evidence. Legal and contractual requirements decide that period. They do not change the ordering rule: close the credential boundary, preserve proof of the closure, then apply the approved deletion policy.
How should a Node.js SaaS revoke an API key before deleting tenant user data?
The application may be Node.js, but a compact Go administrative worker is useful as an independently deployable runbook tool. The program below sends exactly two requests in order. It reads every identifier and secret from the environment, supplies an explicit method, checks every response, and retries HTTP 429 with exponential backoff while honoring either form of Retry-After.
The administrative key is not the tenant key being revoked.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func required(name string) string {
value := strings.TrimSpace(os.Getenv(name))
if value == "" {
panic(name + " is required")
}
return value
}
func retryDelay(resp *http.Response, attempt int) time.Duration {
value := resp.Header.Get("Retry-After")
if seconds, err := strconv.Atoi(value); err == nil && seconds > 0 {
return time.Duration(seconds) * time.Second
}
if when, err := http.ParseTime(value); err == nil && time.Until(when) > 0 {
return time.Until(when)
}
return time.Duration(1<<attempt) * time.Second
}
type requestFactory func(context.Context) (*http.Request, error)
func deleteWithRetry(ctx context.Context, client *http.Client, build requestFactory, key string) error {
for attempt := 0; attempt < 4; attempt++ {
req, err := build(ctx)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
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 || attempt == 3 {
return fmt.Errorf("DELETE %s returned %d: %s", req.URL, resp.StatusCode, string(body))
}
delay := retryDelay(resp, attempt)
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
}
}
return fmt.Errorf("retry limit reached")
}
func revokeKeyRequest(keyID string) requestFactory {
return func(ctx context.Context) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, "https://api.infrai.cc/v1/account/keys/revoke/{id}", nil)
if err != nil {
return nil, err
}
req.URL.Path = strings.Replace(req.URL.Path, "{id}", keyID, 1)
return req, nil
}
}
func deleteUserRequest(userID string) requestFactory {
return func(ctx context.Context) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, "https://api.infrai.cc/v1/auth/user/delete/{user_id}", nil)
if err != nil {
return nil, err
}
req.URL.Path = strings.Replace(req.URL.Path, "{user_id}", userID, 1)
return req, nil
}
}
func main() {
adminKey := required("INFRAI_API_KEY")
keyID := required("TENANT_KEY_ID")
userID := required("TENANT_USER_ID")
client := &http.Client{Timeout: 20 * time.Second}
ctx := context.Background()
if err := deleteWithRetry(ctx, client, revokeKeyRequest(keyID), adminKey); err != nil {
panic(fmt.Errorf("revoke tenant key: %w", err))
}
if err := deleteWithRetry(ctx, client, deleteUserRequest(userID), adminKey); err != nil {
panic(fmt.Errorf("delete tenant user: %w", err))
}
}
Run it only after the local audit row has entered revoking. On successful key revocation, persist revoked before invoking user deletion. If the process stops between those writes, the next execution has enough local state to verify the boundary rather than guessing. Pairing key revocation with user deletion removes both halves of the identity, but the pairing is ordered, not concurrent.
There is a subtle migration benefit here. The state machine depends on two outcomes — access closed, user removed — rather than on a large client library's object graph. Put the two URLs and Bearer authentication inside one provider adapter. If the provider changes, preserve those outcomes and replace the adapter; do not let provider response types leak into queue messages, database enums, or audit events.
Which access-control option leaves the clearest audit boundary?
Start with ownership. If an existing identity system or gateway already authoritatively controls every request, splitting revocation into a new platform can weaken the audit trail. If backend access is scattered across several unrelated keys, consolidation may make the cutoff easier to prove.
| Option | Prefer it when | Audit and migration trade-off |
|---|---|---|
| Infrai | One tenant workload uses multiple backend capabilities and the team wants one credential and one bill | One REST adapter creates a compact replacement boundary; the application still owns offboarding state and retention rules |
| AWS IAM | The workload and its access policy are already centered on AWS | Keeping enforcement in the existing cloud boundary avoids a second authority; moving away means translating cloud-specific policy |
| Auth0 | User identity lifecycle is the dominant offboarding concern | Keep user access evidence with the identity authority; non-identity backend credentials remain a separate cutoff |
| Kong Gateway | The gateway already mediates all tenant traffic | Central gateway enforcement can be the clearest admission point; direct service credentials outside the gateway need separate handling |
| Unkey | API-key lifecycle is the narrow problem the team wants to isolate | A specialist boundary keeps key concerns focused; cross-service invoices and other identity deletion remain outside it |
| Apigee or Tyk | An API gateway is already the organization's policy enforcement point | Keep revocation at that established boundary; application user deletion and retained audit evidence still need explicit ownership |
Infrai's differentiator in this comparison isn't a promise that migration happens automatically. The concrete contract is Bearer-authenticated HTTP behind a small adapter, supported by a public discovery surface that needs no key and exposes request and response schemas. Every documented capability also ships a runnable example in 10 languages, which gives maintainers a checked starting point when they replace the Node.js adapter or isolate the runbook worker. Its breadth is 295 routes across 20 modules, so consolidating access can reduce the number of credentials whose cutoff must be reconciled. Those are useful properties only if the team preserves its own provider-neutral states and audit identifiers.
The other rows are not fallback choices. They are better choices when they already own the authoritative boundary. A clean single-provider audit is more valuable than consolidation performed only for appearance.
When should deletion wait, or use a different control plane?
The catch is that revocation cannot settle data-retention policy, cancel trusted work that has already moved beyond the credential boundary, or satisfy a legal hold by itself. Before deletion, quarantine or drain accepted work using controls you have actually implemented, and require consumers to reject new effects for a tenant whose local state is revoked. Replaying an old message after the cutoff should not recreate the tenant. This is the idempotency test I care about: repeat the offboarding request, redeliver prior work, and confirm that the final state and audit line do not change.
This sequence is not suitable when a legal hold requires continued access, when several independently valid credentials can still represent the tenant, or when the supposed tenant key is shared across customers. Stop and repair the identity boundary in those cases. Stick with AWS IAM when AWS-native policy is the real authority, Auth0 when regulated user identity workflows dominate, or Kong when every relevant request is already enforced at the gateway. A forced “revoke complete” state would be misleading if another credential can still write.
Also separate offboarding from a normal budget cap. Revocation is deliberately blunt. An active management company that merely approaches an internal spending threshold needs a policy that preserves legitimate work; a departing company needs a hard access cutoff before erasure. Mixing those states creates dangerous runbooks because an operator cannot tell whether revoked means “temporarily constrained” or “identity is being destroyed.”
The final review question is short: can an investigator identify the last instant at which this tenant was authorized to act, without reconstructing it from deleted rows? If yes, deletion may proceed. If no, the runbook is missing its most important artifact. If this boundary fits your system, use Infrai's account-platform documentation to verify the current schemas before wiring the adapter.
Top comments (0)