Implementing Google Login with Bosbase JS SDK in Next.js
This guide provides a comprehensive walkthrough for implementing Google OAuth2 authentication in a Next.js application using Bosbase's JavaScript SDK.
Table of Contents
- Overview
- Prerequisites
- How It Works
- Backend Configuration
- Next.js Setup
- Implementation Guide
- Complete Code Examples
- Best Practices
- Troubleshooting
Overview
Bosbase provides OAuth2 authentication through its JavaScript SDK, which simplifies the integration of third-party authentication providers like Google. The SDK handles the OAuth2 flow using a realtime subscription mechanism that eliminates the need for custom redirect handlers or page reloads.
Key Features
- Popup-based authentication: Opens a popup window for OAuth2 login without page reload
- Automatic token management: SDK automatically manages authentication tokens
- Realtime subscription: Uses WebSocket-based realtime subscriptions for seamless OAuth2 callback handling
- PKCE support: Google OAuth2 uses PKCE (Proof Key for Code Exchange) for enhanced security
Prerequisites
Before you begin, ensure you have:
- Bosbase instance running and accessible
- Next.js application (version 13+ recommended for App Router support)
- Google Cloud Console account with OAuth2 credentials
- Auth collection configured in your Bosbase instance
- Bosbase JS SDK installed in your Next.js project
Installing the SDK
npm install bosbase
# or
yarn add bosbase
# or
pnpm add bosbaseHow It Works
OAuth2 Flow Overview
Bosbase simplifies OAuth2 authentication by handling the entire flow automatically. Here's what happens when a user clicks "Sign in with Google":
- User clicks login button: Your Next.js app calls
authWithOAuth2()withprovider: 'google' - SDK opens popup: A popup window opens with Google's OAuth2 consent page
- User authenticates: User signs in with their Google account and grants permissions
- Automatic callback handling: Bosbase receives the OAuth2 callback and processes it automatically
- User record created/updated: Bosbase automatically creates a new user or links to an existing account
- Authentication complete: The SDK receives the auth token and user data, which are stored automatically
What You Need to Know
- No redirect handlers needed: The SDK uses WebSocket-based realtime subscriptions to handle callbacks automatically
- Popup-based flow: Authentication happens in a popup window, so your main page doesn't reload
- Automatic token management: The SDK stores and manages authentication tokens for you
- PKCE security: Google OAuth2 uses PKCE (Proof Key for Code Exchange) for enhanced security - this is handled automatically
Backend Configuration
Before you can use Google login in your Next.js app, you need to configure OAuth2 in two places:
Step 1: Configure Google OAuth2 in Google Cloud Console
- Go to Google Cloud Console
- Create a new project or select an existing one
- Enable the Google+ API
- Go to Credentials → Create Credentials → OAuth 2.0 Client ID
- Configure the OAuth consent screen (if you haven't already)
- Set the Authorized redirect URIs to:
https://your-bosbase-domain.com/api/oauth2-redirectImportant: Replace
your-bosbase-domain.comwith your actual Bosbase instance domain - Copy the Client ID and Client Secret - you'll need these in the next step
Step 2: Configure OAuth2 in Bosbase
Configure Google OAuth2 in your Bosbase collection through the Admin UI:
-
Log in to your Bosbase Admin UI
-
Navigate to your auth collection (e.g., "users")
-
Go to Settings → OAuth2
-
Enable OAuth2 if it's not already enabled
-
Add a new provider:
- Provider name:
google - Client ID: Paste your Google OAuth2 Client ID
- Client Secret: Paste your Google OAuth2 Client Secret
- The OAuth2 URLs are pre-configured for Google
- PKCE: Should be enabled (required for Google)
- Provider name:
-
Configure field mappings (optional):
- Map Google's user data fields to your collection fields
- Common mappings:
name→name,email→email,picture→avatar
Note: If you prefer using the Admin API, refer to the OAuth2 Configuration Guide for API-based setup.
Next.js Setup
Project Structure
For a Next.js App Router application, your structure might look like:
app/
├── layout.tsx
├── page.tsx
├── login/
│ └── page.tsx
├── dashboard/
│ └── page.tsx
└── api/
└── auth/
└── callback/
└── route.ts
lib/
└── bosbase.tsEnvironment Variables
Create a .env.local file:
NEXT_PUBLIC_BOSBASE_URL=https://your-bosbase-instance.com
NEXT_PUBLIC_BOSBASE_COLLECTION=usersImplementation Guide
Step 1: Initialize Bosbase Client
Create a Bosbase client instance that can be reused across your application:
lib/bosbase.ts
import BosBase from 'bosbase';
// Create a singleton instance
let pb: BosBase | null = null;
export function getBosbaseClient(): BosBase {
if (!pb) {
const baseURL = process.env.NEXT_PUBLIC_BOSBASE_URL || '';
if (!baseURL) {
throw new Error('NEXT_PUBLIC_BOSBASE_URL is not set');
}
pb = new BosBase(baseURL);
}
return pb;
}
// For client-side usage
export function getClientSideBosbase(): BosBase {
if (typeof window === 'undefined') {
throw new Error('This function can only be called on the client side');
}
return getBosbaseClient();
}Step 2: Create a Login Component
Create a client component for handling Google login:
components/GoogleLoginButton.tsx
'use client';
import { useState } from 'react';
import { getClientSideBosbase } from '@/lib/bosbase';
import { useRouter } from 'next/navigation';
export default function GoogleLoginButton() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const handleGoogleLogin = async () => {
setLoading(true);
setError(null);
try {
const pb = getClientSideBosbase();
const collectionName = process.env.NEXT_PUBLIC_BOSBASE_COLLECTION || 'users';
// Check if OAuth2 is available
const authMethods = await pb.collection(collectionName).listAuthMethods();
if (!authMethods.oauth2?.enabled) {
throw new Error('OAuth2 is not enabled for this collection');
}
const googleProvider = authMethods.oauth2.providers.find(
(p) => p.name === 'google'
);
if (!googleProvider) {
throw new Error('Google OAuth2 is not configured');
}
// Authenticate with Google
// This opens a popup window and handles the OAuth2 flow automatically
const authData = await pb.collection(collectionName).authWithOAuth2({
provider: 'google',
createData: {
// Optional: additional data for new users
emailVisibility: false,
},
});
// Check if this is a new user
if (authData.meta?.isNew) {
console.log('New user registered:', authData.record);
// Redirect to onboarding or welcome page
router.push('/onboarding');
} else {
console.log('User logged in:', authData.record);
// Redirect to dashboard
router.push('/dashboard');
}
} catch (err: any) {
console.error('Google login failed:', err);
setError(
err.message || 'Failed to authenticate with Google. Please try again.'
);
} finally {
setLoading(false);
}
};
return (
<div>
<button
onClick={handleGoogleLogin}
disabled={loading}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
{loading ? 'Signing in...' : 'Sign in with Google'}
</button>
{error && (
<p className="mt-2 text-red-500 text-sm">{error}</p>
)}
</div>
);
}Step 3: Handle Authentication State
Create a context or hook to manage authentication state:
lib/auth-context.tsx
'use client';
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { getClientSideBosbase } from './bosbase';
import { RecordModel } from 'bosbase';
interface AuthContextType {
user: RecordModel | null;
loading: boolean;
isAuthenticated: boolean;
logout: () => void;
refreshAuth: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<RecordModel | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkAuth();
}, []);
const checkAuth = async () => {
try {
const pb = getClientSideBosbase();
// Check if user is authenticated
if (pb.authStore.isValid && pb.authStore.record) {
// Verify token is still valid
try {
const collectionName = process.env.NEXT_PUBLIC_BOSBASE_COLLECTION || 'users';
await pb.collection(collectionName).authRefresh();
setUser(pb.authStore.record);
} catch (err) {
// Token expired or invalid
pb.authStore.clear();
setUser(null);
}
} else {
setUser(null);
}
} catch (err) {
console.error('Auth check failed:', err);
setUser(null);
} finally {
setLoading(false);
}
};
const logout = () => {
const pb = getClientSideBosbase();
pb.authStore.clear();
setUser(null);
};
const refreshAuth = async () => {
await checkAuth();
};
return (
<AuthContext.Provider
value={{
user,
loading,
isAuthenticated: !!user,
logout,
refreshAuth,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}Step 4: Protect Routes
Create a component to protect authenticated routes:
components/ProtectedRoute.tsx
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/lib/auth-context';
export default function ProtectedRoute({
children,
}: {
children: React.ReactNode;
}) {
const { isAuthenticated, loading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!loading && !isAuthenticated) {
router.push('/login');
}
}, [isAuthenticated, loading, router]);
if (loading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return null;
}
return <>{children}</>;
}Step 5: Update Root Layout
Wrap your application with the AuthProvider:
app/layout.tsx
import { AuthProvider } from '@/lib/auth-context';
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
);
}Step 6: Create Login Page
app/login/page.tsx
import GoogleLoginButton from '@/components/GoogleLoginButton';
export default function LoginPage() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in to your account
</h2>
</div>
<div className="mt-8">
<GoogleLoginButton />
</div>
</div>
</div>
);
}Step 7: Create Dashboard Page
app/dashboard/page.tsx
'use client';
import { useAuth } from '@/lib/auth-context';
import ProtectedRoute from '@/components/ProtectedRoute';
import { useRouter } from 'next/navigation';
export default function DashboardPage() {
const { user, logout } = useAuth();
const router = useRouter();
const handleLogout = () => {
logout();
router.push('/login');
};
return (
<ProtectedRoute>
<div className="min-h-screen p-8">
<div className="max-w-4xl mx-auto">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">Dashboard</h1>
<button
onClick={handleLogout}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
Logout
</button>
</div>
{user && (
<div className="bg-white shadow rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">User Information</h2>
<div className="space-y-2">
<p><strong>ID:</strong> {user.id}</p>
<p><strong>Email:</strong> {user.email || 'N/A'}</p>
<p><strong>Name:</strong> {user.name || 'N/A'}</p>
{user.avatar && (
<div>
<strong>Avatar:</strong>
<img
src={user.avatar}
alt="Avatar"
className="w-16 h-16 rounded-full mt-2"
/>
</div>
)}
</div>
</div>
)}
</div>
</div>
</ProtectedRoute>
);
}Complete Code Examples
Server-Side Authentication Check (Server Component)
If you need to check authentication on the server side:
app/dashboard/page-server.tsx
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { getBosbaseClient } from '@/lib/bosbase';
export default async function DashboardPageServer() {
const pb = getBosbaseClient();
// Get auth token from cookies (if you're storing it there)
const cookieStore = await cookies();
const token = cookieStore.get('bosbase_token')?.value;
if (token) {
pb.authStore.save(token, pb.authStore.record || null);
}
if (!pb.authStore.isValid) {
redirect('/login');
}
// Fetch user data
const collectionName = process.env.NEXT_PUBLIC_BOSBASE_COLLECTION || 'users';
const user = await pb.collection(collectionName).getOne(pb.authStore.record!.id);
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {user.name || user.email}!</p>
</div>
);
}Custom OAuth2 Flow with Manual Code Exchange
If you need more control over the OAuth2 flow (e.g., for mobile apps or custom redirects):
lib/oauth2-manual.ts
'use client';
import { getClientSideBosbase } from './bosbase';
export async function initiateGoogleLogin(): Promise<{
authURL: string;
state: string;
codeVerifier: string;
}> {
const pb = getClientSideBosbase();
const collectionName = process.env.NEXT_PUBLIC_BOSBASE_COLLECTION || 'users';
// Get available auth methods
const authMethods = await pb.collection(collectionName).listAuthMethods();
const provider = authMethods.oauth2.providers.find((p) => p.name === 'google');
if (!provider) {
throw new Error('Google OAuth2 is not configured');
}
// Store provider info for later verification
if (typeof window !== 'undefined') {
sessionStorage.setItem('oauth2_provider', JSON.stringify(provider));
}
return {
authURL: provider.authURL,
state: provider.state,
codeVerifier: provider.codeVerifier,
};
}
export async function handleOAuth2Callback(
code: string,
state: string
): Promise<any> {
const pb = getClientSideBosbase();
const collectionName = process.env.NEXT_PUBLIC_BOSBASE_COLLECTION || 'users';
// Retrieve stored provider info
const providerStr =
typeof window !== 'undefined'
? sessionStorage.getItem('oauth2_provider')
: null;
if (!providerStr) {
throw new Error('Provider info not found');
}
const provider = JSON.parse(providerStr);
// Verify state parameter
if (provider.state !== state) {
throw new Error('State parameter mismatch - possible CSRF attack');
}
// Build redirect URL
const redirectURL =
typeof window !== 'undefined'
? `${window.location.origin}/api/auth/callback`
: '';
// Exchange code for token
const authData = await pb.collection(collectionName).authWithOAuth2Code(
provider.name,
code,
provider.codeVerifier,
redirectURL,
{
// Optional: additional data for new users
emailVisibility: false,
}
);
// Clear stored provider info
if (typeof window !== 'undefined') {
sessionStorage.removeItem('oauth2_provider');
}
return authData;
}API Route for OAuth2 Callback (Alternative Approach)
If you prefer handling the callback in an API route:
app/api/auth/callback/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { handleOAuth2Callback } from '@/lib/oauth2-manual';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const code = searchParams.get('code');
const state = searchParams.get('state');
const error = searchParams.get('error');
if (error) {
return NextResponse.redirect(
new URL(`/login?error=${encodeURIComponent(error)}`, request.url)
);
}
if (!code || !state) {
return NextResponse.redirect(
new URL('/login?error=missing_parameters', request.url)
);
}
try {
const authData = await handleOAuth2Callback(code, state);
// Set auth token in cookie or session
const response = NextResponse.redirect(
new URL('/dashboard', request.url)
);
// Optionally store token in HTTP-only cookie
// response.cookies.set('bosbase_token', authData.token, {
// httpOnly: true,
// secure: process.env.NODE_ENV === 'production',
// sameSite: 'lax',
// });
return response;
} catch (err: any) {
console.error('OAuth2 callback error:', err);
return NextResponse.redirect(
new URL(`/login?error=${encodeURIComponent(err.message)}`, request.url)
);
}
}Best Practices
1. Error Handling
Always implement comprehensive error handling:
try {
const authData = await pb.collection('users').authWithOAuth2({
provider: 'google',
});
} catch (err: any) {
if (err.status === 403) {
// OAuth2 not enabled
console.error('OAuth2 is not enabled for this collection');
} else if (err.status === 400) {
// Invalid request
console.error('Invalid OAuth2 request:', err.data);
} else if (err.status === 401) {
// Authentication failed
console.error('OAuth2 authentication failed');
} else {
// Other errors
console.error('Unexpected error:', err);
}
}2. Token Management
The SDK automatically stores tokens in localStorage (browser) or memory (server). For production apps:
- Client-side: Tokens are stored in
localStorageby default (viaLocalAuthStore) - Server-side: Use
BaseAuthStorefor in-memory storage - Security: Never expose tokens in logs or client-side code
3. Popup Window Handling
The SDK handles popup windows automatically, but be aware of:
- Safari restrictions: Safari may block popups if not triggered directly from a user click
- Popup blockers: Some browsers may block the popup
- Mobile browsers: May handle popups differently
To handle Safari issues:
// Open popup before async operations
const popup = window.open('', 'oauth2_popup');
// Then call authWithOAuth2 with custom urlCallback
await pb.collection('users').authWithOAuth2({
provider: 'google',
urlCallback: (url) => {
if (popup) {
popup.location.href = url;
}
},
});4. State Management
Use React Context or a state management library to share authentication state:
// Example with Zustand
import { create } from 'zustand';
import { getClientSideBosbase } from '@/lib/bosbase';
interface AuthStore {
user: RecordModel | null;
setUser: (user: RecordModel | null) => void;
logout: () => void;
}
export const useAuthStore = create<AuthStore>((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => {
const pb = getClientSideBosbase();
pb.authStore.clear();
set({ user: null });
},
}));5. Loading States
Always show loading states during authentication:
const [loading, setLoading] = useState(false);
const handleLogin = async () => {
setLoading(true);
try {
await pb.collection('users').authWithOAuth2({ provider: 'google' });
} finally {
setLoading(false);
}
};6. New User Detection
Handle new vs. returning users differently:
const authData = await pb.collection('users').authWithOAuth2({
provider: 'google',
});
if (authData.meta?.isNew) {
// New user - redirect to onboarding
router.push('/onboarding');
} else {
// Returning user - redirect to dashboard
router.push('/dashboard');
}Troubleshooting
Common Issues
1. Popup Blocked
Problem: Browser blocks the OAuth2 popup window.
Solution: Ensure the OAuth2 call is triggered directly from a user interaction (click event), not from async code:
// ✅ Good
<button onClick={handleGoogleLogin}>Login</button>
// ❌ Bad (may be blocked in Safari)
<button onClick={async () => {
await someAsyncFunction();
handleGoogleLogin();
}}>Login</button>2. Invalid Redirect URI
Problem: Google returns "redirect_uri_mismatch" error.
Solution: Ensure the redirect URI in Google Cloud Console exactly matches:
https://your-bosbase-domain.com/api/oauth2-redirect3. OAuth2 Not Enabled
Problem: Error "OAuth2 is not enabled for this collection".
Solution: Enable OAuth2 in your Bosbase collection settings through the admin UI or API.
4. Provider Not Found
Problem: Error "Missing or invalid provider 'google'".
Solution: Ensure Google OAuth2 provider is configured in your collection with:
- Correct provider name:
google - Valid Client ID and Client Secret
- Correct OAuth2 URLs
5. Token Expired
Problem: User gets logged out unexpectedly.
Solution: Implement token refresh logic:
// Check token validity periodically
useEffect(() => {
const interval = setInterval(async () => {
if (pb.authStore.isValid) {
try {
await pb.collection('users').authRefresh();
} catch (err) {
// Token expired
pb.authStore.clear();
router.push('/login');
}
}
}, 5 * 60 * 1000); // Check every 5 minutes
return () => clearInterval(interval);
}, []);6. CORS Issues
Problem: CORS errors when calling Bosbase API.
Solution: Configure CORS in your Bosbase instance to allow requests from your Next.js domain.
7. Realtime Connection Issues
Problem: OAuth2 popup opens but authentication never completes.
Solution:
- Check WebSocket connection to Bosbase
- Verify realtime service is enabled
- Check browser console for WebSocket errors
- Ensure your network/firewall allows WebSocket connections
Debugging Tips
- Enable verbose logging:
pb.beforeSend = (url, options) => {
console.log('Request:', url, options);
return { url, options };
};- Check auth store state:
console.log('Auth store:', {
isValid: pb.authStore.isValid,
token: pb.authStore.token?.substring(0, 20) + '...',
record: pb.authStore.record,
});- Monitor realtime connection:
pb.realtime.subscribe('@oauth2', (e) => {
console.log('OAuth2 event:', e);
});Additional Resources
- Bosbase JS SDK Documentation
- Bosbase Authentication Guide
- OAuth2 Configuration Guide
- Google OAuth2 Documentation
Summary
This tutorial has walked you through:
- Setup: Configuring Google OAuth2 in Google Cloud Console and Bosbase
- Next.js Integration: Step-by-step implementation with React components
- Authentication State: Managing user authentication with React Context
- Route Protection: Securing pages that require authentication
- Best Practices: Error handling, loading states, and user experience
- Troubleshooting: Common issues and their solutions
The Bosbase JS SDK makes Google login simple by handling the entire OAuth2 flow automatically. With just a few lines of code, you can add secure Google authentication to your Next.js application without managing redirect handlers or token exchanges.
Quick Start Checklist
- Google OAuth2 credentials created in Google Cloud Console
- Redirect URI configured:
https://your-bosbase-domain.com/api/oauth2-redirect - OAuth2 enabled in Bosbase collection
- Google provider configured with Client ID and Secret
- Bosbase JS SDK installed in Next.js project
- Environment variables set (
NEXT_PUBLIC_BOSBASE_URL,NEXT_PUBLIC_BOSBASE_COLLECTION) - Login component implemented
- Authentication context/provider set up
- Protected routes configured
You're now ready to add Google login to your Next.js application!