Instructions for AI coding agents working on the
react-authmonorepo.
This is a monorepo for React Auth, a library that simplifies authentication flows in React and React Native apps. It is managed with pnpm workspaces and contains two publishable packages plus example apps.
| Package | Path | npm name | Description |
|---|---|---|---|
| Core library | lib/ |
@forward-software/react-auth |
Framework-agnostic auth primitives: AuthClient interface, createAuth(), AuthProvider, useAuthClient hook, EnhancedAuthClient wrapper with event emitter and state management |
| Google Sign-In adapter | packages/google-signin/ |
@forward-software/react-auth-google |
Ready-made AuthClient implementation and GoogleSignInButton for Web (Google Identity Services) and React Native (Expo native module) |
| Apple Sign-In adapter | packages/apple-signin/ |
@forward-software/react-auth-apple |
Ready-made AuthClient implementation and AppleSignInButton for Web (Sign in with Apple JS) and React Native (Expo native module) |
Located in examples/. These are not published — they exist for documentation and manual testing only.
examples/base/— minimal Vite + React exampleexamples/reqres/— authenticates against the ReqRes APIexamples/refresh-token/— demonstrates token refresh with Axios interceptorsexamples/expo/— React Native (Expo) integration
# Install all dependencies (use frozen lockfile for CI-like behavior)
pnpm install
# Build all packages
pnpm -r build
# Run all tests
pnpm -r test
# Lint all packages
pnpm -r lint
# Clean build outputs
pnpm -r clean# Build a specific package
pnpm --filter @forward-software/react-auth build
pnpm --filter @forward-software/react-auth-google build
# Test a specific package
pnpm --filter @forward-software/react-auth test
pnpm --filter @forward-software/react-auth-google test
# Watch mode for tests (useful during development)
pnpm --filter @forward-software/react-auth test:watchThe pnpm-workspace.yaml defines workspace members as lib and packages/*. The catalog: protocol in pnpm-workspace.yaml pins shared dependency versions (React, TypeScript, Vite, Vitest, etc.) across all packages.
The core library exposes two things from lib/src/index.ts:
createAuthfunctionAuthClienttype
-
lib/src/auth.tsx— Contains all core logic:AuthClient<T, C>interface — the contract adapters must implement. OnlyonLogin()is required; all other lifecycle hooks (onInit,onPostInit,onPreLogin,onPostLogin,onPreRefresh,onRefresh,onPostRefresh,onPreLogout,onLogout,onPostLogout) are optional.AuthClientEnhancementsclass — wraps anAuthClientwith state management (isInitialized,isAuthenticated,tokens), event emission (on/off/emitfor init/login/refresh/logout events),useSyncExternalStoreintegration (subscribe/getSnapshot), and a refresh queue that deduplicates concurrent refresh calls.wrapAuthClient()— usesObject.setPrototypeOfto merge the enhancement class with the originalAuthClientinstance, producing anEnhancedAuthClient.createAuth()— creates a React context, wraps the providedAuthClient, and returns{ AuthProvider, authClient, useAuthClient }.AuthProvider— React component that callsauthClient.init()on mount, shows optionalLoadingComponent/ErrorComponent, and provides the auth context to children.useAuthClient— hook that reads from the auth context (throws if used outsideAuthProvider).
-
lib/src/utils.ts— Contains:Deferred<T>— Promise wrapper used for the refresh queue.createEventEmitter()— simple typed event emitter (on/off/emit).
- The library targets ES6 and uses
react-jsxJSX transform. use-sync-external-store/shimis the only runtime dependency (peer dependency isreact >= 16.8).- TypeScript strict mode is enabled.
- The
EnhancedAuthClienttype isAC & AuthClientEnhancements<AC, E>— it preserves the original client's type while adding enhanced properties/methods.
This package provides a GoogleAuthClient class (implements AuthClient) and a GoogleSignInButton component, with platform-specific implementations resolved at build/bundle time.
src/index.ts— Web entry: re-exports fromsrc/web/src/index.native.ts— React Native entry: re-exports fromsrc/native/- Both export:
GoogleAuthClient,GoogleSignInButton, and all types fromsrc/types.ts
src/types.ts— Shared types:GoogleAuthTokens,GoogleAuthCredentials,TokenStorageinterface,GoogleAuthConfig,GoogleWebAuthConfig,GoogleNativeAuthConfig.src/web/GoogleAuthClient.ts— Web implementation using Google Identity Services (GSI). UseslocalStorageby default for token persistence. Parses JWTexpclaim to track expiration.src/native/GoogleAuthClient.ts— React Native implementation using Expo native modules. Requires externalstorage(e.g., MMKV). Supports silent sign-in for token refresh.src/web/GoogleSignInButton.tsx— Renders Google's official GSI button on web.src/native/GoogleSignInButton.tsx— Native sign-in button component.src/native/GoogleSignInModule.ts— Expo module bridge (calls into native Swift/Kotlin code).src/web/gsi.ts— Low-level GSI script loading and initialization utilities, exposed as a separate export (@forward-software/react-auth-google/web/gsi).
The package.json uses the "react-native" field and conditional "exports" to let bundlers resolve the correct entry point:
{
"main": "dist/index.js",
"react-native": "dist/index.native.js",
"exports": {
".": {
"react-native": "./dist/index.native.js",
"default": "./dist/index.js"
}
}
}- iOS:
ios/GoogleSignInModule.swift— Swift implementation using Apple's Authentication Services. - Android:
android/src/main/java/expo/modules/googlesignin/GoogleSignInModule.kt— Kotlin implementation using Android Credential Manager. - Configured via
expo-module.config.jsonfor Expo autolinking.
All packages use Vitest with jsdom environment, @testing-library/react, and @testing-library/jest-dom.
Vitest config (identical in both packages):
{
environment: "jsdom",
globals: true,
include: ["**/*.{test,spec}.{js,jsx,ts,tsx}"],
}- Test files live in a
test/directory alongsidesrc/. - File naming:
*.spec.tsor*.spec.tsx. - Tests use the Arrange / Act / Assert pattern (with explicit comments).
- Mock auth clients are defined in
test/test-utils.tsx(core lib) andtest/test-utils.ts(google-signin). - Use
vi.spyOn()for mocking,vi.fn()for stubs. - React components are tested with
@testing-library/react(render,act,cleanup). - Always call
rtl.cleanupinafterEach.
test/authClient.spec.ts— Unit tests forEnhancedAuthClient(init, login, refresh, logout lifecycle events and hooks).test/context.spec.ts— Tests for the React context (useAuthClienthook behavior).test/provider.spec.tsx— Tests forAuthProvider(initialization, loading/error components, auth state propagation).test/test-utils.tsx—MockAuthClientclass,createMockAuthClient(),createMockAuthClientWithHooks(),createChild()helper,flushPromises().
test/GoogleAuthClient.web.spec.ts— Web adapter tests (token persistence, login, logout, expiration handling).test/GoogleAuthClient.native.spec.ts— Native adapter tests.test/test-utils.ts—MockTokenStorageclass,createMockIdToken(),createExpiredMockIdToken().
# Run all tests
pnpm -r test
# Run tests for a specific package
pnpm --filter @forward-software/react-auth test
pnpm --filter @forward-software/react-auth-google test
# Run a specific test file
cd lib && pnpm vitest run test/authClient.spec.ts
cd packages/google-signin && pnpm vitest run test/GoogleAuthClient.web.spec.ts
# Run a specific test by name
cd lib && pnpm vitest run -t "should notify success"- TypeScript strict mode in all packages.
- Target: ES6.
- JSX transform:
react-jsx(noimport Reactneeded in JSX files, but the core lib does import React explicitly). - Module resolution:
node. - Linting via ESLint:
pnpm --filter <package> lint. - No Prettier config at root — follow existing formatting conventions in each file.
- Use single quotes for strings (following existing code style).
- Export types with
export typewhen exporting only type information.
Follow this order (separated by blank lines where shown in existing code):
- External dependencies — React, third-party libraries (e.g.,
react,expo-modules-core,use-sync-external-store) - Type-only imports from external deps — using
import type { ... }(e.g.,import type { PropsWithChildren } from 'react') - Internal value imports — from
../types,./utils,./gsi, etc. - Internal type-only imports — using
import type { ... }from local files
// ✅ Correct
import React, { useEffect, useRef, useCallback } from 'react';
import type { GoogleAuthCredentials, GoogleWebAuthConfig } from '../types';
import { loadGsiScript, initializeGsi, renderGsiButton } from './gsi';
import type { GsiButtonConfig } from './gsi';Always use import type for imports that are only used as types — never import a type with a regular import if it's not used as a value.
- Use
typefor object shapes, unions, and intersections:export type MyTokens = { ... } - Use
interfaceonly for contracts that classes implement:export interface TokenStorage { ... } - Prefer
typeoverinterfacewhen not implementing with a class - Export types directly from the file where they are defined — re-export from
index.tsusingexport * from './types' - Place shared types in a dedicated
types.tsfile per package
AuthClientimplementations should be classes (not plain objects) for adapter packages- Private fields use the
privatekeyword (not#private fields) - Constructor should apply defaults using spread:
this.config = { scopes: DEFAULT_SCOPES, ...config } - Config, storage, and storageKey are
privatereadonly fields set in the constructor
- Use bare
catch {}(without binding the error) when the error is intentionally ignored (e.g., best-effort cleanup like GSI revoke) - Use
catch (err)when the error needs to be forwarded (e.g., toonErrorcallbacks) - Throw
new Error('descriptive message')— never throw raw strings or objects - Error messages should describe what went wrong and what the caller should do, without including sensitive data
- Files: PascalCase for classes/components (
GoogleAuthClient.ts,GoogleSignInButton.tsx), camelCase for utilities (gsi.ts,utils.ts), kebab-case for test utils (test-utils.ts) - Types: PascalCase with descriptive suffixes —
GoogleAuthTokens,GoogleAuthCredentials,GoogleWebAuthConfig,TokenStorage - Constants: UPPER_SNAKE_CASE —
DEFAULT_SCOPES,DEFAULT_STORAGE_KEY - Test files:
{Subject}.spec.tsor{Subject}.{platform}.spec.ts(e.g.,GoogleAuthClient.web.spec.ts) - Platform-specific files:
index.ts(web default),index.native.ts(React Native)
import { describe, it, expect, vi, afterEach } from 'vitest';
import * as rtl from '@testing-library/react';
import '@testing-library/jest-dom';
// Import from src
import { createAuth } from '../src';
// Import test utilities
import { createMockAuthClient } from './test-utils';
afterEach(rtl.cleanup);
describe('FeatureName', () => {
describe('scenario', () => {
it('should do something specific', async () => {
// Arrange
const mock = createMockAuthClient();
vi.spyOn(mock, 'onInit').mockResolvedValue(null);
// Act
await rtl.act(async () => {
// ... trigger the action
});
// Assert
expect(mock.onInit).toHaveBeenCalledTimes(1);
});
});
});- Always use explicit
// Arrange,// Act,// Assertcomments - Always call
afterEach(rtl.cleanup)at the top level of the test file - Always wrap async React operations in
rtl.act(async () => { ... }) - Never test implementation details — test behavior (events emitted, state changes, rendered output)
- Never import from
dist/— always import from../srcor../src/auth - Mock only what you own — mock
AuthClientmethods, not React internals or library code - Use
vi.spyOn(object, 'method')to spy on existing methods; usevi.fn()for standalone stubs - Use
mockResolvedValue/mockResolvedValueOncefor async mocks,mockReturnValuefor sync - Test both success and failure paths for each lifecycle method (init, login, refresh, logout)
- Test that lifecycle hooks (onPreLogin, onPostLogin, etc.) are called in the correct order
- Test event emissions (e.g.,
loginStarted,loginSuccess,loginFailed) via.on()subscriptions
For adapter package tests, follow these additional patterns:
- Create a
MockTokenStorageclass implementingTokenStoragewith aMap-based in-memory store and aclear()method for test cleanup - Create helper functions to generate mock tokens (e.g.,
createMockIdToken(claims),createExpiredMockIdToken()) - Test token persistence: verify tokens are stored after login and cleared after logout
- Test token restoration: verify
onInit()restores valid tokens and rejects expired ones - Test with and without
persistTokensoption - Separate web and native tests into different files:
*.web.spec.tsand*.native.spec.ts
- Read and understand the relevant source in
lib/src/auth.tsxandlib/src/utils.ts. - Write or update tests in
lib/test/following existing patterns (Arrange/Act/Assert, usecreateMockAuthClient). - Run
pnpm --filter @forward-software/react-auth testand ensure all tests pass. - Run
pnpm --filter @forward-software/react-auth buildto verify the build succeeds. - Run
pnpm --filter @forward-software/react-auth lintto check for lint errors.
Adapter packages live under packages/ and must:
- Implement the
AuthClientinterface from@forward-software/react-auth. At minimum, implementonLogin(). Optionally implementonInit,onLogout,onRefresh, and lifecycle hooks. - Support platform-specific entry points if targeting both web and React Native:
src/index.ts— web entry, re-exports fromsrc/web/src/index.native.ts— React Native entry, re-exports fromsrc/native/- Configure
"main","react-native", and"exports"inpackage.json
- Define shared types in a
src/types.tsfile (tokens, credentials, config, storage interface). - Provide a UI component (e.g.,
SignInButton) for both platforms if applicable. - Add
@forward-software/react-authas both adevDependencyand apeerDependency. - Write tests in a
test/directory with platform-specific spec files (e.g.,*.web.spec.ts,*.native.spec.ts). Create mock utilities intest/test-utils.ts. - Use the same build tooling: TypeScript compilation with
tsc, Vitest for testing, sametsconfig.jsonstructure. - Register the package in CI/CD and release configuration (critical — the package will not be tested or published otherwise):
pnpm-workspace.yaml— already covered by thepackages/*glob, no action needed unless the package is outsidepackages/..github/workflows/build-test.yml— add the new package's npm name to both thetestandbuildjobmatrix.packagearrays so it is tested and built in CI.release-please-config.json— add an entry under"packages"with the package path (e.g.,"packages/my-adapter": {}) to enable automated versioning, changelog generation, and npm publishing via the release workflow..github/dependabot.yml— add the package path to thedirectorieslist under thenpmpackage ecosystem so its dependencies are monitored for updates..github/ISSUE_TEMPLATE/bug_report.yml— add the new package name to the "Which package is affected?" dropdown options..github/ISSUE_TEMPLATE/feature_request.yml— add the new package name to the "Which package is this for?" dropdown options..github/CODEOWNERS— add a rule for the new package path with the appropriate owner(s).
- Update documentation:
README.md— add the new package to the Packages table (with npm badge and description).SECURITY.md— add the new package and its supported version to the Supported Versions table.CONTRIBUTING.md— update any section that lists existing packages (e.g., architecture overview, examples).AGENTS.md— update the Project overview packages table and any architecture sections that reference existing packages.- Create a
README.mdin the package directory with install instructions, quick start, API reference, and consistent badges/footer (follow the structure ofpackages/google-signin/README.md).
{
"scripts": {
"build:code": "tsc --removeComments",
"build:types": "tsc --declaration --emitDeclarationOnly",
"build": "npm-run-all clean build:*",
"lint": "eslint src",
"test": "vitest",
"test:watch": "vitest watch",
"clean": "rimraf dist"
}
}Important: When adding a new package, you must update the GitHub Actions workflows and release configuration. Without this, the package will not be tested, built, or published. See the checklist in "How to implement or enhance an adapter package" step 8 above.
- Runs on pushes to all branches except
main. - Tests each package against Node.js matrix:
lts/-1,lts/*,latest. - Builds each package separately.
- Uses
pnpm i --frozen-lockfilethenpnpm install --no-frozen-lockfile --config.auto-install-peers=truefor peer dependencies.
- Runs on pushes to
mainand weekly on Tuesday evenings. - Uses Release Please to automate versioning and changelogs.
- Builds and publishes to npm with provenance (
id-token: write). - Configuration in
release-please-config.json.
- Run
pnpm --filter <package> lintandpnpm --filter <package> testbefore committing. - Ensure
pnpm --filter <package> buildsucceeds. - Add or update tests for any code changes.
- Follow Conventional Commits for commit messages (used by Release Please for changelog generation).
- See CONTRIBUTING.md for full contribution guidelines.
Release Please uses commit messages to determine version bumps and generate changelogs. Use these prefixes:
feat: add token expiration event # → minor version bump (0.x.0)
fix: prevent duplicate refresh calls # → patch version bump (0.0.x)
fix!: change onRefresh signature # → major version bump (x.0.0) — breaking change
chore: update dev dependencies # → no release
docs: update README examples # → no release
test: add missing logout tests # → no release
refactor: extract token validation logic # → no release
For scoped changes, include the package scope:
feat(google-signin): add One Tap support
fix(react-auth): handle concurrent refresh race condition
- ✅ Code compiles:
pnpm --filter <package> build - ✅ Linting passes:
pnpm --filter <package> lint - ✅ All tests pass:
pnpm --filter <package> test - ✅ New tests added for new/changed code
- ✅ No
console.logor debug statements left in source code - ✅ No tokens, credentials, or secrets in error messages
- ✅ Commit message follows Conventional Commits format
- ✅ If adding a new package: CI workflows and release config updated
Avoid these mistakes that agents frequently make:
- Do not modify
package.jsonversion fields — versions are managed automatically by Release Please. Never manually bump"version". - Do not add
node_modulesordistto commits — these are in.gitignore. - Do not break the
AuthClientinterface — adding optional methods is fine; changing the signature ofonLoginor removing methods is a breaking change requiring afeat!:orfix!:commit. - Do not add React as a dependency — it must remain a
peerDependency. The same applies toexpo-modules-coreandreact-nativein adapter packages. - Do not use
anyin TypeScript — use proper types or generics. The codebase uses strict mode. - Do not introduce new runtime dependencies unless absolutely necessary — the core lib has only one dependency (
use-sync-external-store). Prefer zero-dependency implementations. - Do not mix platform code — web code goes in
src/web/, native code goes insrc/native/. Shared types go insrc/types.ts. Never import fromreact-nativein web files or from browser APIs in native files. - Do not skip the build step —
pnpm buildmust succeed because the published package usesdist/, notsrc/. - Do not use relative imports crossing package boundaries — always use the npm package name (e.g.,
import { createAuth } from '@forward-software/react-auth', notimport { createAuth } from '../../lib/src').
This project handles authentication tokens and credentials. Follow these rules when making changes:
- Never log or expose tokens — do not add
console.log, debug logging, or error messages that include token values, credentials, or secrets. - JWT parsing is read-only — the
base64UrlDecode/expextraction inGoogleAuthClientis used only to check expiration. Never modify JWT contents or attempt to forge tokens. - Token storage — tokens may be persisted via the
TokenStorageinterface (localStorage on web, MMKV or AsyncStorage on React Native). Never store tokens in cookies, URL parameters, or global variables. - Validate at boundaries — when processing external input (credentials from sign-in flows, tokens from storage), validate the shape before using it (e.g., check
idTokenexists before accessing it). - No credential leakage in errors — error messages thrown by adapters must not include user credentials or token values. Use generic messages like
"credentials with idToken are required". - HTTPS only — any examples or documentation referencing API endpoints should use
https://URLs. - Nonce support — the Google adapter supports a
nonceparameter to bind ID tokens to a session and prevent replay attacks. Preserve this feature when modifying the sign-in flow. - Peer dependency ranges — when updating dependency versions, check for known security vulnerabilities. Do not pin to versions with known CVEs.