NamoID public betaBuilding customer authentication? Get setup help and share feedback with other builders.Join the Slack community
NamoID
All posts
NamoID Blog

Publishable Keys Aren't Secrets

A key in frontend JavaScript is public. Users can read it in DevTools, browser extensions can inspect it, and anyone can copy it from a network request. Calling that value a “secret” doesn't make it one.

That does not mean browser applications cannot use keys. It means the key must be designed as a public identifier with tightly limited authority. This article explains the boundary between publishable and secret keys, how NamoID enforces it, and the review questions that catch dangerous key designs.

The short answer

A publishable key identifies an application environment and permits only browser-safe operations. It is expected to be copied. A secret key authenticates a trusted backend and permits privileged server operations. Disclosure of a secret key is a security incident.

Browser                                      Backend
namoid_auth_pk_test_...                      namoid_auth_sk_test_...
public identifier                            confidential credential
origin restricted                            never sent to browser
rate limited                                 privileged server operations
browser-safe configuration                   token/session validation

The difference is authority, not appearance. Both values may look random, but only one is a credential.

Threat-model the key before naming it

The fastest way to classify a key is to assume an attacker has copied it. Then list what the attacker can do without any other credential.

Copied valueExpected attacker capabilityUnacceptable capability
Publishable keyRead browser-safe auth configuration and start rate-limited public flowsRead users, validate arbitrary tokens, change settings, or create privileged sessions
Auth secret keyNothing, because disclosure triggers immediate revocationAny successful trusted-server operation after revocation
User access tokenAct within that user's granted scope until expiry or revocationCross-user, cross-project, or administrative access

This exercise exposes a common design failure: a value is labelled publishable because it sits in frontend code, while the backend quietly grants it server authority. The label does not create safety. Endpoint authorization does.

Apply the same test to error messages and metadata. A browser-safe config response should not reveal provider secrets, internal IDs that enable privileged routes, private callback URLs, or operational limits useful for bypassing abuse controls.

What a publishable key identifies

A NamoID publishable key has a namoid_auth_pk_ prefix and belongs to one project environment. It tells NamoID which Hosted Auth configuration the browser wants: issuer, enabled sign-in methods, branding, access mode, and related browser-safe settings.

The key does not prove that the caller is trusted. NamoID therefore treats every request carrying it as untrusted internet traffic. Current enforcement includes:

  • An exact browser Origin allowlist.
  • Per-key minute and hour rate limits.
  • Environment scoping, so a Test key cannot act as a Live key.
  • A restricted endpoint surface that requires the publishable key type.

Origin checks are useful abuse controls, but they are not authentication. Non-browser clients can forge an Origin header. The security boundary remains the limited authority of the key itself.

That distinction also explains why a publishable key can appear in documentation and browser bundles but should not be posted casually. It is not confidential, yet copying it can still consume rate limits, create noisy telemetry, or probe public flows. Treat origin restrictions, endpoint limits, bot controls, and monitoring as abuse containment around a deliberately low-authority identifier.

Why frontend code needs a key at all

Custom and embedded authentication UIs need a deterministic way to discover the correct environment without shipping privileged credentials. A publishable key provides that lookup handle.

import { createNamoIDClient } from "@namoidhq/js";
 
const namoid = createNamoIDClient({
  publishableKey: process.env.NEXT_PUBLIC_NAMOID_PUBLISHABLE_KEY!,
});
 
const config = await namoid.auth.getConfig();

The NEXT_PUBLIC_ prefix is appropriate here because the value is intentionally browser-safe. It would be catastrophic on a secret key.

The browser can now load the environment's public auth configuration and start NamoID Auth without receiving a server credential.

What secret keys can do

A NamoID auth secret key has a namoid_auth_sk_ prefix. It is designed for a backend route, worker, or server process. It can validate access tokens and perform server-side authentication or session operations within its project environment.

import { validateAuthToken } from "@namoidhq/js/server";
 
const result = await validateAuthToken({
  token: accessToken,
  apiKey: process.env.NAMOID_AUTH_SECRET_KEY!,
});

Secret keys belong in a secrets manager or protected deployment environment. They should never appear in:

  • Client-side bundles or mobile application packages.
  • Git history, example .env files, screenshots, or support tickets.
  • URLs, analytics events, exception messages, or request logs.
  • A database in plaintext when a one-way key hash is sufficient.

NamoID shows the full secret once and stores its hash for later authentication. The prefix remains visible so operators can identify and rotate the right key without exposing it.

If your backend must call several services, do not reuse one secret everywhere. Issue a separate credential per environment and consumer when the system supports it. Independent credentials let you revoke one compromised worker without taking down the web application, and last_used_at becomes meaningful during an investigation.

