We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features
- Adding a new adapter package
Read our Code of Conduct to keep our community approachable and respectable.
In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR.
Use the table of contents icon on the top left corner of this document to get to a specific section of this guide quickly.
To get an overview of the project, read the README.
Here are some resources to help you get started with open source contributions:
- Finding ways to contribute to open source on GitHub
- Set up Git
- GitHub flow
- Collaborating with pull requests
This project is managed with pnpm workspaces. Make sure you have pnpm installed before getting started.
# Install all dependencies
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 during development
pnpm --filter @forward-software/react-auth test:watchThis is a monorepo containing two publishable packages and example applications.
| Package | Path | npm name | Description |
|---|---|---|---|
| Core library | lib/ |
@forward-software/react-auth |
Framework-agnostic auth primitives: AuthClient interface, createAuth(), AuthProvider, useAuthClient hook |
| Google Sign-In adapter | packages/google-signin/ |
@forward-software/react-auth-google |
Ready-made AuthClient implementation and GoogleSignInButton for Web and React Native |
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
The core library exposes two things from lib/src/index.ts:
createAuthfunctionAuthClienttype
Key source files:
-
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, event emission, and a refresh queue.createAuth()— creates a React context, wraps the providedAuthClient, and returns{ AuthProvider, authClient, useAuthClient }.AuthProvider— React component that callsauthClient.init()on mount and provides the auth context.useAuthClient— hook that reads from the auth context (throws if used outsideAuthProvider).
-
lib/src/utils.ts— Contains utility types:Deferred<T>andcreateEventEmitter().
This package provides a GoogleAuthClient class and a GoogleSignInButton component, with platform-specific implementations resolved at build time.
src/index.ts— Web entrysrc/index.native.ts— React Native entrysrc/types.ts— Shared typessrc/web/— Web-specific implementation (Google Identity Services)src/native/— React Native implementation (Expo native modules)
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
Report bugs using GitHub's issues
Report a bug by opening a new issue
Great Bug Reports should contain:
- A quick summary and/or background
- Steps to reproduce
- Be specific!
- Give sample code if you can; the sample code should allow anyone with a base setup to reproduce your issue
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
We use GitHub Flow, so all code changes happen through Pull Requests
We actively welcome your pull requests:
- Fork the repo and create your branch from
main. - If you've added code that should be tested, add tests.
- If you've changed APIs, update the documentation.
- Ensure the test suite passes.
- Make sure your code lints.
- Issue your pull request!
- Read and understand the relevant source in
lib/src/auth.tsxandlib/src/utils.ts. - Write or update tests in
lib/test/following existing patterns (see Testing below). - 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 should follow these conventions:
- 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. Follow thepackage.jsonscripts convention:{ "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" } } - Register the package in CI/CD and release configuration — this is critical; without it the package will not be tested, built, or published:
.github/workflows/build-test.yml— add the new package's npm name to both thetestandbuildjobmatrix.packagearrays.release-please-config.json— add an entry under"packages"with the package path (e.g.,"packages/my-adapter": {}) to enable automated versioning and npm publishing..github/dependabot.yml— add the package path to thedirectorieslist under thenpmpackage ecosystem..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).
All packages use Vitest with jsdom, @testing-library/react, and @testing-library/jest-dom.
# 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"- Test files live in a
test/directory alongsidesrc/. - File naming:
*.spec.tsor*.spec.tsx. - Tests use the Arrange / Act / Assert pattern with explicit comments.
- Use
vi.spyOn()for mocking existing methods; usevi.fn()for standalone stubs. - React components are tested with
@testing-library/react(render,act,cleanup). - When using
@testing-library/react, always callrtl.cleanupin anafterEachhook.
import { describe, it, expect, vi, afterEach } from 'vitest';
import * as rtl from '@testing-library/react';
import '@testing-library/jest-dom';
import { createAuth } from '../src';
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);
});
});
});For adapter package tests, follow these additional patterns:
- Create a
MockTokenStorageclass implementingTokenStoragewith aMap-based in-memory store. - 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. - Separate web and native tests into different files:
*.web.spec.tsand*.native.spec.ts.
This project uses ESLint to maintain a unified coding style. Before committing your changes, run pnpm -r lint and address any warnings or errors.
- TypeScript strict mode is enabled in all packages.
- Target: ES6.
- JSX transform:
react-jsx. - Follow the linter and existing file style for quote usage (single vs double).
- Export types with
export typewhen exporting only type information.
Follow this order (separated by blank lines):
- External dependencies — React, third-party libraries
- Type-only imports from external deps — using
import type { ... } - Internal value imports — from local files
- 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.
- Use
typefor object shapes, unions, and intersections:export type MyTokens = { ... } - Use
interfaceonly for contracts that classes implement:export interface TokenStorage { ... } - Place shared types in a dedicated
types.tsfile per package.
- Files: PascalCase for classes/components (
GoogleAuthClient.ts), camelCase for utilities (gsi.ts), kebab-case for test utils (test-utils.ts) - Types: PascalCase with descriptive suffixes —
GoogleAuthTokens,GoogleAuthConfig - Constants: UPPER_SNAKE_CASE —
DEFAULT_SCOPES,DEFAULT_STORAGE_KEY - Test files:
{Subject}.spec.tsor{Subject}.{platform}.spec.ts - Platform-specific files:
index.ts(web default),index.native.ts(React Native)
- Use bare
catch {}when the error is intentionally ignored (e.g., best-effort cleanup). - Use
catch (err)when the error needs to be forwarded to callbacks. - Throw
new Error('descriptive message')— never throw raw strings or objects. - Error messages must not include user credentials or token values.
- Runs on pushes to all branches except
main. - Tests each package against multiple Node.js versions:
lts/-1,lts/*,latest. - Builds each package separately.
- Runs on pushes to
main. - Uses Release Please to automate versioning and changelogs.
- Builds and publishes packages to npm.
- Configuration in
release-please-config.json.
Note: Do not manually modify
"version"fields inpackage.json— versions are managed automatically by Release Please.
This project follows Conventional Commits, which Release Please uses to determine version bumps and generate changelogs.
feat: add token expiration event # → minor bump (x.y.0)
fix: prevent duplicate refresh calls # → patch bump (x.y.z)
fix!: change onRefresh signature # → major 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 changes scoped to a specific package:
feat(google-signin): add One Tap support
fix(react-auth): handle concurrent refresh race condition
Before opening a pull request, please verify:
- ✅ 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 (see Creating or enhancing an adapter package)
A few things to keep in mind when working on this project:
- Do not modify
package.jsonversion fields — versions are managed automatically by Release Please. - 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 that requires 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; strict mode is enabled throughout. - Do not introduce new runtime dependencies unless absolutely necessary — the core lib has only one dependency (
use-sync-external-store). - Do not mix platform code — web code goes in
src/web/, native code goes insrc/native/. Shared types go insrc/types.ts. - 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').
This project handles authentication tokens and credentials. Please follow these rules:
- 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
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.
- No credential leakage in errors — error messages thrown by adapters must not include user credentials or token values.
- HTTPS only — any examples or documentation referencing API endpoints should use
https://URLs. - Nonce support — the Google adapter supports a
nonceparameter to prevent replay attacks. Preserve this feature when modifying the sign-in flow.
By contributing, you agree that your contributions will be licensed under the same MIT License that covers this project. Feel free to contact the maintainers if that's a concern.