The Problem
Building OAuth authentication from scratch means handling token storage, session management, provider-specific quirks, PKCE flows, callback routing, and state management—all while keeping it secure and user-friendly. Most developers end up with authentication logic scattered across multiple components, duplicated code for each OAuth provider, and brittle state management that breaks on page refresh. Supabase provides OAuth infrastructure, but the official SDK still requires boilerplate: calling signInWithOAuth, handling redirects, managing loading states, error handling, and syncing auth state across components. The real challenge is encapsulating all this complexity into a single, reusable hook that works consistently across Google, GitHub, Discord, and other providers while handling edge cases like redirect loops, failed callbacks, and session persistence. This integration delivers exactly that: one custom hook that handles complete OAuth flows with consistent error handling, automatic session recovery, and provider-agnostic implementation.
Tech Stack & Prerequisites
- Node.js v20+ and npm/pnpm
- React v18+ or Next.js 14+
- TypeScript 5+
- Supabase Account (free tier available)
- @supabase/supabase-js v2.39+
- @supabase/auth-ui-react v0.4+ (optional for pre-built UI)
- OAuth Provider Credentials (Google, GitHub, Discord, etc.)
Step-by-Step Implementation
Step 1: Setup
Option A: Next.js Setup
npx create-next-app@latest supabase-oauth-hook --typescript --app --tailwind
cd supabase-oauth-hook
npm install @supabase/supabase-js @supabase/auth-helpers-nextjs
Option B: Vite + React Setup
npm create vite@latest supabase-oauth-hook -- --template react-ts
cd supabase-oauth-hook
npm install
npm install @supabase/supabase-js
Create project structure:
mkdir -p lib/supabase hooks components
touch lib/supabase/client.ts hooks/useOAuth.ts components/AuthButton.tsx
Step 2: Configuration
2.1: Create Supabase Project
- Go to supabase.com
- Create a new project
- Note your project URL and anon key from Settings → API
2.2: Configure OAuth Providers
For Google OAuth:
- Go to Google Cloud Console
- Create OAuth 2.0 credentials
- Add authorized redirect URI:
https://your-project-ref.supabase.co/auth/v1/callback - Copy Client ID and Client Secret
For GitHub OAuth:
- Go to GitHub Settings → Developer settings → OAuth Apps
- Create new OAuth app
- Authorization callback URL:
https://your-project-ref.supabase.co/auth/v1/callback - Copy Client ID and Client Secret
For Discord OAuth:
- Go to Discord Developer Portal
- Create application → OAuth2
- Add redirect:
https://your-project-ref.supabase.co/auth/v1/callback - Copy Client ID and Client Secret
2.3: Add Providers to Supabase
In Supabase Dashboard → Authentication → Providers:
- Enable Google, GitHub, Discord, or other providers
- Paste Client ID and Client Secret for each provider
- Configure redirect URLs
2.4: Environment Variables
Create .env.local (Next.js) or .env (Vite):
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
# Optional: Specify redirect URL for OAuth callbacks
NEXT_PUBLIC_SITE_URL=http://localhost:3000
Add to .gitignore:
echo ".env.local" >> .gitignore
echo ".env" >> .gitignore
Step 3: Core Logic
3.1: Supabase Client
Create lib/supabase/client.ts:
// lib/supabase/client.ts
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error('Missing Supabase environment variables');
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true,
},
});
3.2: OAuth Custom Hook
Create hooks/useOAuth.ts:
// hooks/useOAuth.ts
import { useState, useEffect, useCallback } from 'react';
import { supabase } from '@/lib/supabase/client';
import type { Provider, Session, AuthError } from '@supabase/supabase-js';
export interface OAuthOptions {
redirectTo?: string;
scopes?: string;
queryParams?: Record<string, string>;
}
export interface UseOAuthResult {
// State
session: Session | null;
user: Session['user'] | null;
loading: boolean;
error: AuthError | null;
// Actions
signInWithProvider: (provider: Provider, options?: OAuthOptions) => Promise<void>;
signOut: () => Promise<void>;
// Status checks
isAuthenticated: boolean;
isLoading: boolean;
}
export function useOAuth(): UseOAuthResult {
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<AuthError | null>(null);
/**
* Initialize session on mount
*/
useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session }, error }) => {
if (error) {
console.error('Session fetch error:', error);
setError(error);
}
setSession(session);
setLoading(false);
});
// Listen for auth state changes
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
setLoading(false);
setError(null);
});
return () => subscription.unsubscribe();
}, []);
/**
* Sign in with OAuth provider
*/
const signInWithProvider = useCallback(
async (provider: Provider, options: OAuthOptions = {}) => {
try {
setLoading(true);
setError(null);
const { error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: options.redirectTo ||
`${window.location.origin}/auth/callback`,
scopes: options.scopes,
queryParams: options.queryParams,
},
});
if (error) {
throw error;
}
// Note: User will be redirected, so this won't execute
// Loading state will be handled by redirect
} catch (err) {
console.error('OAuth sign-in error:', err);
setError(err as AuthError);
setLoading(false);
}
},
[]
);
/**
* Sign out current user
*/
const signOut = useCallback(async () => {
try {
setLoading(true);
setError(null);
const { error } = await supabase.auth.signOut();
if (error) {
throw error;
}
setSession(null);
} catch (err) {
console.error('Sign-out error:', err);
setError(err as AuthError);
} finally {
setLoading(false);
}
}, []);
return {
session,
user: session?.user ?? null,
loading,
error,
signInWithProvider,
signOut,
isAuthenticated: !!session,
isLoading: loading,
};
}
3.3: Enhanced Hook with Provider-Specific Options
Create hooks/useOAuthAdvanced.ts:
// hooks/useOAuthAdvanced.ts
import { useState, useEffect, useCallback } from 'react';
import { supabase } from '@/lib/supabase/client';
import type { Provider, Session, AuthError } from '@supabase/supabase-js';
export interface ProviderConfig {
scopes?: string;
queryParams?: Record<string, string>;
}
export interface ProviderConfigs {
google?: ProviderConfig;
github?: ProviderConfig;
discord?: ProviderConfig;
[key: string]: ProviderConfig | undefined;
}
export interface UseOAuthAdvancedOptions {
redirectTo?: string;
onSuccess?: (session: Session) => void;
onError?: (error: AuthError) => void;
providerConfigs?: ProviderConfigs;
}
export interface UseOAuthAdvancedResult {
session: Session | null;
user: Session['user'] | null;
loading: boolean;
error: AuthError | null;
signInWithProvider: (provider: Provider) => Promise<void>;
signOut: () => Promise<void>;
isAuthenticated: boolean;
isLoading: boolean;
refreshSession: () => Promise<void>;
}
export function useOAuthAdvanced(
options: UseOAuthAdvancedOptions = {}
): UseOAuthAdvancedResult {
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<AuthError | null>(null);
const {
redirectTo,
onSuccess,
onError,
providerConfigs = {},
} = options;
/**
* Get provider-specific configuration
*/
const getProviderConfig = useCallback(
(provider: Provider): ProviderConfig => {
return providerConfigs[provider] || {};
},
[providerConfigs]
);
/**
* Initialize and listen to auth state
*/
useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session }, error }) => {
if (error) {
console.error('Session fetch error:', error);
setError(error);
onError?.(error);
} else if (session) {
setSession(session);
onSuccess?.(session);
}
setLoading(false);
});
// Listen for auth state changes
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
console.log('Auth state changed:', event);
setSession(session);
setLoading(false);
if (event === 'SIGNED_IN' && session) {
onSuccess?.(session);
} else if (event === 'SIGNED_OUT') {
setSession(null);
}
setError(null);
});
return () => subscription.unsubscribe();
}, [onSuccess, onError]);
/**
* Sign in with OAuth provider
*/
const signInWithProvider = useCallback(
async (provider: Provider) => {
try {
setLoading(true);
setError(null);
const config = getProviderConfig(provider);
const { error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: redirectTo || `${window.location.origin}/auth/callback`,
scopes: config.scopes,
queryParams: config.queryParams,
},
});
if (error) {
throw error;
}
} catch (err) {
const authError = err as AuthError;
console.error('OAuth sign-in error:', authError);
setError(authError);
setLoading(false);
onError?.(authError);
}
},
[getProviderConfig, redirectTo, onError]
);
/**
* Sign out current user
*/
const signOut = useCallback(async () => {
try {
setLoading(true);
setError(null);
const { error } = await supabase.auth.signOut();
if (error) {
throw error;
}
setSession(null);
} catch (err) {
const authError = err as AuthError;
console.error('Sign-out error:', authError);
setError(authError);
onError?.(authError);
} finally {
setLoading(false);
}
}, [onError]);
/**
* Manually refresh session
*/
const refreshSession = useCallback(async () => {
try {
setLoading(true);
const { data, error } = await supabase.auth.refreshSession();
if (error) {
throw error;
}
if (data.session) {
setSession(data.session);
onSuccess?.(data.session);
}
} catch (err) {
const authError = err as AuthError;
console.error('Session refresh error:', authError);
setError(authError);
onError?.(authError);
} finally {
setLoading(false);
}
}, [onSuccess, onError]);
return {
session,
user: session?.user ?? null,
loading,
error,
signInWithProvider,
signOut,
isAuthenticated: !!session,
isLoading: loading,
refreshSession,
};
}
3.4: Auth Button Component
Create components/AuthButton.tsx:
// components/AuthButton.tsx
'use client';
import { useOAuth } from '@/hooks/useOAuth';
import type { Provider } from '@supabase/supabase-js';
interface AuthButtonProps {
provider: Provider;
children?: React.ReactNode;
className?: string;
}
export function AuthButton({ provider, children, className = '' }: AuthButtonProps) {
const { signInWithProvider, loading } = useOAuth();
const defaultLabels: Record<string, string> = {
google: 'Continue with Google',
github: 'Continue with GitHub',
discord: 'Continue with Discord',
facebook: 'Continue with Facebook',
twitter: 'Continue with Twitter',
};
const handleSignIn = async () => {
await signInWithProvider(provider);
};
return (
<button
onClick={handleSignIn}
disabled={loading}
className={`px-4 py-2 rounded font-medium transition disabled:opacity-50 ${className}`}
>
{loading ? 'Signing in...' : (children || defaultLabels[provider] || `Sign in with ${provider}`)}
</button>
);
}
3.5: Auth Callback Handler (Next.js)
Create app/auth/callback/route.ts:
// app/auth/callback/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get('code');
const next = requestUrl.searchParams.get('next') || '/dashboard';
if (code) {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (error) {
console.error('OAuth callback error:', error);
return NextResponse.redirect(new URL('/auth/error', request.url));
}
}
// Redirect to the original page or dashboard
return NextResponse.redirect(new URL(next, request.url));
}
3.6: Protected Route Component
Create components/ProtectedRoute.tsx:
// components/ProtectedRoute.tsx
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useOAuth } from '@/hooks/useOAuth';
interface ProtectedRouteProps {
children: React.ReactNode;
redirectTo?: string;
}
export function ProtectedRoute({
children,
redirectTo = '/login'
}: ProtectedRouteProps) {
const { isAuthenticated, isLoading } = useOAuth();
const router = useRouter();
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push(redirectTo);
}
}, [isAuthenticated, isLoading, redirectTo, router]);
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
</div>
);
}
if (!isAuthenticated) {
return null;
}
return <>{children}</>;
}
3.7: Login Page
Create app/login/page.tsx:
// app/login/page.tsx
'use client';
import { useOAuth } from '@/hooks/useOAuth';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
export default function LoginPage() {
const { signInWithProvider, isAuthenticated, isLoading, error } = useOAuth();
const router = useRouter();
useEffect(() => {
if (isAuthenticated) {
router.push('/dashboard');
}
}, [isAuthenticated, router]);
const providers = [
{ name: 'google', label: 'Google', icon: '🔍', color: 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50' },
{ name: 'github', label: 'GitHub', icon: '🐙', color: 'bg-gray-900 text-white hover:bg-gray-800' },
{ name: 'discord', label: 'Discord', icon: '💬', color: 'bg-indigo-600 text-white hover:bg-indigo-700' },
];
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
<div className="max-w-md w-full space-y-8">
<div className="text-center">
<h2 className="text-3xl font-bold text-gray-900">Welcome</h2>
<p className="mt-2 text-sm text-gray-600">
Sign in to your account
</p>
</div>
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded">
<p className="text-sm text-red-800">{error.message}</p>
</div>
)}
<div className="space-y-3">
{providers.map((provider) => (
<button
key={provider.name}
onClick={() => signInWithProvider(provider.name as any)}
disabled={isLoading}
className={`w-full flex items-center justify-center px-4 py-3 border rounded-md shadow-sm text-sm font-medium transition ${provider.color}`}
>
<span className="mr-2">{provider.icon}</span>
Continue with {provider.label}
</button>
))}
</div>
<div className="mt-6 text-center">
<p className="text-xs text-gray-500">
By signing in, you agree to our Terms of Service and Privacy Policy
</p>
</div>
</div>
</div>
);
}
3.8: Dashboard Page
Create app/dashboard/page.tsx:
// app/dashboard/page.tsx
'use client';
import { useOAuth } from '@/hooks/useOAuth';
import { ProtectedRoute } from '@/components/ProtectedRoute';
export default function DashboardPage() {
const { user, signOut, isLoading } = useOAuth();
if (isLoading) {
return <div>Loading...</div>;
}
return (
<ProtectedRoute>
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-4xl mx-auto px-4">
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
<button
onClick={signOut}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition"
>
Sign Out
</button>
</div>
<div className="space-y-4">
<div>
<h2 className="text-lg font-semibold mb-2">Profile Information</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-600">Email</p>
<p className="font-medium">{user?.email}</p>
</div>
<div>
<p className="text-sm text-gray-600">User ID</p>
<p className="font-mono text-sm">{user?.id}</p>
</div>
<div>
<p className="text-sm text-gray-600">Provider</p>
<p className="font-medium capitalize">
{user?.app_metadata?.provider || 'N/A'}
</p>
</div>
<div>
<p className="text-sm text-gray-600">Created At</p>
<p className="text-sm">
{user?.created_at ? new Date(user.created_at).toLocaleDateString() : 'N/A'}
</p>
</div>
</div>
</div>
{user?.user_metadata && Object.keys(user.user_metadata).length > 0 && (
<div>
<h2 className="text-lg font-semibold mb-2">Additional Data</h2>
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto">
{JSON.stringify(user.user_metadata, null, 2)}
</pre>
</div>
)}
</div>
</div>
</div>
</div>
</ProtectedRoute>
);
}
Step 4: Testing
4.1: Start Development Server
npm run dev
Visit http://localhost:3000/login
4.2: Test OAuth Flow
Step 1: Click Provider Button
- Click “Continue with Google” (or other provider)
- You’ll be redirected to provider’s login page
Step 2: Authorize
- Sign in to your provider account
- Authorize the application
- You’ll be redirected back to
/auth/callback
Step 3: Verify Session
- Should redirect to
/dashboard - Check user information is displayed
- Verify session persists on page refresh
4.3: Test Hook Directly
Create a test component:
// components/AuthTest.tsx
'use client';
import { useOAuth } from '@/hooks/useOAuth';
export function AuthTest() {
const { session, user, loading, error, isAuthenticated } = useOAuth();
return (
<div className="p-4 bg-gray-100 rounded">
<h3 className="font-bold mb-2">Auth State:</h3>
<pre className="text-xs">
{JSON.stringify(
{
isAuthenticated,
loading,
userId: user?.id,
email: user?.email,
error: error?.message,
},
null,
2
)}
</pre>
</div>
);
}
4.4: Test Sign Out
In dashboard:
- Click “Sign Out” button
- Should redirect to login page
- Session should be cleared
- Attempting to access
/dashboardshould redirect to/login
4.5: Test Session Persistence
- Sign in successfully
- Refresh the page
- Should remain authenticated
- Close and reopen browser
- Should still be authenticated (session persisted in localStorage)
4.6: Test Multiple Providers
Test each provider separately:
# Open different browser profiles or use incognito
# Test Google
# Test GitHub
# Test Discord
```
Verify each provider returns correct user metadata.
## Common Errors & Troubleshooting
### 1. **Error: "Invalid redirect URL" or callback fails**
**Cause:** Redirect URL in Supabase doesn't match the actual callback URL, or wildcard patterns are misconfigured.
**Fix:** Ensure exact URL match in Supabase Dashboard:
In Supabase Dashboard → Authentication → URL Configuration:
```
Site URL: http://localhost:3000
Redirect URLs:
http://localhost:3000/auth/callback
https://yourdomain.com/auth/callback
For Next.js, ensure callback route exists:
// Verify app/auth/callback/route.ts exists and exports GET handler
export async function GET(request: NextRequest) {
// Must handle code exchange
const code = requestUrl.searchParams.get('code');
if (!code) {
return NextResponse.redirect(new URL('/auth/error?message=no_code', request.url));
}
// ... rest of handler
}
```
For development with multiple ports:
```
http://localhost:3000/auth/callback
http://localhost:3001/auth/callback
http://127.0.0.1:3000/auth/callback
2. Error: “Session not persisting” or user logged out on refresh
Cause: Supabase client not configured for session persistence, or localStorage is blocked/cleared.
Fix: Ensure proper client configuration and storage:
// lib/supabase/client.ts
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
autoRefreshToken: true, // CRITICAL: Auto-refresh tokens
persistSession: true, // CRITICAL: Persist to localStorage
detectSessionInUrl: true, // Detect session from URL params
storage: typeof window !== 'undefined' ? window.localStorage : undefined,
},
});
For Next.js with SSR, use separate clients:
// lib/supabase/server.ts (for Server Components)
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export function createClient() {
const cookieStore = cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
},
}
);
}
Check for storage blockers:
// In useOAuth hook, add storage check
useEffect(() => {
// Test localStorage availability
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
} catch (e) {
console.error('localStorage not available:', e);
setError({ message: 'Please enable cookies and local storage' } as AuthError);
}
}, []);
3. Error: Provider-specific errors (e.g., “invalid_scope” for Google)
Cause: Requesting scopes that aren’t enabled in the OAuth provider’s configuration, or provider requires specific parameters.
Fix: Use provider-specific configurations:
// For Google - request additional scopes
await signInWithProvider('google', {
scopes: 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile',
});
// For GitHub - request specific permissions
await signInWithProvider('github', {
scopes: 'read:user user:email',
});
// For Discord - request guilds
await signInWithProvider('discord', {
scopes: 'identify email guilds',
});
Create provider configurations in advanced hook:
const { signInWithProvider } = useOAuthAdvanced({
providerConfigs: {
google: {
queryParams: {
access_type: 'offline', // Request refresh token
prompt: 'consent', // Force consent screen
},
},
github: {
scopes: 'read:user user:email',
},
discord: {
queryParams: {
permissions: '0', // Discord-specific permissions
},
},
},
});
Verify provider credentials in Supabase:
// Test OAuth config
const testOAuthConfig = async (provider: string) => {
try {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: provider as Provider,
options: {
redirectTo: window.location.origin + '/test',
skipBrowserRedirect: true, // Don't redirect, just test config
},
});
console.log(`${provider} config:`, data);
if (error) console.error(`${provider} error:`, error);
} catch (e) {
console.error(`${provider} failed:`, e);
}
};
Security Checklist
- Use HTTPS in production – OAuth requires secure transport for tokens
- Validate redirect URLs – Only allow whitelisted callback URLs in Supabase
- Never expose service role key – Use anon key only in client-side code
- Implement PKCE flow – Supabase handles this automatically, ensure it’s not disabled
- Set short token expiration – Configure in Supabase Dashboard → Authentication → Settings
- Implement token refresh – Hook already handles this via
autoRefreshToken: true - Validate session server-side – Don’t trust client session, verify with Supabase on server
- Use secure cookie settings in production (httpOnly, secure, sameSite)
- Implement rate limiting on auth endpoints to prevent abuse
- Log authentication events for security monitoring
- Revoke tokens on sign-out – Hook calls
signOut()which revokes tokens - Store sensitive data server-side – Never store secrets in localStorage
- Implement CORS properly – Restrict origins to your domains only
- Use state parameter – Supabase handles CSRF protection automatically
- Monitor for suspicious activity – Track failed login attempts
- Implement session timeout for inactive users
- Validate user email – Check
email_verifiedfield before granting access - Use Row Level Security – Protect Supabase data with RLS policies
- Rotate OAuth secrets regularly in provider dashboards
Related Resources
For complete authentication infrastructure with multi-factor authentication, see our guide on implementing MFA with Auth0 and NextAuth covering TOTP and SMS verification.
When building secure API endpoints that require authenticated requests, check our tutorial on securing APIs with OAuth 2.0 and JWT for token validation patterns.
For implementing role-based access control after authentication, explore our Discord + Stripe integration guide covering membership tier automation.
When designing scalable user management systems, read our article on designing REST APIs for SaaS applications with authentication best practices.
For teams comparing authentication providers, our Auth0 SSO implementation guide helps evaluate enterprise OAuth solutions.

Finly Insights Team is a group of software developers, cloud engineers, and technical writers with real hands-on experience in the tech industry. We specialize in cloud computing, cybersecurity, SaaS tools, AI automation, and API development. Every article we publish is thoroughly researched, written, and reviewed by people who have actually worked in these fields.