Environment separation limits mistakes

Every auth key is scoped to Test or Live. The prefix encodes that environment:

namoid_auth_pk_test_...   browser-safe Test key
namoid_auth_sk_test_...   server-only Test key
namoid_auth_pk_live_...   browser-safe Live key
namoid_auth_sk_live_...   server-only Live key

This is more than naming. The backend resolves the key to a concrete environment and rejects cross-environment use. A developer can publish a Test key in a preview deployment without granting access to production users or sessions.

Projects provide a second boundary. Keys for customer-portal should not authorize calls for admin-console, even when both belong to the same tenant. Smaller blast radii make rotation and incident response practical.

Environment markers help humans, but the backend must enforce the resolved environment from the stored key record. Never authorize by parsing _test_ or _live_ from attacker-controlled text. Prefixes are diagnostics; the database association is the security boundary.

Origin controls and rate limits

Publishable keys must have allowed origins. Use exact origins, including scheme and port:

https://app.example.com
https://preview.example.com
http://localhost:3000

Do not allow *. Do not add an origin merely to silence a CORS error. Remove preview origins when those deployments disappear.

Rate limits serve a different purpose. They contain automated abuse when a public key is copied, but they should not be the only limit on an authentication endpoint. OTP, password, and waitlist operations also need per-IP and per-identity controls. The authentication rate-limiting guide explains why one global bucket is insufficient.

Mobile apps cannot keep static secrets either

A mobile binary is distributed to users, so a determined analyst can extract strings, inspect runtime memory, or intercept the app's requests. A key embedded in iOS, Android, desktop, or CLI code should therefore be treated like a publishable key unless it is provisioned dynamically into a protected device-bound mechanism.

Mobile apps also lack the reliable browser-origin signal used by web applications. Compensating controls can include attestation, proof-of-possession, device-bound keys, and aggressive per-identity limits, but none turns a shared static string inside the package into a secret.

The same rule applies to public source repositories and downloadable examples. If every installation receives the same value, assume the world can obtain it.

Rotate without an outage

Use overlapping rotation:

  1. Create a replacement key in the same environment.
  2. Deploy the replacement to every consumer.
  3. Observe the old key's last_used_at until traffic stops.
  4. Disable the old key.
  5. Delete it after a short rollback window.

For a leaked secret, skip the leisurely observation period: create, deploy, revoke, and review audit and access logs. Rotating a publishable key may reduce nuisance traffic, but it does not repair a privilege flaw because the replacement remains public.

Write the secret-key incident path before you need it:

  1. Identify the key, environment, consumers, and last known safe time.
  2. Create a replacement and deploy it through the secrets manager.
  3. Revoke the exposed key as soon as the replacement is healthy.
  4. Search key-use, authentication, and administrative events from the exposure window.
  5. Rotate any downstream credential the key could access.
  6. Fix the leak source, including logs, build artifacts, tickets, or Git history.

Deleting the line from the latest commit is not remediation. The old value may remain in clones, caches, CI logs, package archives, and screenshots.

Review checklist

Before shipping a key-based integration, ask:

  • Can this value appear in a browser without granting privileged authority?
  • Does the backend enforce key type rather than trusting its prefix?
  • Is the key bound to one project and environment?
  • Are allowed origins exact and minimal?
  • Are endpoint-specific abuse limits applied after key validation?
  • Can operators rotate the key without downtime?
  • Are full secret values excluded from logs and analytics?

If a copied publishable key can read users, validate arbitrary tokens, change settings, or create privileged sessions, it is not publishable. It is a leaked secret with optimistic branding.

NamoID exposes publishable and secret auth keys as separate types because the browser/server boundary should be enforced by the product, not remembered by every developer. Create both from the environment's API Keys page, then use the publishable key in browser SDKs and the secret key only in trusted server code.

FAQ

Is a publishable key safe to commit to Git?

It is not a secret, so exposure does not grant trusted-server authority. Still, prefer environment configuration so Test and Live values do not get mixed and rotation stays simple. Never commit a secret key, even to a private repository.

Can CORS protect a publishable key?

No. CORS controls which browser origins can read responses. A script or command-line client can send requests outside the browser and can forge the Origin header. Publishable endpoints must remain safe even when the key and requested origin are known.

Should a backend use a publishable key?

A backend may call a browser-safe endpoint with one, but it gains no trusted authority. Use an auth secret key only for documented server operations that require it, and keep that credential in a secrets manager.

Should I rotate a copied publishable key?

Rotate it when you need to contain abuse or clean up an unintended disclosure, but investigate the endpoint permissions first. If copying the key exposed privileged data or operations, the real defect is excessive authority, not insufficient secrecy.

Related posts