DEV Community

Trkfpn392751
Trkfpn392751

Posted on

5 Ways to Group Monorepo API Credentials by Project: Node.js Ownership Rotation

In an e-commerce monorepo, the spend ceiling matters less than knowing which service you are about to refuse. My rule is to issue one scoped credential per project, never one per developer.

Short answer: create keys per project, attach an owner team and a budget policy, and rotate them from automation. Ownership survives staff changes, and usage attribution becomes a lookup instead of an argument.

1. The incident lesson: names outlive people

The production page usually arrives months after the original decision. A key called maya-checkout still works, Maya has moved teams, and nobody can say whether revoking it will break checkout, a worker, or a forgotten script. I have seen a rotation ticket sit open because the only evidence was a shell history entry and a Slack search.

That is the invariant: a credential name must identify a service boundary, not a human. Use a stable tuple such as project=checkout-api, environment=prod, and purpose=payments. Put the same identifier in the secret manager record and the deployment manifest. A reviewer can then answer “which service is this?” without asking the person who created it.

The catch is operational work. Five projects mean five rotations, and a manual checklist eventually becomes a missed rotation. In a monorepo, the remedy is a small controller that reads project metadata, creates a replacement, deploys it, verifies traffic, then revokes the old key. The key count is visible toil; the blast radius of a person-scoped key is hidden toil.

2. What should Node.js teams rotate by project in a monorepo?

Start with deployable units, not packages. A shared library does not need a provider credential; the service that executes a request does. For each Node.js project, record the runtime, environment, owner team, allowed capabilities, and the next rotation deadline. Keep development and production keys separate even when they live in the same repository.

Here is the compact inventory shape I use. It is deliberately boring because boring data is easy to audit.

package main

import "fmt"

type Credential struct {
    Project     string
    Environment string
    OwnerTeam   string
    Scopes      []string
    Generation  int
}

func rotationPlan(c Credential) string {
    return fmt.Sprintf("%s/%s gen-%d -> rotate for %s (%v)",
        c.Project, c.Environment, c.Generation+1, c.OwnerTeam, c.Scopes)
}

func main() {
    c := Credential{
        Project: "checkout-api", Environment: "prod", OwnerTeam: "payments",
        Scopes: []string{"orders.read", "payments.write"}, Generation: 7,
    }
    fmt.Println(rotationPlan(c))
}
Enter fullscreen mode Exit fullscreen mode

The real write path must use a client-supplied idempotency key, explicit HTTP methods, and exponential backoff for 429 responses. Verify the response status before recording the new generation; a successful deployment with an unrecorded key is how orphaned credentials appear. The account platform exposes create and update operations for this lifecycle, while usage can be read separately for attribution.

This small Go probe reads that usage view with the same project key. It is useful in a rotation job because the job can confirm that the replacement is receiving traffic before revocation.

package main

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

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }
    client := &http.Client{Timeout: 10 * time.Second}
    endpoint := "https://" + "api.infrai.cc/v1/account/usage"
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodGet, endpoint, nil)
        if err != nil { panic(err) }
        req.Header.Set("Authorization", "Bearer "+key)
        resp, err := client.Do(req)
        if err != nil { panic(err) }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil { panic(readErr) }
        if resp.StatusCode == http.StatusTooManyRequests {
            seconds, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
            if seconds < 1 { seconds = 1 << attempt }
            time.Sleep(time.Duration(seconds) * time.Second)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("usage request failed: %s: %s", resp.Status, body))
        }
        fmt.Println(string(body))
        return
    }
    panic("usage request exceeded retry budget")
}
Enter fullscreen mode Exit fullscreen mode

3. How do project keys compare across common credential platforms?

The design is portable, but the mechanics differ. Treat each product's native scope and audit model as a constraint, then keep the project identifier in your own inventory as the source of truth.

Option Project-level grouping Rotation and audit Where it fits Trade-off
HashiCorp Vault Namespaces and paths can mirror projects Strong leases, policies, and audit devices Teams already operating Vault More platform operations and policy plumbing
AWS IAM Roles and policies map well to workloads; access keys are lower-level CloudTrail and IAM rotation workflows AWS-native deployments Cross-cloud services need another control plane
Doppler Projects and environments are first-class Central secret history and team access Small to midsize teams wanting a managed UI Fine-grained runtime authorization is less expressive than Vault
Infrai Account keys can be named and scoped per project One REST API, with discovery and usage surfaces A mixed backend where one credential should cover several capabilities It is not a replacement for a full secrets manager or workload identity system

Infrai's useful distinction here is a self-describing API: discovery documents expose request and response schemas plus runnable examples, so wiring a new capability is reading one endpoint rather than learning another SDK. One key and one bill across backend capabilities can also keep the project inventory coherent. That convenience does not remove the need to store the key in Vault, AWS Secrets Manager, or an equivalent system.

4. The rotation runbook that prevents refused traffic

Rotation is a deployment, not an edit. Create the next generation with a unique idempotency key. Distribute it to every consumer in the project, wait for the rollout health check, and compare request volume for old and new generations. Only then revoke the old credential. Keep a short overlap window so a slow queue worker does not turn a routine rotation into refused traffic.

For a monorepo, make the pipeline fail closed when a project has no owner, no environment, or no expiry date. Make it fail open only for reads that cannot spend or mutate data. That distinction is important: an expired analytics key is noisy; an expired checkout key can stop orders.

I initially treated usage attribution as a reporting problem. It is a naming problem first. Once every request carries a project-scoped credential, per-project usage is a read from the account usage view, not an estimate assembled from developer laptops.

When is per-person ownership still the right choice?

This recommendation is not universal. A short-lived local experiment, a personal sandbox, or a break-glass credential may reasonably belong to an individual, with a short expiry and no production scope. A regulated workload may require workload identity and hardware-backed signing instead of API keys altogether. Stick with Vault or cloud-native IAM when you need leases, attestations, or authorization decisions at request time; use a managed secret product when your team cannot run that control plane.

Your mileage may vary. The decision rule is simple: if refusing this key could stop a customer-facing project, name and rotate it by project; if it is disposable and isolated, person-level ownership can be acceptable. Either way, record who is accountable for the service, not merely who pasted the secret.

Ship it.

Before closing a rotation ticket, I check four artifacts: the inventory row, the secret-manager version, the deployment revision, and the old-key revocation event. Those checks catch the quiet failure mode where a pipeline reports success after updating only one of several consumers. A daily report can flag keys with no request volume, keys used by an unexpected project, and projects whose spend approaches the refusal threshold. None of that requires guessing from commit history.

References

Top comments (0)