Authentication — TopNetworks, Inc.
This skill governs all authentication and authorization work. Derived from route-genius (Better Auth 1.x + Google OAuth + Supabase sessions) as the canonical full-stack auth implementation in the workspace.
Scope
Use for: Login flows, session management, protected route middleware, OAuth integration, domain-restricted access, Google Drive OAuth (secondary), and access control patterns.
Not for: Database RLS configuration (see database skill), general API route design (see backend skill), or AdZep ad network integration (that is a separate concern — see project instruction files).
Auth Stack by Project
| Project | Auth Method | Provider | Session Storage |
|---|---|---|---|
| route-genius | Better Auth 1.x | Google OAuth 2.0 | PostgreSQL (Supabase) |
| topfinanzas-* (financial platforms) | No authentication | N/A — public content sites | N/A |
| emailgenius-broadcasts-generator | Internal tool | GCP service account | N/A |
| arbitrage-manager-dashboard | Internal tool | GCP IAM / service accounts | N/A |
Financial content platforms (topfinanzas-us-next, uk-topfinanzas-com, budgetbee-next, kardtrust) are public-facing and have no user authentication. They do not implement login, sessions, or protected routes.
Better Auth — Route-Genius Implementation
Installation & Setup
bash1npm install better-auth pg
typescript1// lib/auth.ts — server-side auth configuration 2import { betterAuth } from "better-auth"; 3import { Pool } from "pg"; 4 5export const auth = betterAuth({ 6 database: new Pool({ 7 connectionString: process.env.DATABASE_URL, 8 }), 9 10 socialProviders: { 11 google: { 12 clientId: process.env.GOOGLE_CLIENT_ID!, 13 clientSecret: process.env.GOOGLE_CLIENT_SECRET!, 14 }, 15 }, 16 17 // Domain restriction — only @topnetworks.co and @topfinanzas.com allowed 18 user: { 19 additionalFields: {}, 20 }, 21 22 // Trusted origins (must match deployment environments) 23 trustedOrigins: [ 24 "http://localhost:3070", 25 "https://route-genius.vercel.app", 26 "https://route.topnetworks.co", 27 ], 28 29 session: { 30 expiresIn: 60 * 60 * 24 * 7, // 7 days 31 updateAge: 60 * 60 * 24, // Refresh daily 32 cookieCache: { 33 enabled: true, 34 maxAge: 5 * 60, // 5-minute cookie cache 35 }, 36 }, 37});
typescript1// lib/auth-client.ts — client-side auth configuration 2import { createAuthClient } from "better-auth/react"; 3 4export const authClient = createAuthClient({ 5 baseURL: process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3070", 6}); 7 8export const { signIn, signOut, useSession } = authClient;
API Route Handler
typescript1// app/api/auth/[...all]/route.ts 2import { auth } from "@/lib/auth"; 3import { toNextJsHandler } from "better-auth/next-js"; 4 5export const { GET, POST } = toNextJsHandler(auth);
Domain Restriction
Only @topnetworks.co and @topfinanzas.com email domains are permitted. This is enforced at the application level in lib/auth.ts using Better Auth's callback hooks:
typescript1// lib/auth.ts — domain restriction implementation 2export const auth = betterAuth({ 3 // ...base config... 4 5 hooks: { 6 after: [ 7 { 8 matcher(context) { 9 return context.path === "/sign-in/social/callback"; 10 }, 11 handler: async (ctx) => { 12 const email = ctx.context?.session?.user?.email ?? ""; 13 const allowedDomains = ["@topnetworks.co", "@topfinanzas.com"]; 14 const isAllowed = allowedDomains.some((domain) => 15 email.endsWith(domain), 16 ); 17 18 if (!isAllowed) { 19 // Sign out the user and redirect to login with error 20 await auth.api.signOut({ headers: ctx.request.headers }); 21 return Response.redirect( 22 `${process.env.NEXT_PUBLIC_APP_URL}/login?error=unauthorized_domain`, 23 ); 24 } 25 }, 26 }, 27 ], 28 }, 29});
Session Management
Server-Side Session Access
typescript1// lib/auth-session.ts 2import { auth } from "./auth"; 3import { headers } from "next/headers"; 4import { redirect } from "next/navigation"; 5 6// Use in Server Components and Server Actions 7export async function getServerSession() { 8 const session = await auth.api.getSession({ 9 headers: await headers(), 10 }); 11 return session; 12} 13 14// Auth guard — use at the top of every Server Action 15export async function requireUserId(): Promise<string> { 16 const session = await getServerSession(); 17 if (!session?.user?.id) { 18 redirect("/login"); 19 } 20 return session.user.id; 21} 22 23// Auth guard — use in Server Components (doesn't redirect, returns null) 24export async function getOptionalSession() { 25 const session = await getServerSession(); 26 return session?.user ?? null; 27}
Client-Side Session Access
typescript1"use client"; 2import { useSession } from "@/lib/auth-client"; 3 4export function UserAvatar() { 5 const { data: session, isPending } = useSession(); 6 7 if (isPending) return <Skeleton />; 8 if (!session) return null; 9 10 return ( 11 <img 12 src={session.user.image ?? "/default-avatar.png"} 13 alt={session.user.name} 14 /> 15 ); 16}
Middleware Protection (proxy.ts)
Route-genius uses proxy.ts (Next.js 16 middleware convention). Next.js 15 projects use middleware.ts.
typescript1// proxy.ts (route-genius — Next.js 16) 2import { NextResponse } from "next/server"; 3import type { NextRequest } from "next/server"; 4 5// Routes accessible without authentication 6const PUBLIC_PATHS = [ 7 "/login", 8 "/api/auth", // Better Auth endpoints 9 "/api/redirect", // Public redirect endpoint 10 "/api/analytics", // Public analytics API 11 "/analytics", // Public analytics pages 12 "/privacy", 13 "/terms", 14]; 15 16export function middleware(request: NextRequest) { 17 const pathname = request.nextUrl.pathname; 18 19 const isPublic = PUBLIC_PATHS.some((p) => pathname.startsWith(p)); 20 if (isPublic) return NextResponse.next(); 21 22 // Cookie name varies by protocol: 23 // HTTPS: __Secure-better-auth.session_token 24 // HTTP (localhost): better-auth.session_token 25 const sessionCookie = 26 request.cookies.get("__Secure-better-auth.session_token") || 27 request.cookies.get("better-auth.session_token"); 28 29 if (!sessionCookie) { 30 const loginUrl = new URL("/login", request.url); 31 loginUrl.searchParams.set("callbackUrl", pathname); 32 return NextResponse.redirect(loginUrl); 33 } 34 35 // Redirect authenticated users away from /login 36 if (pathname === "/login") { 37 return NextResponse.redirect(new URL("/dashboard", request.url)); 38 } 39 40 return NextResponse.next(); 41} 42 43export const config = { 44 matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.png$).*)"], 45};
Login Page Implementation
typescript1// app/login/page.tsx 2"use client"; 3import { signIn } from "@/lib/auth-client"; 4import { Button } from "@/components/ui/button"; 5import Image from "next/image"; 6 7export default function LoginPage() { 8 async function handleGoogleSignIn() { 9 await signIn.social({ 10 provider: "google", 11 callbackURL: "/dashboard", 12 }); 13 } 14 15 return ( 16 <main className="min-h-screen flex items-center justify-center bg-gradient-to-br from-lime-50 via-cyan-50 to-blue-100"> 17 <div className="bg-white rounded-2xl shadow-lg p-8 w-full max-w-md text-center"> 18 <Image 19 src="https://storage.googleapis.com/media-topfinanzas-com/images/topnetworks-positivo-sinfondo.webp" 20 alt="TopNetworks" 21 width={180} 22 height={40} 23 className="mx-auto mb-6" 24 /> 25 <h1 className="text-2xl font-bold text-gray-800 mb-2">Sign in</h1> 26 <p className="text-gray-600 mb-8 text-sm"> 27 Access is restricted to @topnetworks.co and @topfinanzas.com accounts. 28 </p> 29 <Button 30 onClick={handleGoogleSignIn} 31 className="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3" 32 > 33 Continue with Google 34 </Button> 35 </div> 36 </main> 37 ); 38}
Better Auth Database Tables
Better Auth auto-manages these tables — never modify them manually:
| Table | Purpose |
|---|---|
user | User profiles (id, name, email, image, createdAt) |
session | Active sessions (id, userId, token, expiresAt) |
account | OAuth account links (userId, provider, providerAccountId) |
verification | Email verification tokens |
These tables are created/migrated automatically when Better Auth initializes with a database connection.
Better Auth Migration (initial setup)
typescript1// Run once to create Better Auth tables 2import { auth } from "@/lib/auth"; 3await auth.api.migrate(); // Creates all Better Auth tables
Google Drive OAuth (Secondary — Backup Feature)
Route-genius uses a separate OAuth flow for Google Drive access, stored in HTTP-only cookies:
typescript1// app/api/auth/google-drive/callback/route.ts 2import { cookies } from "next/headers"; 3import type { NextRequest } from "next/server"; 4 5export async function GET(request: NextRequest) { 6 const { searchParams } = new URL(request.url); 7 const code = searchParams.get("code"); 8 9 if (!code) { 10 return Response.redirect( 11 `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/settings?error=drive_auth_failed`, 12 ); 13 } 14 15 // Exchange code for tokens 16 const tokenResponse = await fetch("https://oauth2.googleapis.com/token", { 17 method: "POST", 18 body: new URLSearchParams({ 19 code, 20 client_id: process.env.GOOGLE_DRIVE_CLIENT_ID!, 21 client_secret: process.env.GOOGLE_DRIVE_CLIENT_SECRET!, 22 redirect_uri: process.env.GOOGLE_DRIVE_REDIRECT_URI!, 23 grant_type: "authorization_code", 24 }), 25 }); 26 27 const tokens = await tokenResponse.json(); 28 29 // Store in HTTP-only cookie — 30 days 30 const cookieStore = await cookies(); 31 cookieStore.set("rg_gdrive_tokens", JSON.stringify(tokens), { 32 httpOnly: true, 33 secure: process.env.NODE_ENV === "production", 34 maxAge: 60 * 60 * 24 * 30, 35 path: "/", 36 }); 37 38 return Response.redirect( 39 `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/settings?drive=connected`, 40 ); 41}
Google Drive OAuth scope: https://www.googleapis.com/auth/drive.file (only files created by the app — principle of least privilege).
Cookie Naming Convention
Better Auth uses different cookie name prefixes based on protocol:
| Environment | Cookie Name |
|---|---|
| Production (HTTPS) | __Secure-better-auth.session_token |
| Development (HTTP) | better-auth.session_token |
Always check for both when reading session cookies in middleware.
Environment Variables Required
bash1# Better Auth core 2DATABASE_URL= # PostgreSQL connection string for session storage 3NEXT_PUBLIC_APP_URL= # Canonical app URL 4BETTER_AUTH_URL= # Optional — overrides NEXT_PUBLIC_APP_URL for auth 5 6# Google OAuth (user authentication) 7GOOGLE_CLIENT_ID= # Google OAuth 2.0 Client ID 8GOOGLE_CLIENT_SECRET= # Google OAuth 2.0 Client Secret 9 10# Google Drive OAuth (backup feature — separate OAuth client) 11GOOGLE_DRIVE_CLIENT_ID= 12GOOGLE_DRIVE_CLIENT_SECRET= 13GOOGLE_DRIVE_REDIRECT_URI= # e.g. http://localhost:3070/api/auth/google-drive/callback
Access Control Patterns
Server Action Guard
typescript1// Pattern: always first line in any Server Action 2export async function mutateDataAction(data: InputType) { 3 const userId = await requireUserId(); // Redirects to /login if not authenticated 4 5 // All subsequent code runs only for authenticated users 6 const result = await performOperation(data, userId); 7 return { success: true, data: result }; 8}
Server Component Guard
typescript1// Pattern: in Server Components where you want to conditionally render 2import { getServerSession } from "@/lib/auth-session"; 3import { redirect } from "next/navigation"; 4 5export default async function DashboardPage() { 6 const session = await getServerSession(); 7 if (!session) redirect("/login"); 8 9 // Render authenticated UI 10 return <Dashboard user={session.user} />; 11}
Client Component Session Check
typescript1"use client"; 2import { useSession } from "@/lib/auth-client"; 3import { useRouter } from "next/navigation"; 4import { useEffect } from "react"; 5 6export function ProtectedClientComponent() { 7 const { data: session, isPending } = useSession(); 8 const router = useRouter(); 9 10 useEffect(() => { 11 if (!isPending && !session) { 12 router.push("/login"); 13 } 14 }, [session, isPending, router]); 15 16 if (isPending) return <LoadingSkeleton />; 17 if (!session) return null; 18 19 return <div>Authenticated content</div>; 20}
Sign Out
typescript1"use client"; 2import { signOut } from "@/lib/auth-client"; 3import { useRouter } from "next/navigation"; 4 5export function SignOutButton() { 6 const router = useRouter(); 7 8 async function handleSignOut() { 9 await signOut({ 10 fetchOptions: { 11 onSuccess: () => router.push("/login"), 12 }, 13 }); 14 } 15 16 return <button onClick={handleSignOut}>Sign out</button>; 17}
Constraints
- Never implement custom session storage — use Better Auth's PostgreSQL adapter
- Never store sensitive tokens (OAuth tokens, session data) in
localStorage— use HTTP-only cookies - Never manually modify Better Auth tables (
user,session,account,verification) - Never skip
requireUserId()in Server Actions that access user data - Never bypass domain restriction for convenience —
@topnetworks.coand@topfinanzas.comonly - Never use the same Google OAuth client ID for user authentication and Google Drive backup — these are separate OAuth credentials
- The
SUPABASE_SERVICE_ROLE_KEYbypasses RLS — this is intentional for server-side operations but must never be used for auth validation - Session cookie prefix (
__Secure-) is auto-applied on HTTPS — do not hard-code the prefix; always check for both variants in middleware - Do not add
auth.uid()to Supabase RLS policies — Better Auth does not use Supabase Auth, soauth.uid()will always return null