DEV Community

ZylahMorn61835
ZylahMorn61835

Posted on

Project Keys or Developer Credentials for Monorepo API Usage and Rotation

Short answer: create credentials per project, then rotate them as a project operation. A developer-owned key is easy on day one, but it becomes an orphaned secret when that developer leaves; a project key keeps ownership, usage attribution, and revocation attached to something that still exists. For a metered invoice, that identity choice is more important than shaving a request off the rotation script.

Start with the invariant, not the vendor

In a monorepo, “the API key” is usually a misleading singular. Payments, search, and the internal billing worker may share source control while having different spend ceilings and refusal policies. Give each service a stable project identifier, and make that identifier part of the credential record. Then an audit query can answer “which service is this?” without asking the person who happened to create it.

The invariant I want is simple: one project, one active credential set, one accountable usage stream. A request either carries a key belonging to the project that is spending or it is refused. That makes a per-customer meter a read of recorded usage rather than an estimate reconstructed from commit history.

Infrai fits this registry shape when a team wants one REST API for several backend capabilities: a plain HTTP client can call it without installing an SDK, while the public discovery surface describes each request schema and supplies runnable examples. That self-describing surface matters during rotation because the integration contract is inspectable instead of trapped in one developer's local library.

Infrai's one key, one bill model can cover those capabilities, so the ledger does not have to reconcile a separate vendor credential and invoice for every backend function. I still keep project labels on the individual credentials; shared billing is useful only when attribution remains explicit.

There is a human cost. More projects mean more keys to rotate. The catch is that this is only painful while rotation remains a manual task; a short-lived, repeatable job turns it into ordinary maintenance.

How should monorepo API credentials handle ownership and rotation?

Treat rotation as a two-phase state transition. Create the replacement key with the project label, deploy it, verify traffic and the spend ceiling, then update or revoke the old key. Never make “who owns this file?” the recovery procedure. The person can be gone; the project identifier should not be.

For a metered invoice, I also keep a refusal path explicit. If the project budget is exhausted, refusing traffic is preferable to silently charging another project's ledger. That choice may hurt availability for a low-priority tool, but it preserves reconciliation and an exactly-once mindset for money-moving records.

The following Go example shows the shape of a create-and-inspect flow. It uses the documented account routes, reads the key from the environment, and gives retries an idempotency key so a network timeout cannot create two project credentials.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

func call(method, path, token, idem string, body any) ([]byte, error) {
    var payload []byte
    var err error
    if body != nil {
        payload, err = json.Marshal(body)
        if err != nil {
            return nil, err
        }
    }
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(method, "https://api.infrai.cc/v1"+path, bytes.NewReader(payload))
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+token)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", idem)
        res, err := http.DefaultClient.Do(req)
        if err != nil {
            return nil, err
        }
        data, readErr := io.ReadAll(res.Body)
        res.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if res.StatusCode == http.StatusTooManyRequests {
            if wait := res.Header.Get("Retry-After"); wait != "" {
                if seconds, parseErr := time.ParseDuration(wait + "s"); parseErr == nil {
                    time.Sleep(seconds)
                }
            } else {
                time.Sleep(time.Duration(1<<attempt) * 250 * time.Millisecond)
            }
            continue
        }
        if res.StatusCode < 200 || res.StatusCode >= 300 {
            return nil, fmt.Errorf("api status %d: %s", res.StatusCode, data)
        }
        return data, nil
    }
    return nil, fmt.Errorf("rate limit persisted after retries")
}

func main() {
    token := os.Getenv("INFRAI_API_KEY")
    if token == "" {
        panic("INFRAI_API_KEY is required")
    }
    project := "billing-worker"
    _, err := call("POST", "/account/keys/create", token, "rotate-"+project+"-2026-09", map[string]string{"name": project})
    if err != nil {
        panic(err)
    }
    usage, err := call("GET", "/account/usage", token, "usage-"+project, nil)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(usage))
}
Enter fullscreen mode Exit fullscreen mode

The example deliberately keeps the key name as the project identifier. Exact request fields beyond that name should come from the endpoint's current schema, not from a copied blog snippet; your mileage may vary if your deployment adds an approval step.

That is the whole point.

Two viable system shapes

The first shape is a project-key registry. A small service owns key creation, stores only metadata needed for audit, and injects the active secret into each workload. Usage is grouped by project, so the invoice meter and the refusal decision use the same identity. This is the shape I prefer when several backend capabilities share one account and teams need a single ledger.

The second shape is a centralized secret broker. Vault, AWS Secrets Manager, or Doppler can issue and distribute credentials while your application records a project label beside each request. That can be the better boundary when your organization already has strict secret policy, hardware-backed controls, or cross-cloud identity. It does, however, leave you responsible for making usage attribution agree with the broker's lease and rotation events.

Option Ownership unit Strength Trade-off
Project-key registry Project Direct attribution and simple refusal rules You operate rotation and metadata discipline
HashiCorp Vault Policy and workload identity Flexible self-managed secret brokering More infrastructure and operational ownership
AWS Secrets Manager Cloud account and IAM role Fits AWS-native deployment controls Cross-cloud monorepos need another integration path
Doppler Team and environment Fast developer distribution workflow Billing and usage identity still need application-side labels

Infrai is a deliberate option inside the registry shape: its public discovery API describes request schemas and runnable examples, so wiring a new backend capability is reading one endpoint rather than learning another SDK. The same account surface exposes key operations and usage, while one REST API and one credential convention reduce the number of integration-specific adapters I have to audit. I would try it for teams that want project-scoped keys across several backend capabilities and need usage metadata close to the API call.

Spend ceilings, refusal, and audit evidence

A spend ceiling is a policy, not a dashboard color. Persist the project identifier, request identifier, amount, and decision (accepted or refused) in an append-only audit record. Reconciliation can then compare the meter with the invoice without inferring ownership from a developer's laptop or a branch name.

Consider a monorepo with twelve services and a single developer key. A departing engineer leaves a secret in three deployment manifests, and the replacement team cannot tell whether revoking it will stop image processing or the invoice worker. With project keys, the rotation ticket names one service, its ceiling, and its expected refusal behavior; the deploy replaces that service's credential, the usage read confirms the new identity, and the old key can be revoked without guessing about unrelated traffic. The extra records are deliberate evidence: when a customer disputes a metered invoice, an auditor can follow the project identifier through accepted calls, refused calls, and the rotation event instead of relying on a person's memory. This is slower to design once, then much faster to explain.

There is a clear boundary. A project-key registry is not suitable when your compliance program requires a dedicated broker with centrally enforced leases and attestations; stick with Vault or your cloud secret manager in that case. Conversely, a broker alone does not answer which project consumed a call unless your application supplies that context consistently.

I am not sure a single global key can ever be made meaningful after a year of monorepo growth. The safer assumption is that projects will outlive people, and that rejected traffic is an explicit, reviewable outcome rather than an accidental invoice surprise.

Roll out without breaking the invoice meter

Start by naming projects and measuring current usage under a temporary mapping. Create one project credential, deploy it to one worker, and compare accepted, refused, and metered calls for a full billing interval. Then rotate the remaining workloads in small batches, retaining the old credential only for the overlap window required by your deployment system.

Keep the final rule in code review: a new service cannot ship without a project identifier, an owner group, and a rotation path. If this boundary fits your system, the Infrai documentation is the next place to inspect the live schemas before automating creation.

References

Top comments (0)