Skip to main content

Authentication & RLS

Authentication Flow

HostMetrics uses Supabase Auth with email/password authentication.

AuthProvider

src/components/auth/AuthProvider.tsx provides a React Context with:
PropertyTypeDescription
userUser | nullCurrent authenticated user
sessionSession | nullJWT session
isLoadingbooleanAuth state loading
signOut()() => Promise<void>Sign out and redirect
refreshSession()() => Promise<void>Force refresh session
The provider listens to supabase.auth.onAuthStateChange() for real-time session updates across browser tabs.

Protected Routes

  • (dashboard)/ layout group — Requires authenticated session
  • (auth)/ layout group — Public (login, signup, reset password)
  • /fleet/[slug] — Public fleet pages (no auth)
  • /p/[token] — Token-authenticated investor portal
  • /r/[token] — Token-authenticated investor report

API Route Authentication

Server-side API routes validate tokens manually:
// Pattern used in /api/turo/sync, /api/tolls/sync, etc.
const authHeader = request.headers.get("authorization");
const token = authHeader?.replace("Bearer ", "");
const { data: { user }, error } = await supabase.auth.getUser(token);
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

Row Level Security (RLS)

Every table has RLS enabled with policies ensuring users can only access their own data.

The Pattern

-- SELECT: Users see only their own rows
CREATE POLICY "select_own" ON table_name
    FOR SELECT USING (user_id = auth.uid());

-- INSERT: Users can only insert rows with their own user_id
CREATE POLICY "insert_own" ON table_name
    FOR INSERT WITH CHECK (user_id = auth.uid());

-- UPDATE: Users can only update their own rows
CREATE POLICY "update_own" ON table_name
    FOR UPDATE USING (user_id = auth.uid());

-- DELETE: Users can only delete their own rows
CREATE POLICY "delete_own" ON table_name
    FOR DELETE USING (user_id = auth.uid());

Application-Level Enforcement

In addition to RLS, the application code always filters by user_id:
// src/lib/db/_client.ts
export async function getCurrentUserId(): Promise<string> {
    const { data: { user } } = await getClient().auth.getUser();
    if (!user) throw new Error("Not authenticated");
    return user.id;
}

// Used in every DB module:
const userId = await getCurrentUserId();
const { data } = await supabase
    .from("trips")
    .select("*")
    .eq("user_id", userId);
This double enforcement (RLS + application filtering) ensures data isolation even if one layer has a bug.