feat: migrate backend to supabase, update prisma schema wrt supabase and add auth testing with logout button - #4
Conversation
📝 WalkthroughWalkthroughAdds Supabase authentication to the Expo mobile app and aligns the backend Prisma schema. The mobile side introduces a Supabase client with SecureStore token persistence, an ChangesSupabase Auth Integration
Sequence Diagram(s)sequenceDiagram
participant User
participant AuthScreen as Login/Signup Screen
participant SupabaseAuth as supabase.auth
participant AuthProvider
participant RootLayout
User->>AuthScreen: submit credentials
AuthScreen->>SupabaseAuth: signInWithPassword / signUp
SupabaseAuth-->>AuthScreen: error (display) or success
SupabaseAuth->>AuthProvider: onAuthStateChange fires with new session
AuthProvider->>AuthProvider: setSession / setUser
AuthProvider->>RootLayout: useAuth() returns updated session
RootLayout->>RootLayout: segment check → replace route to /
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (2)
mobile/src/components/auth-ui.tsx (2)
20-25: ⚡ Quick winUse a TouchableOpacity ref type for
PrimaryButton.Line 20 declares
forwardRef<View, ...>but the ref is attached to<TouchableOpacity>. UseReact.ElementRef<typeof TouchableOpacity>(orTouchableOpacity) for the forwarded ref type to keep the public ref contract correct.Suggested fix
-export const PrimaryButton = React.forwardRef<View, TouchableOpacityProps & { title: string; loading?: boolean; iconRight?: React.ReactNode }>(({ title, loading, style, iconRight, ...props }, ref) => { +type PrimaryButtonProps = TouchableOpacityProps & { + title: string; + loading?: boolean; + iconRight?: React.ReactNode; +}; + +export const PrimaryButton = React.forwardRef<React.ElementRef<typeof TouchableOpacity>, PrimaryButtonProps>( +({ title, loading, style, iconRight, ...props }, ref) => { const theme = useTheme(); return (🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/src/components/auth-ui.tsx` around lines 20 - 25, In the PrimaryButton component, the forwardRef generic type declaration uses View as the ref type, but the actual ref is being attached to TouchableOpacity. Update the forwardRef type parameter from View to React.ElementRef<typeof TouchableOpacity> to ensure the public ref contract matches the underlying component being referenced.
23-27: ⚡ Quick winDisable presses while
loadinginsidePrimaryButton.
loadingcurrently only changes visuals. If a caller forgets to passdisabled, duplicate submits are still possible.Suggested fix
export const PrimaryButton = React.forwardRef<...>(({ title, loading, style, iconRight, ...props }, ref) => { const theme = useTheme(); + const isDisabled = Boolean(props.disabled || loading); return ( <TouchableOpacity ref={ref} - style={[styles.primaryButton, { backgroundColor: theme.primary, opacity: props.disabled ? 0.7 : 1 }, style]} + disabled={isDisabled} + style={[styles.primaryButton, { backgroundColor: theme.primary, opacity: isDisabled ? 0.7 : 1 }, style]} activeOpacity={0.8} {...props} >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/src/components/auth-ui.tsx` around lines 23 - 27, The PrimaryButton component should prevent button presses when the loading state is active, not just when the disabled prop is true. Modify the TouchableOpacity component to disable presses by checking both props.disabled and props.loading together - the button should be disabled if either condition is true. This ensures duplicate submissions are prevented even if a caller forgets to pass the disabled prop alongside the loading state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/.env.example`:
- Line 2: The backend/.env.example file contains duplicate DATABASE_URL entries
at multiple locations, where the second entry will silently override the first
during configuration, leading to misconfiguration. Identify all occurrences of
the DATABASE_URL key in the file and remove the duplicate entries, keeping only
one active DATABASE_URL configuration that represents the correct database
connection string for local development.
In `@mobile/app/_layout.tsx`:
- Around line 19-42: The RootLayoutNav component renders the Stack navigator
even when initialized is false, causing a flash of protected content before the
useEffect redirect logic executes. Gate the entire rendering of the
ThemeProvider and Stack components behind a check for the initialized flag -
only render the navigation tree when initialized is true, otherwise return null
or a loading indicator to prevent premature rendering of protected tabs.
In `@mobile/app/`(auth)/login.tsx:
- Around line 68-70: The TouchableOpacity component rendering the "Forgot
password?" text has no onPress handler, making it a non-functional control.
Either add an onPress prop to the TouchableOpacity that navigates to a password
reset/recovery screen (or triggers appropriate password recovery flow), or
remove the entire TouchableOpacity component and its child Text if the forgot
password functionality is not currently implemented.
In `@mobile/app/`(auth)/signup.tsx:
- Around line 62-65: The SocialButton components for "Continue with Google" and
"Continue with Phone" are missing onPress event handlers, creating
non-functional UI elements that appear interactive. Add onPress callbacks to
both SocialButton instances in the socialBlock that implement the appropriate
authentication or navigation logic for each provider, ensuring users can
actually interact with these buttons.
In `@mobile/app/`(tabs)/index.tsx:
- Around line 7-9: The handleLogout function currently ignores errors returned
by supabase.auth.signOut(), which means logout failures occur silently without
user feedback. Modify the handleLogout function to capture the result of the
supabase.auth.signOut() call and destructure or check the error property from
the returned object. If an error exists, handle it appropriately by logging it
and providing user feedback (such as displaying an error message) instead of
allowing the logout to fail silently.
In `@mobile/src/hooks/useAuth.tsx`:
- Around line 22-33: The useEffect hook in useAuth.tsx needs to handle both
error checking and cleanup on unmount. The getSession() call returns an object
with both data and error properties, so you need to check if the error property
exists and handle initialization failures instead of silently ignoring them.
Additionally, add a cleanup flag (like a mounted or isMounted ref) that gets set
to false in a cleanup function returned from the useEffect, and check this flag
before calling setSession, setUser, and setInitialized to prevent state updates
after component unmount. Return a cleanup function from the useEffect that sets
this flag and unsubscribes from the onAuthStateChange subscription to fully
prevent memory leaks.
In `@mobile/src/lib/supabase.ts`:
- Around line 17-20: The createClient call in the supabase initialization uses
non-null assertions on the environment variables EXPO_PUBLIC_SUPABASE_URL and
EXPO_PUBLIC_SUPABASE_ANON_KEY, which results in unclear runtime errors if they
are missing. Replace these non-null assertions with explicit validation that
checks if both environment variables are defined before the createClient call,
and throw a descriptive error message that clearly states which variable is
missing if validation fails. This ensures the application fails fast with
actionable feedback during startup.
- Around line 9-14: The setItem and removeItem methods in the storage adapter
are calling SecureStore.setItemAsync and SecureStore.deleteItemAsync but not
returning the promises they produce. This creates a race condition where
Supabase's persistSession and autoRefreshToken features cannot await these
operations. Add return statements before each SecureStore async call in both the
setItem method (before SecureStore.setItemAsync) and the removeItem method
(before SecureStore.deleteItemAsync) so that the promises are properly returned
and can be awaited by the Supabase client.
---
Nitpick comments:
In `@mobile/src/components/auth-ui.tsx`:
- Around line 20-25: In the PrimaryButton component, the forwardRef generic type
declaration uses View as the ref type, but the actual ref is being attached to
TouchableOpacity. Update the forwardRef type parameter from View to
React.ElementRef<typeof TouchableOpacity> to ensure the public ref contract
matches the underlying component being referenced.
- Around line 23-27: The PrimaryButton component should prevent button presses
when the loading state is active, not just when the disabled prop is true.
Modify the TouchableOpacity component to disable presses by checking both
props.disabled and props.loading together - the button should be disabled if
either condition is true. This ensures duplicate submissions are prevented even
if a caller forgets to pass the disabled prop alongside the loading state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: bcc14e2d-c688-476b-b241-09c0fc321ac2
⛔ Files ignored due to path filters (1)
mobile/pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (35)
backend/.env.examplebackend/prisma/schema.prismamobile/.env.examplemobile/AGENTS.mdmobile/CLAUDE.mdmobile/app.jsonmobile/app/(auth)/_layout.tsxmobile/app/(auth)/login.tsxmobile/app/(auth)/signup.tsxmobile/app/(auth)/welcome.tsxmobile/app/(tabs)/_layout.tsxmobile/app/(tabs)/explore.tsxmobile/app/(tabs)/index.tsxmobile/app/_layout.tsxmobile/eslint.config.jsmobile/package.jsonmobile/pnpm-workspace.yamlmobile/scripts/reset-project.jsmobile/src/components/animated-icon.module.cssmobile/src/components/animated-icon.tsxmobile/src/components/animated-icon.web.tsxmobile/src/components/app-tabs.web.tsxmobile/src/components/auth-ui.tsxmobile/src/components/external-link.tsxmobile/src/components/hint-row.tsxmobile/src/components/themed-text.tsxmobile/src/components/ui/collapsible.tsxmobile/src/components/web-badge.tsxmobile/src/constants/theme.tsmobile/src/hooks/use-color-scheme.web.tsmobile/src/hooks/useAuth.tsxmobile/src/lib/supabase.tsmobile/src/screens/_layout.tsxmobile/src/screens/explore.tsxmobile/src/screens/index.tsx
💤 Files with no reviewable changes (13)
- mobile/CLAUDE.md
- mobile/AGENTS.md
- mobile/src/components/ui/collapsible.tsx
- mobile/src/components/animated-icon.module.css
- mobile/src/components/hint-row.tsx
- mobile/src/components/animated-icon.web.tsx
- mobile/scripts/reset-project.js
- mobile/src/components/web-badge.tsx
- mobile/src/screens/explore.tsx
- mobile/src/screens/index.tsx
- mobile/src/components/external-link.tsx
- mobile/src/components/animated-icon.tsx
- mobile/src/screens/_layout.tsx
| @@ -1 +1,9 @@ | |||
| # if you're using neondb then use this | |||
| DATABASE_URL="postgresql://postgres:password@localhost:5432/kryze_innoforge?schema=public" | |||
There was a problem hiding this comment.
Avoid duplicate DATABASE_URL keys in the example file.
Two active DATABASE_URL entries means the second one always overrides the first, which can silently misconfigure local setup.
Suggested fix
-# if you're using neondb then use this
-DATABASE_URL="postgresql://postgres:password@localhost:5432/kryze_innoforge?schema=public"
+# Option A: local Postgres
+# DATABASE_URL=postgresql://postgres:password@localhost:5432/kryze_innoforge?schema=public
# if you're using supabase :)
# Connect to Postgres via the shared transaction-mode pooler (IPv4-only)
-DATABASE_URL="postgresql://postgres.rjzxredxxfbkrlmuyabb:[YOUR-PASSWORD]`@aws-1-ap-southeast-1.pooler.supabase.com`:6543/postgres?pgbouncer=true"
+DATABASE_URL=postgresql://postgres.rjzxredxxfbkrlmuyabb:[YOUR-PASSWORD]`@aws-1-ap-southeast-1.pooler.supabase.com`:6543/postgres?pgbouncer=trueAlso applies to: 6-6
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/.env.example` at line 2, The backend/.env.example file contains
duplicate DATABASE_URL entries at multiple locations, where the second entry
will silently override the first during configuration, leading to
misconfiguration. Identify all occurrences of the DATABASE_URL key in the file
and remove the duplicate entries, keeping only one active DATABASE_URL
configuration that represents the correct database connection string for local
development.
Source: Linters/SAST tools
| const { session, initialized } = useAuth(); | ||
| const segments = useSegments(); | ||
| const router = useRouter(); | ||
|
|
||
| useEffect(() => { | ||
| if (!initialized) return; | ||
|
|
||
| const inAuthGroup = segments[0] === '(auth)'; | ||
|
|
||
| if (!session && !inAuthGroup) { | ||
| router.replace('/(auth)/welcome'); | ||
| } else if (session && inAuthGroup) { | ||
| router.replace('/'); | ||
| } | ||
| }, [session, initialized, segments, router]); | ||
|
|
||
| return ( | ||
| <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> | ||
| <Stack screenOptions={{ headerShown: false }}> | ||
| <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> | ||
| <Stack.Screen name="(auth)" options={{ headerShown: false }} /> | ||
| </Stack> | ||
| </ThemeProvider> | ||
| ); |
There was a problem hiding this comment.
Gate navigator rendering until auth initialization completes.
RootLayoutNav renders the Stack even when initialized === false, and redirect logic is deferred (Line 24). That allows a pre-redirect flash of protected tabs.
Suggested fix
function RootLayoutNav() {
const colorScheme = useColorScheme();
const { session, initialized } = useAuth();
const segments = useSegments();
const router = useRouter();
+
+ if (!initialized) {
+ return null;
+ }
useEffect(() => {
- if (!initialized) return;
-
const inAuthGroup = segments[0] === '(auth)';
if (!session && !inAuthGroup) {
router.replace('/(auth)/welcome');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { session, initialized } = useAuth(); | |
| const segments = useSegments(); | |
| const router = useRouter(); | |
| useEffect(() => { | |
| if (!initialized) return; | |
| const inAuthGroup = segments[0] === '(auth)'; | |
| if (!session && !inAuthGroup) { | |
| router.replace('/(auth)/welcome'); | |
| } else if (session && inAuthGroup) { | |
| router.replace('/'); | |
| } | |
| }, [session, initialized, segments, router]); | |
| return ( | |
| <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> | |
| <Stack screenOptions={{ headerShown: false }}> | |
| <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> | |
| <Stack.Screen name="(auth)" options={{ headerShown: false }} /> | |
| </Stack> | |
| </ThemeProvider> | |
| ); | |
| const { session, initialized } = useAuth(); | |
| const segments = useSegments(); | |
| const router = useRouter(); | |
| if (!initialized) { | |
| return null; | |
| } | |
| useEffect(() => { | |
| const inAuthGroup = segments[0] === '(auth)'; | |
| if (!session && !inAuthGroup) { | |
| router.replace('/(auth)/welcome'); | |
| } else if (session && inAuthGroup) { | |
| router.replace('/'); | |
| } | |
| }, [session, initialized, segments, router]); | |
| return ( | |
| <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> | |
| <Stack screenOptions={{ headerShown: false }}> | |
| <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> | |
| <Stack.Screen name="(auth)" options={{ headerShown: false }} /> | |
| </Stack> | |
| </ThemeProvider> | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mobile/app/_layout.tsx` around lines 19 - 42, The RootLayoutNav component
renders the Stack navigator even when initialized is false, causing a flash of
protected content before the useEffect redirect logic executes. Gate the entire
rendering of the ThemeProvider and Stack components behind a check for the
initialized flag - only render the navigation tree when initialized is true,
otherwise return null or a loading indicator to prevent premature rendering of
protected tabs.
| <TouchableOpacity style={styles.forgotPassword}> | ||
| <Text style={[styles.forgotText, { color: theme.primary }]}>Forgot password?</Text> | ||
| </TouchableOpacity> |
There was a problem hiding this comment.
Wire or remove the “Forgot password?” action.
Line 68 renders a tappable control with no handler, so it is currently a dead action.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mobile/app/`(auth)/login.tsx around lines 68 - 70, The TouchableOpacity
component rendering the "Forgot password?" text has no onPress handler, making
it a non-functional control. Either add an onPress prop to the TouchableOpacity
that navigates to a password reset/recovery screen (or triggers appropriate
password recovery flow), or remove the entire TouchableOpacity component and its
child Text if the forgot password functionality is not currently implemented.
| <View style={styles.socialBlock}> | ||
| <SocialButton title="Continue with Google" icon="G" /> | ||
| <SocialButton title="Continue with Phone" icon="📱" /> | ||
| </View> |
There was a problem hiding this comment.
Avoid shipping social sign-up buttons as no-op controls.
These CTAs look interactive but have no onPress, which is misleading for users.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mobile/app/`(auth)/signup.tsx around lines 62 - 65, The SocialButton
components for "Continue with Google" and "Continue with Phone" are missing
onPress event handlers, creating non-functional UI elements that appear
interactive. Add onPress callbacks to both SocialButton instances in the
socialBlock that implement the appropriate authentication or navigation logic
for each provider, ensuring users can actually interact with these buttons.
| const handleLogout = async () => { | ||
| await supabase.auth.signOut(); | ||
| }; |
There was a problem hiding this comment.
Handle sign-out errors explicitly.
At Line 8, logout failures are currently silent. Check the returned error so users aren’t left signed in without feedback.
Suggested fix
import { ThemedView } from '`@/components/themed-view`';
import { ThemedText } from '`@/components/themed-text`';
-import { StyleSheet, Button } from 'react-native';
+import { StyleSheet, Button, Alert } from 'react-native';
import { supabase } from '`@/lib/supabase`';
export default function HomeScreen() {
const handleLogout = async () => {
- await supabase.auth.signOut();
+ const { error } = await supabase.auth.signOut();
+ if (error) {
+ Alert.alert('Logout failed', error.message);
+ }
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mobile/app/`(tabs)/index.tsx around lines 7 - 9, The handleLogout function
currently ignores errors returned by supabase.auth.signOut(), which means logout
failures occur silently without user feedback. Modify the handleLogout function
to capture the result of the supabase.auth.signOut() call and destructure or
check the error property from the returned object. If an error exists, handle it
appropriately by logging it and providing user feedback (such as displaying an
error message) instead of allowing the logout to fail silently.
| useEffect(() => { | ||
| supabase.auth.getSession().then(({ data: { session } }) => { | ||
| setSession(session); | ||
| setUser(session?.user ?? null); | ||
| setInitialized(true); | ||
| }); | ||
|
|
||
| const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { | ||
| setSession(session); | ||
| setUser(session?.user ?? null); | ||
| }); | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the useAuth hook file
find . -name "useAuth.tsx" -type fRepository: ankushchk/kryze
Length of output: 90
🏁 Script executed:
cat -n ./mobile/src/hooks/useAuth.tsxRepository: ankushchk/kryze
Length of output: 1636
🏁 Script executed:
# Search for other usages of getSession to understand its behavior
rg "getSession" --type ts --type tsx -B 2 -A 2Repository: ankushchk/kryze
Length of output: 87
🏁 Script executed:
# Search for getSession with different approach
rg "getSession" -B 2 -A 2Repository: ankushchk/kryze
Length of output: 355
🌐 Web query:
Supabase getSession() error handling rejection promise
💡 Result:
The Supabase Auth getSession() method does not throw a promise rejection; instead, it returns an object containing both a session and an error property [1][2]. You should handle errors by checking the error property rather than using a try-catch block for promise rejections [1][2]. Key aspects of handling getSession() results: 1. Method Signature: The method returns a Promise that resolves to an object: { data: { session: Session | null }, error: ApiError | null } [1][2]. 2. Error Handling: Always check for the presence of the error object [3]. If error is present, it indicates that the session could not be retrieved or refreshed successfully (e.g., due to an expired or revoked refresh token) [1][4]. 3. Session Preservation: As of June 2026, recent updates to the Supabase JS SDK have improved how getSession() handles failed token refreshes [5][6]. If a refresh fails but the current access token is still valid (within its expiry window), the SDK now preserves the session instead of returning an error or clearing storage [5][6]. This prevents unnecessary user logouts during transient network or refresh failures [5]. 4. Recommended Usage: - For client-side apps, checking error is sufficient to determine if the user is authenticated [3]. - For server-side contexts (like Next.js SSR), you should prioritize getUser() over getSession() [2][7][8]. getUser() makes an API call to verify the JWT against the Supabase Auth server, whereas getSession() only reads from local storage or cookies, which can be insecure or stale [8]. Example pattern: const { data, error } = await supabase.auth.getSession; if (error) { // Handle the error (e.g., log, redirect to login, or clear local state) console.error('Error fetching session:', error.message); return; } if (data.session) { // Session is valid console.log('Session retrieved:', data.session); } else { // No session found, user is likely logged out console.log('No active session'); }
Citations:
- 1: supabase/supabase-js@60129091
- 2: https://supabase.com/docs/reference/javascript/auth-getsession
- 3: getSession() does not properly retrieve session in React Native supabase/supabase#17016
- 4: Bug: _callRefreshToken permanently deletes session on non-retryable refresh failure, even when access token is still valid supabase/supabase-js#2145
- 5: fix(auth): preserve valid session on refresh failure and cooldown repeat failures supabase/supabase-js#2436
- 6: fix(auth): preserve valid session on refresh failure and cooldown repeat failures supabase/supabase-js#2430
- 7: https://stackoverflow.com/questions/78518201/how-to-properly-use-supabase-auth-getsession
- 8: https://www.rapidevelopers.com/supabase-tutorial/how-to-do-server-side-auth-check-in-supabase
Check error property from getSession() and handle state cleanup on unmount.
The getSession() promise doesn't reject—it resolves with an object containing both data and error properties. The current code doesn't check the error property, so initialization failures are silently ignored. Additionally, state updates can occur after component unmount when getSession() completes asynchronously, causing memory leak warnings.
Use a cleanup flag to prevent state updates after unmount and handle the error case:
Suggested fix
useEffect(() => {
+ let active = true;
+ (async () => {
- supabase.auth.getSession().then(({ data: { session } }) => {
+ const { data: { session }, error } = await supabase.auth.getSession();
+ if (!active) return;
setSession(session);
setUser(session?.user ?? null);
+ })().catch(err => {
+ if (active) console.error('Failed to fetch session:', err);
+ }).finally(() => {
+ if (active) setInitialized(true);
});
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
+ if (!active) return;
setSession(session);
setUser(session?.user ?? null);
});
return () => {
+ active = false;
subscription.unsubscribe();
};
}, []);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| supabase.auth.getSession().then(({ data: { session } }) => { | |
| setSession(session); | |
| setUser(session?.user ?? null); | |
| setInitialized(true); | |
| }); | |
| const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { | |
| setSession(session); | |
| setUser(session?.user ?? null); | |
| }); | |
| useEffect(() => { | |
| let active = true; | |
| (async () => { | |
| const { data: { session }, error } = await supabase.auth.getSession(); | |
| if (!active) return; | |
| setSession(session); | |
| setUser(session?.user ?? null); | |
| })().catch(err => { | |
| if (active) console.error('Failed to fetch session:', err); | |
| }).finally(() => { | |
| if (active) setInitialized(true); | |
| }); | |
| const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { | |
| if (!active) return; | |
| setSession(session); | |
| setUser(session?.user ?? null); | |
| }); | |
| return () => { | |
| active = false; | |
| subscription.unsubscribe(); | |
| }; | |
| }, []); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mobile/src/hooks/useAuth.tsx` around lines 22 - 33, The useEffect hook in
useAuth.tsx needs to handle both error checking and cleanup on unmount. The
getSession() call returns an object with both data and error properties, so you
need to check if the error property exists and handle initialization failures
instead of silently ignoring them. Additionally, add a cleanup flag (like a
mounted or isMounted ref) that gets set to false in a cleanup function returned
from the useEffect, and check this flag before calling setSession, setUser, and
setInitialized to prevent state updates after component unmount. Return a
cleanup function from the useEffect that sets this flag and unsubscribes from
the onAuthStateChange subscription to fully prevent memory leaks.
| setItem: (key: string, value: string) => { | ||
| SecureStore.setItemAsync(key, value); | ||
| }, | ||
| removeItem: (key: string) => { | ||
| SecureStore.deleteItemAsync(key); | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find mobile -name "supabase.ts" -o -name "supabase.js" 2>/dev/null | head -20Repository: ankushchk/kryze
Length of output: 86
🏁 Script executed:
cd mobile && find . -name "*.ts" -o -name "*.tsx" | grep -i supabase | head -20Repository: ankushchk/kryze
Length of output: 81
🏁 Script executed:
git ls-files mobile | grep -i supabaseRepository: ankushchk/kryze
Length of output: 86
🏁 Script executed:
cat -n mobile/src/lib/supabase.tsRepository: ankushchk/kryze
Length of output: 973
🏁 Script executed:
rg -A 10 -B 5 "ExpoSecureStoreAdapter|setItem|removeItem" mobile/src/lib/supabase.tsRepository: ankushchk/kryze
Length of output: 777
🏁 Script executed:
rg "ExpoSecureStoreAdapter" mobile --type ts --type tsxRepository: ankushchk/kryze
Length of output: 87
🌐 Web query:
Supabase storage adapter interface TypeScript getItem setItem removeItem promises
💡 Result:
In Supabase, the storage adapter used for authentication persistence (session management) follows the SupportedStorage interface [1][2][3]. This interface allows you to implement custom storage logic (e.g., for SSR, mobile environments, or non-browser contexts) [4][2][5]. The SupportedStorage interface defines three methods [2][3]: getItem(key: string): string | null | Promise<string | null>; setItem(key: string, value: string): void | Promise; removeItem(key: string): void | Promise; Key Implementation Details: 1. Asynchronous Support: While many browser-based implementations are synchronous (like standard localStorage), the interface explicitly allows these methods to return Promises [4][2]. This is essential for environments like React Native (using AsyncStorage) or server-side environments where storage access may be asynchronous [2][5]. 2. Serialization: The Supabase Auth client handles the serialization of session data (JSON.stringify/parse) [3], but the storage adapter is responsible for storing and retrieving the raw string value associated with the provided key [2]. 3. Usage: You can inject a custom adapter when initializing the Supabase client under the auth.storage configuration option: import { createClient } from '@supabase/supabase-js'; const customStorage = { getItem: (key) => { /* logic to retrieve string / }, setItem: (key, value) => { / logic to store string / }, removeItem: (key) => { / logic to delete key */ }, }; const supabase = createClient('URL', 'KEY', { auth: { storage: customStorage, }, }); Important Note: This interface is specific to the Auth client's session persistence [1][3]. It is separate from the Supabase Storage service used for uploading and managing user files (buckets), which uses a different StorageClient API [6][7]. The file storage service does not use this getItem/setItem interface [8][9].
Citations:
- 1: https://github.com/supabase/supabase-js/blob/bd024171/packages/core/auth-js/src/lib/local-storage.ts
- 2: Storage gets cleared every other day (iOS) supabase/supabase-js#1317
- 3: supabase/supabase-js@93722840
- 4: https://github.com/supabase/auth-helpers/blob/main/packages/shared/src/cookieAuthStorageAdapter.ts
- 5: https://github.com/supabase/supabase/blob/c104beabb6edb4ddf606e248dc092254cddc9d8a/examples/auth/expo-social-auth/lib/supabase.web.ts
- 6: https://github.com/supabase/supabase-js/blob/develop/packages/core/storage-js/README.md
- 7: https://www.npmjs.com/package/@supabase/storage-js?activeTab=explore
- 8: https://github.com/supabase/supabase-js/blob/bd024171/packages/core/storage-js/src/packages/StorageFileApi.ts
- 9: https://github.com/supabase/storage-js/blob/main/src/packages/StorageFileApi.ts
🌐 Web query:
Expo SecureStore setItemAsync deleteItemAsync return value promise
💡 Result:
Both setItemAsync and deleteItemAsync in the Expo SecureStore library return a Promise [1][2]. For setItemAsync(key, value, options): The returned promise resolves when the key-value pair is successfully stored [1]. It rejects if the value cannot be stored on the device (e.g., due to size limitations or other underlying system errors) [1][3]. For deleteItemAsync(key, options): The returned promise resolves when the operation is complete [2]. It rejects if the value cannot be deleted [2]. In both cases, because they return promises, you should use async/await or.then/.catch patterns to handle the results and potential errors [1][2].
Citations:
- 1: https://docs.expo.dev/versions/latest/sdk/securestore/
- 2: https://docs.expo.dev/versions/v55.0.0/sdk/securestore/
- 3: https://docs.expo.dev/versions/v56.0.0/sdk/securestore
Return the SecureStore promises from the adapter methods.
On lines 10 and 13, setItemAsync and deleteItemAsync are invoked but not returned, making storage mutations fire-and-forget. Since persistSession and autoRefreshToken are enabled, Supabase expects to await these operations during token refresh. Without returning the promises, you create a race condition where session data may not be fully persisted before the auth client proceeds, causing potential session loss.
Suggested fix
const ExpoSecureStoreAdapter = {
getItem: (key: string) => {
return SecureStore.getItemAsync(key);
},
setItem: (key: string, value: string) => {
- SecureStore.setItemAsync(key, value);
+ return SecureStore.setItemAsync(key, value);
},
removeItem: (key: string) => {
- SecureStore.deleteItemAsync(key);
+ return SecureStore.deleteItemAsync(key);
},
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| setItem: (key: string, value: string) => { | |
| SecureStore.setItemAsync(key, value); | |
| }, | |
| removeItem: (key: string) => { | |
| SecureStore.deleteItemAsync(key); | |
| }, | |
| setItem: (key: string, value: string) => { | |
| return SecureStore.setItemAsync(key, value); | |
| }, | |
| removeItem: (key: string) => { | |
| return SecureStore.deleteItemAsync(key); | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mobile/src/lib/supabase.ts` around lines 9 - 14, The setItem and removeItem
methods in the storage adapter are calling SecureStore.setItemAsync and
SecureStore.deleteItemAsync but not returning the promises they produce. This
creates a race condition where Supabase's persistSession and autoRefreshToken
features cannot await these operations. Add return statements before each
SecureStore async call in both the setItem method (before
SecureStore.setItemAsync) and the removeItem method (before
SecureStore.deleteItemAsync) so that the promises are properly returned and can
be awaited by the Supabase client.
| export const supabase = createClient( | ||
| process.env.EXPO_PUBLIC_SUPABASE_URL!, | ||
| process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!, | ||
| { |
There was a problem hiding this comment.
Fail fast with explicit env validation instead of non-null assertions.
Line 18 and Line 19 rely on !; if either env var is missing, startup fails with a less-actionable runtime error. Validate once and throw a clear message.
Suggested fix
+const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
+const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;
+
+if (!supabaseUrl || !supabaseAnonKey) {
+ throw new Error('Missing EXPO_PUBLIC_SUPABASE_URL or EXPO_PUBLIC_SUPABASE_ANON_KEY');
+}
+
export const supabase = createClient(
- process.env.EXPO_PUBLIC_SUPABASE_URL!,
- process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
+ supabaseUrl,
+ supabaseAnonKey,
{ 🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mobile/src/lib/supabase.ts` around lines 17 - 20, The createClient call in
the supabase initialization uses non-null assertions on the environment
variables EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY, which
results in unclear runtime errors if they are missing. Replace these non-null
assertions with explicit validation that checks if both environment variables
are defined before the createClient call, and throw a descriptive error message
that clearly states which variable is missing if validation fails. This ensures
the application fails fast with actionable feedback during startup.
Screen.Recording.2026-06-19.at.7.43.06.PM.mov
Summary by CodeRabbit
Release Notes
New Features
Chores