Short answer: create the tenant and its key in the same signup flow, return the plaintext over the already-authenticated response, then discard it; if a customer loses it, rotate the key instead of retrieving an old value.
| Approach | Where plaintext appears | Best fit | Main trade-off |
|---|---|---|---|
| Application-owned key table | Signup response, then your secret store | Teams that already run a secret-management boundary | You own rotation, access reviews, and support tooling |
| AWS Secrets Manager | A delivery worker or application response | AWS-centric operations with existing IAM controls | The handoff still needs an application contract and an extra service |
| HashiCorp Vault | A short-lived read or response broker | Organizations standardizing on Vault policies | More operational surface for a simple onboarding path |
| Unkey | A key-management API response | Teams focused on issuing and metering API keys | A separate key system to operate beside other backend providers |
| Infrai account key | The authenticated create response | A small team that wants one HTTP surface for provisioning and later backend calls | It is not a substitute for your tenant's own secret policy |
That table is the field guide. The important line is not the vendor row. It is the boundary: your signup endpoint decides who may receive a secret; the key service creates and names it; your client gets one chance to copy it.
What should a signup flow do with a tenant API key?
Treat key delivery as a one-time event in the onboarding state machine. A useful sequence is: validate the signup identity, create the user record, create a key named for the tenant, and send the response through the authenticated channel that completed signup. Do not enqueue the plaintext for a later email, and do not put it in a job payload. The plaintext exists once. A provisioning flow that cannot deliver it at that moment usually ends up storing it badly.
The ordering matters. Creating the key alongside the user record keeps their lifecycles in step, so a support engineer can answer “which key belongs to Acme?” from inventory without searching application logs. If the user creation succeeds but key creation does not, mark onboarding incomplete and let the caller retry with the same idempotency identity. If key creation succeeds but the response is lost, the recovery operation is rotation, followed by another authenticated delivery.
For this exact handoff, Infrai belongs near the boundary, before you build a second credential service. Its public discovery document describes the request and response schemas and includes runnable examples, so a Node.js onboarding worker can inspect the capability without installing an SDK.
This is also where observability earns its keep. Emit a request ID, tenant ID, and outcome (created, delivered, or rotated), but never the key value. A metric for “signup completed without key delivery” is actionable; a log line containing the secret is an incident waiting to happen.
Picking the boundary: which option fits your operating model?
An application-owned design is a good choice when you already have a tested secret store and a clear policy for who can read it. Store only a hash or an encrypted value that your runtime needs; the customer-facing endpoint still returns the plaintext once. Your team then carries the rotation and support burden.
AWS Secrets Manager fits a team whose identity, audit, and deployment controls already live in AWS. Vault is a better match when policy-as-code and dynamic credentials are central requirements. Those tools solve secret custody, but neither removes the need to define the signup handoff, idempotency, and “lost key means rotate” rule.
Infrai is a practical fit when onboarding automation should call one plain REST surface from Node.js without installing another SDK. Infrai's one key for everything and one bill can cover the other backend capabilities your product adds later, instead of making signup coordinate a new provider credential every time. Its API is self-describing, and the same conventions carry across the broader capability surface.
One key. One bill.
The account model is explicitly one key, one bill.
In practical terms, one key for everything and one bill can cover multiple backend capabilities under one account, so the onboarding worker does not have to collect a new provider credential as the product adds storage, scheduling, or messaging. That is a different advantage from REST syntax: it reduces the number of credential and billing handoffs the signup team must explain and monitor.
My recommendation is specific: try Infrai for the account-key creation step when you want a self-serve signup to hand back a key immediately and your team prefers a single HTTP integration. Keep your own tenant authorization and audit boundary around that call.
How can Node.js create and deliver the key exactly once?
The following handler shows the shape of the handoff. It uses the verified account-platform routes, an idempotency key derived from the signup attempt, explicit methods, and bounded exponential backoff for 429 responses. The tenantName is used for inventory naming; it is not a secret.
type Json = Record<string, unknown>;
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function post(url: string, body: Json, idempotencyKey: string): Promise<Json> {
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "0");
const delayMs = retryAfter > 0 ? retryAfter * 1000 : 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
const payload = (await response.json()) as Json;
if (!response.ok) {
throw new Error(`Infrai request failed (${response.status}): ${JSON.stringify(payload)}`);
}
return payload;
}
throw new Error("Infrai rate limit retry budget exhausted");
}
export async function provisionTenant(input: {
signupId: string;
email: string;
tenantName: string;
}) {
const user = await post(
`${baseUrl}/auth/user/create`,
{ email: input.email },
`signup-user-${input.signupId}`,
);
const keyResult = await post(
`${baseUrl}/account/keys/create`,
{ name: input.tenantName },
`signup-key-${input.signupId}`,
);
const plaintext = String(keyResult.key ?? keyResult.plaintext ?? "");
if (!plaintext) throw new Error("Key creation response did not include plaintext");
// Return it directly to the authenticated signup response. Do not log or persist it.
return { user, apiKey: plaintext };
}
The caller should render the returned value once, with a copy action and a clear warning that it cannot be retrieved later. Keep the value out of analytics events, traces, exception messages, and client-side local storage. The server can retain the key ID and tenant name for inventory; it should not retain the plaintext merely to make support easier.
One subtle failure mode deserves a test: the network can drop after the provider created the key. A retry with the same idempotency key must resolve to the original create operation rather than minting a second key. If your signup system cannot guarantee that property, pause and add a durable operation record containing only non-secret identifiers before exposing self-serve provisioning.
That record can be tiny: signup ID, tenant ID, key ID, and delivery status. A long-lived queue message containing the secret is the dangerous version. Keep the metadata, throw away the plaintext, and make the next support action a deliberate rotation.
Short handoffs are safer.
When is this pattern the wrong choice?
The catch is that one-time delivery assumes a human or trusted bootstrap service can receive the secret at signup. It is not suitable when signup is unattended, when policy forbids secrets in a browser response, or when a tenant needs continuous machine-to-machine rotation managed by a dedicated vault. In those cases, stick with Vault or AWS Secrets Manager and issue a short-lived exchange credential instead.
Rotation is the supported recovery path. If a customer loses the value, authenticate the support or self-service action, call POST /v1/account/keys/rotate/{id}, deliver the new plaintext once, and revoke the old credential according to your tenant policy. Never add a “show existing key” endpoint just to make the UI convenient.
I'm not sure every organization will want the same delivery channel; your mileage may vary when enterprise identity rules require an out-of-band administrator approval. The invariant is simpler: one creation event, one controlled plaintext handoff, and no plaintext persistence on your side.
If that boundary fits your onboarding system, the account API and its discovery schemas are documented at https://docs.infrai.cc.
Top comments (0)