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
bash
1npm install better-auth pg
typescript
1// 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});
typescript
1// 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
typescript
1// 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:
typescript
1// 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
typescript
1// 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
typescript
1"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.
typescript
1// 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
typescript
1// 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)
typescript
1// 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:
typescript
1// 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
bash
1# 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
typescript
1// 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
typescript
1// 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
typescript
1"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
typescript
1"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.co and @topfinanzas.com only
- Never use the same Google OAuth client ID for user authentication and Google Drive backup — these are separate OAuth credentials
- The
SUPABASE_SERVICE_ROLE_KEY bypasses 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, so auth.uid() will always return null