---
title: Best Practices
description: Security, caching, and UX patterns for OAuth 2.0 integrations.
---

## 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:

```ts
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:

```
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](/oauth/mcp-integration) for details.
