Authentication
How authentication works in ScaleRocket — email/password, OAuth, session management, and protected routes.
Authentication
ScaleRocket handles all authentication through the dashboard app (apps/app) — login, signup, password reset, and OAuth flows. Protected routes are enforced using TanStack Router.
How Auth Works
- User signs up or logs in via the dashboard app
- Supabase Auth creates a session and returns a JWT
- The Supabase client stores the session in
localStorage - Every API request includes the JWT for authorization
- Edge Functions verify the JWT to identify the user
- Row Level Security in PostgreSQL uses the JWT to filter data
How Auth Works
- User signs up or logs in via the dashboard app
- Convex Auth creates a session and stores it server-side
- The Convex client automatically manages the auth token
- Every Convex query/mutation is authenticated automatically
- Server functions use
getAuthUserId(ctx)to identify the user - Convex's permission system controls data access per function
Email/Password Signup
The default signup flow:
- User fills in email + password on
/signup - The app calls
supabase.auth.signUp() - Supabase sends a confirmation email
- User clicks the confirmation link
- User is redirected back to the app as authenticated
// apps/app/src/lib/auth.ts
import { supabase } from "./supabase";
export async function signUp(email: string, password: string) {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`,
},
});
if (error) throw error;
return data;
}
export async function signIn(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
return data;
}- User fills in email + password on
/signup - The app calls
signIn("password", formData)withflow: "signUp" - Convex Auth sends a verification email
- User clicks the verification link
- User is redirected back to the app as authenticated
// apps/app/src/lib/auth.ts
import { useAuthActions } from "@convex-dev/auth/react";
// Inside a React component
const { signIn } = useAuthActions();
// Sign up
async function handleSignUp(email: string, password: string) {
const formData = new FormData();
formData.set("email", email);
formData.set("password", password);
formData.set("flow", "signUp");
await signIn("password", formData);
}
// Sign in
async function handleSignIn(email: string, password: string) {
const formData = new FormData();
formData.set("email", email);
formData.set("password", password);
formData.set("flow", "signIn");
await signIn("password", formData);
}OAuth Setup (Google, GitHub)
ScaleRocket supports OAuth providers out of the box.
After configuring the providers in Supabase (see Supabase Setup), the client code is simple:
// apps/app/src/lib/auth.ts
export async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});
if (error) throw error;
return data;
}
export async function signInWithGitHub() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: "github",
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});
if (error) throw error;
return data;
}After configuring the providers in Convex Auth, the client code is simple:
// apps/app/src/lib/auth.ts
import { useAuthActions } from "@convex-dev/auth/react";
// Inside a React component
const { signIn } = useAuthActions();
// Google OAuth
async function signInWithGoogle() {
await signIn("google");
}
// GitHub OAuth
async function signInWithGitHub() {
await signIn("github");
}Auth Callback Page
After OAuth redirect, the callback page exchanges the code for a session:
// apps/app/src/routes/auth/callback.tsx
import { useEffect } from "react";
import { useNavigate } from "@tanstack/react-router";
import { supabase } from "../../lib/supabase";
export default function AuthCallback() {
const navigate = useNavigate();
useEffect(() => {
supabase.auth.onAuthStateChange((event) => {
if (event === "SIGNED_IN") {
navigate({ to: "/dashboard" });
}
});
}, [navigate]);
return <div>Completing sign in...</div>;
}With Convex Auth, OAuth callbacks are handled automatically. No callback page is needed — the user is redirected back to the app and the session is established by the Convex client.
Password Reset
ScaleRocket includes a complete password reset flow:
// Request password reset
export async function resetPassword(email: string) {
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/auth/reset-password`,
});
if (error) throw error;
}
// Update password (on the reset page)
export async function updatePassword(newPassword: string) {
const { error } = await supabase.auth.updateUser({
password: newPassword,
});
if (error) throw error;
}The flow:
- User enters their email on
/forgot-password - Supabase sends a reset email with a magic link
- User clicks the link, lands on
/auth/reset-password - User enters a new password
- The app calls
updateUser()to set the new password
ScaleRocket includes a complete password reset flow:
import { useAuthActions } from "@convex-dev/auth/react";
const { signIn } = useAuthActions();
// Request password reset
async function resetPassword(email: string) {
const formData = new FormData();
formData.set("email", email);
formData.set("flow", "reset");
await signIn("password", formData);
}
// The reset code/link is sent via email.
// When the user submits the new password with the reset token:
async function updatePassword(code: string, newPassword: string) {
const formData = new FormData();
formData.set("code", code);
formData.set("newPassword", newPassword);
formData.set("flow", "reset-verification");
await signIn("password", formData);
}The flow:
- User enters their email on
/forgot-password - Convex Auth sends a reset email with a code/link
- User clicks the link or enters the code
- User enters a new password
- The app calls
signIn("password", formData)with the reset verification flow
Session Management
Supabase handles session refresh automatically. You can listen for auth state changes:
// apps/app/src/lib/supabase.ts
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY
);// Listen for auth changes anywhere in the app
supabase.auth.onAuthStateChange((event, session) => {
if (event === "SIGNED_OUT") {
// Redirect to login
}
if (event === "TOKEN_REFRESHED") {
// Session was refreshed automatically
}
});Get the current user:
const { data: { user } } = await supabase.auth.getUser();Sign out:
await supabase.auth.signOut();Convex handles sessions automatically. Use the useConvexAuth() hook to check auth state:
import { useConvexAuth } from "convex/react";
function MyComponent() {
const { isAuthenticated, isLoading } = useConvexAuth();
if (isLoading) return <div>Loading...</div>;
if (!isAuthenticated) return <div>Not logged in</div>;
return <div>Welcome!</div>;
}Get the current user on the server side:
import { getAuthUserId } from "@convex-dev/auth/server";
// Inside a Convex query or mutation
const userId = await getAuthUserId(ctx);Sign out:
import { useAuthActions } from "@convex-dev/auth/react";
const { signOut } = useAuthActions();
await signOut();Protected Routes (TanStack Router)
ScaleRocket uses TanStack Router with a beforeLoad guard to protect routes:
// apps/app/src/routes/__root.tsx
import { createRootRoute } from "@tanstack/react-router";
import { supabase } from "../lib/supabase";
export const Route = createRootRoute({
beforeLoad: async ({ location }) => {
const { data: { session } } = await supabase.auth.getSession();
const publicRoutes = ["/login", "/signup", "/forgot-password", "/auth/callback"];
const isPublicRoute = publicRoutes.some((route) =>
location.pathname.startsWith(route)
);
if (!session && !isPublicRoute) {
throw redirect({ to: "/login" });
}
if (session && (location.pathname === "/login" || location.pathname === "/signup")) {
throw redirect({ to: "/dashboard" });
}
},
});// apps/app/src/components/AuthGuard.tsx
import { useConvexAuth } from "convex/react";
import { Navigate } from "@tanstack/react-router";
export function AuthGuard({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useConvexAuth();
if (isLoading) return <div>Loading...</div>;
if (!isAuthenticated) return <Navigate to="/login" />;
return <>{children}</>;
}Wrap your protected route layouts with AuthGuard to enforce authentication.
This ensures:
- Unauthenticated users are redirected to
/login - Authenticated users are redirected away from login/signup pages
- Public routes (login, signup, callback) are always accessible
User Profile
After signup, ScaleRocket creates a profile for the user:
A row is created in the profiles table via a database trigger:
-- supabase/migrations/create_profile_trigger.sql
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger AS $$
BEGIN
INSERT INTO public.profiles (id, email, full_name, avatar_url)
VALUES (
NEW.id,
NEW.email,
NEW.raw_user_meta_data->>'full_name',
NEW.raw_user_meta_data->>'avatar_url'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();A profile is created via a getOrCreateProfile mutation, called automatically after authentication:
// convex/users.ts
import { mutation, query } from "./_generated/server";
import { getAuthUserId } from "@convex-dev/auth/server";
export const getOrCreateProfile = mutation({
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Not authenticated");
const existing = await ctx.db
.query("profiles")
.withIndex("by_userId", (q) => q.eq("userId", userId))
.unique();
if (existing) return existing._id;
const user = await ctx.db.get(userId);
return await ctx.db.insert("profiles", {
userId,
email: user?.email ?? "",
fullName: user?.name ?? "",
avatarUrl: user?.image ?? "",
});
},
});Next Steps
- Set up API calls — authenticate requests to Edge Functions
- Configure the credits system — track usage per user
- Set up OAuth providers — Google and GitHub configuration
Done reading? Mark this page as complete.