Skip to content
better-i18n.com
On this page

Security #

  • Protect your client_secret — store it in environment variables or a secrets manager, never in source code, browser bundles, or client-side config. If leaked, contact us to rotate immediately
  • Never store tokens client-side — all tokens (client_secret, refresh_token, bi_oat_) are server-side secrets. The bi_oat_ prefix distinguishes OAuth tokens from bi_pub_ public keys
  • client_secret is required on both token exchange (grant_type=authorization_code) and refresh (grant_type=refresh_token). Requests without it return 401 Unauthorized
  • Use the narrowest scope set — users approve fewer permissions more readily
  • Handle 410 grant_revoked as a first-class state — don't show a generic error, show "Reconnect"
  • Don't retry 403 missing_scope — the user must re-authorize with wider scope

Caching #

  • Cache installation tokens in memory — 1h TTL with 60s refresh window = ~2 mints/hour
  • Don't persist installation tokens to disk — they're short-lived; caching in Redis with matching TTL is fine, Postgres is wasteful
  • Key cache by (grantId, organizationId) — a scope change re-issues a new grant

Token rotation #

  • Always persist the latest refresh_token — it may rotate on each use
  • Refresh proactively — don't wait for a 401, check expiry before each API call

Multiple organizations #

Each grant is scoped to one organization. The consent screen can create grants for multiple organizations in one approval when the user selects more than one. Store one connection row per returned grant.

To add access later:

  1. Start a new authorize flow
  2. Ask the user to select the additional organization
  3. Store the new grant_id + refresh_token

If a user re-authorizes for an org they already have a grant for, we update the existing grant in-place. Your callback should UPSERT on (userId, organizationId).

What to store #

The minimum per connection:

TypeScript
interface BetterI18nConnection {
  ownerId: string;          // Your user ID
  grantId: string;          // From token exchange
  refreshToken: string;     // Server-side only
  organizationId: string;   // From installation token response or organizations API
  scopes: string[];         // What was granted
  projectIds: string[];     // [] = all projects
  disconnectedAt?: Date;    // Set on revoke
}

// Store separately — shared across all connections:
// BETTER_I18N_CLIENT_ID    (env var)
// BETTER_I18N_CLIENT_SECRET (env var or secrets manager)

CDN delivery #

Translations published via the API are served at:

Code
https://cdn.better-i18n.com/{org}/{project}/{locale}.json

This URL is public — no auth needed. Your users' apps fetch translations from the CDN at runtime using our SDKs or a simple fetch().

MCP for AI agents #

The same bi_oat_ token works in the MCP server. If your platform has AI coding capabilities, inject the MCP config into the user's workspace after OAuth consent. See MCP Integration for details.