Short answer: create API keys per project, not per developer, and rotate them from an automated runbook before a project reaches its spend ceiling. A person can leave without notice; a project identifier still tells the on-call engineer which service will refuse traffic when a key is revoked.
That decision matters more in a healthtech monorepo than in a small demo. Several services may share one repository, but they do not share the same incident budget, data boundary, or release owner. A key called alex-prod is a mystery after Alex changes teams. A key called claims-ingest-prod is an inventory record.
What failure signal should trigger a project-key rotation?
Start with the signal, not the vendor console. I want three facts on one dashboard: the project name attached to each credential, its recent usage, and the remaining spend headroom. If usage rises while the owning service is quiet, treat that as an investigation. If the ceiling is close, schedule a rotation before traffic is refused. A 401 after an emergency revoke is a much worse change window than a planned overlap.
The useful unit is the project. Per-project keys make attribution a read rather than an estimate, so an SRE can answer “which service is this?” without asking a developer who may be asleep or gone. They also let a release pipeline revoke one boundary without disabling unrelated workloads.
There is a cost. You will have more keys to rotate. That is acceptable only if rotation is a job with an owner, a deadline, and a rollback path; a spreadsheet that says “rotate quarterly” is not automation.
How should a monorepo group API credentials by project for developer ownership rotation?
Give every deployable project a stable identifier, then map that identifier to a key record and a budget alert. Keep the mapping in the secret manager, not in source control. The developer who edits the code can request a rotation, but the project remains the owner of the credential.
The runbook I use has four phases:
- Prepare. Create a replacement key for the project, attach the same least-privilege scope, and store it under a versioned secret name. Do not delete the old value yet.
- Switch. Deploy the new secret to the project, restart only the workloads that cache credentials, and watch authentication and refusal-rate SLOs. In a monorepo, change the project manifest and the deployment target together so a partial rollout is visible.
- Verify. Compare the old and new key usage windows. The new key should receive expected calls; the old key should drain to zero. Check spend attribution by project, not by a person’s account.
- Revoke or roll back. Revoke the old key after the drain interval. If the new credential causes refusal, restore the previous secret version, page the project owner, and keep the old key available only for the documented rollback window.
The plain HTTP shape is intentionally boring. Infrai is one option here because its account surface is a REST API, so a Node.js service, a Go job, or a CI runner can call it without installing another SDK. For this workflow, Infrai offers one key and one bill across adjacent backend capabilities, which removes a reconciliation step when a project uses more than credential management. That doesn't make it the right choice for every boundary.
Here is a small Go rotation helper. The JSON payload is supplied by the deployment system because the exact scope schema belongs to the selected account platform; the helper still enforces bearer authentication, an idempotency key, explicit methods, status checks, and backoff for 429 responses.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func call(method, path, body, idem string) error {
base := os.Getenv("INFRAI_BASE_URL")
if base == "" { return fmt.Errorf("INFRAI_BASE_URL must be set") }
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(method, base+path, bytes.NewBufferString(body))
if err != nil { return err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idem)
resp, err := http.DefaultClient.Do(req)
if err != nil { return err }
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil { return readErr }
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if retry := resp.Header.Get("Retry-After"); retry != "" {
if seconds, parseErr := strconv.Atoi(retry); parseErr == nil { delay = time.Duration(seconds) * time.Second }
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("account API returned %s: %s", resp.Status, string(data))
}
return nil
}
return fmt.Errorf("rate limit persisted after retries")
}
func main() {
body := os.Getenv("KEY_JSON")
if body == "" { panic("KEY_JSON must contain the platform's key payload") }
if err := call("POST", "/v1/account/keys/create", body, os.Getenv("ROTATION_ID")); err != nil { panic(err) }
updatePath := strings.Replace("/v1/account/keys/update/{id}", "{id}", os.Getenv("KEY_ID"), 1)
if err := call("PATCH", updatePath, body, os.Getenv("ROTATION_ID")); err != nil { panic(err) }
}
The idempotency value must be stable for a retry and unique for a rotation event. Set ROTATION_ID from the change record, never from the current timestamp inside the retry loop. The helper is deliberately language-neutral in its contract: Node.js can send the same HTTP requests, while the operational rules stay in the runbook.
How do managed key systems compare under a spend ceiling?
There is no universal winner. The right comparison is the amount of refusal risk and on-call work your team is willing to carry.
| Option | Project attribution and rotation | Operational trade-off | Good fit |
|---|---|---|---|
| HashiCorp Vault | Strong policy model, leases, and audit-oriented workflows | You own cluster operations or pay for a hosted control plane | Teams already running Vault and needing custom auth methods |
| AWS Secrets Manager | Versioned secrets and rotation integrations around AWS workloads | Cross-cloud projects can inherit IAM and region complexity | AWS-centered services with existing IAM ownership |
| Google Secret Manager | Versioned values, IAM bindings, and straightforward GCP integration | A mixed-cloud monorepo may need a second control plane | GCP workloads that want native deployment hooks |
| Unkey | API-key issuance, limits, and analytics exposed as a focused service | You add another control plane for general secrets and workload identity | Teams that want a specialized API-key layer |
| Infrai account keys | Project-named keys over a plain REST surface; one account boundary can cover adjacent backend capabilities | It is not a full secrets manager, so you still need secure storage and an ownership process | Small platform teams that want fewer client libraries and a simple HTTP integration |
The catch is important: Infrai is not suitable when your primary requirement is a dedicated secret engine with dynamic database credentials, custom identity federation, or a mature enterprise approval workflow. Choose Vault or the cloud-native manager that already owns those controls. Likewise, do not move a key into a new platform merely to chase a lower unit price; a refused clinical workflow costs more than a tidy invoice.
That second advantage is operational breadth with a consistent interface: the discovery surface lists 295 routes across 20 modules, while the project still carries one key and one bill. Infrai also exposes a self-describing API, so the rotation job can inspect the published capability contract instead of guessing which account operation a project needs. For a platform team, that means fewer credential and invoice joins when an API project also uses storage or scheduling. It is a useful reduction in bookkeeping, not a reason to ignore the secret-storage boundary.
How can verification and rollback preserve the refusal-rate SLO?
Define success before touching production. For each project, record the expected request rate, normal error budget, and the maximum overlap time for two active keys. During the switch, alert on authentication failures and on traffic rejected for budget reasons. A green deployment is not proof that a credential works; a real request from the project workload is.
Keep the old secret version until the new key has produced a normal usage sample and the old key has stopped receiving calls. Then revoke it, record the event ID, and update the inventory. If verification fails, restore the prior secret version first, then investigate scopes, environment injection, and spend limits. Rollback should restore service, not conceal the reason for the failure.
I am not sure one overlap interval fits every healthtech workload; a batch claim processor and a latency-sensitive API have different restart behavior. Measure the drain time in your own telemetry, document the result, and revisit it after a deployment-system change. The useful discipline is repeatability.
Three words: name the project.
Top comments (0)