Tutorials

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

  1. Overview
  2. Prerequisites
  3. How It Works
  4. Backend Configuration
  5. Next.js Setup
  6. Implementation Guide
  7. Complete Code Examples
  8. Best Practices
  9. 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:

  1. Bosbase instance running and accessible
  2. Next.js application (version 13+ recommended for App Router support)
  3. Google Cloud Console account with OAuth2 credentials
  4. Auth collection configured in your Bosbase instance
  5. Bosbase JS SDK installed in your Next.js project

Installing the SDK

npm install bosbase
# or
yarn add bosbase
# or
pnpm add bosbase

How 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":

  1. User clicks login button: Your Next.js app calls authWithOAuth2() with provider: 'google'
  2. SDK opens popup: A popup window opens with Google's OAuth2 consent page
  3. User authenticates: User signs in with their Google account and grants permissions
  4. Automatic callback handling: Bosbase receives the OAuth2 callback and processes it automatically
  5. User record created/updated: Bosbase automatically creates a new user or links to an existing account
  6. 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

  1. Go to Google Cloud Console
  2. Create a new project or select an existing one
  3. Enable the Google+ API
  4. Go to CredentialsCreate CredentialsOAuth 2.0 Client ID
  5. Configure the OAuth consent screen (if you haven't already)
  6. Set the Authorized redirect URIs to:
    https://your-bosbase-domain.com/api/oauth2-redirect

    Important: Replace your-bosbase-domain.com with your actual Bosbase instance domain

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

  1. Log in to your Bosbase Admin UI

  2. Navigate to your auth collection (e.g., "users")

  3. Go to SettingsOAuth2

  4. Enable OAuth2 if it's not already enabled

  5. 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)
  6. Configure field mappings (optional):

    • Map Google's user data fields to your collection fields
    • Common mappings: namename, emailemail, pictureavatar

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.ts

Environment Variables

Create a .env.local file:

NEXT_PUBLIC_BOSBASE_URL=https://your-bosbase-instance.com
NEXT_PUBLIC_BOSBASE_COLLECTION=users

Implementation 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 localStorage by default (via LocalAuthStore)
  • Server-side: Use BaseAuthStore for 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-redirect

3. 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

  1. Enable verbose logging:
pb.beforeSend = (url, options) => {
  console.log('Request:', url, options);
  return { url, options };
};
  1. Check auth store state:
console.log('Auth store:', {
  isValid: pb.authStore.isValid,
  token: pb.authStore.token?.substring(0, 20) + '...',
  record: pb.authStore.record,
});
  1. Monitor realtime connection:
pb.realtime.subscribe('@oauth2', (e) => {
  console.log('OAuth2 event:', e);
});

Additional Resources

Summary

This tutorial has walked you through:

  1. Setup: Configuring Google OAuth2 in Google Cloud Console and Bosbase
  2. Next.js Integration: Step-by-step implementation with React components
  3. Authentication State: Managing user authentication with React Context
  4. Route Protection: Securing pages that require authentication
  5. Best Practices: Error handling, loading states, and user experience
  6. 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!