Simplifying auth flows in Expo Router with protected routes
Development••4 minutes read
Kadi Kraman
Engineering
How to use the new protected routes to control which screens users can see based on their authentication state - directly in your layout files.

With the release of Expo SDK 53, Expo Router introduced a powerful new feature for managing authentication flows: protected routes. This long-awaited addition greatly simplifies access control for the logged in and logged out screens of your app.
No more juggling redirects and grouping folders: in this post we’ll look at how to use protected routes to control which screens users can see based on their authentication state - directly in your layout files.
What are protected routes?
Both Stack and Tabs now include a Protected component, to be used within your layout files. This Protected component takes a guard prop: when guard={true} then the screen can be accessed, and if guard={false}, it cannot.
If the user attempts to navigate to a screen that is guarded, the navigation fails silently.
If the user was already on the screen that becomes guarded, Router will open the next available screen instead.
Using protected routes for logged in and logged out views
Here’s a folder structure for an app with a login flow. We want our logged in users to see a bottom tabs layout, and logged out users to either sign in or create an account.
/app/(tabs)_layout.tsxindex.tsxsettings.tsx_layout.tsxsign-in.tsxcreate-account.tsx
In the root _layout.tsx file, we can now wrap the two sets of screens in protected routes and guard them accordingly:
import { Stack } from "expo-router";import { useAuthState } from "@/utils/authState";export default function RootLayout() {const { isLoggedIn } = useAuthState();return (<Stack><Stack.Protected guard={isLoggedIn}><Stack.Screen name="(tabs)" /></Stack.Protected><Stack.Protected guard={!isLoggedIn}><Stack.Screen name="sign-in" /><Stack.Screen name="create-account" /></Stack.Protected></Stack>);}
Now when the app first launches, Router will attempt to open the index route which is within the tabs layout. If the user is not logged in, then this stack will be unavailable and we will fall back to the first available screen: in this case the sign-in screen.
Screens outside of the protected groups will be accessible to all users.
Nesting protected routes
Protected route groups may be nested, which can be useful for combining rules.
In this case we would like the user to be taken directly the the create-account page if they need to create an account, otherwise they should go to the sign in page.
import { Stack } from "expo-router";import { useAuthState } from "@/utils/authState";export default function RootLayout() {const { isLoggedIn, shouldCreateAccount } = useAuthState();return (<Stack><Stack.Protected guard={isLoggedIn}><Stack.Screen name="(tabs)" /></Stack.Protected><Stack.Protected guard={!isLoggedIn}><Stack.Protected guard={shouldCreateAccount}><Stack.Screen name="create-account" /></Stack.Protected><Stack.Screen name="sign-in" /></Stack.Protected></Stack>);}
We can do this by moving the create-account screen to be first in the logged out group, and adding another protected route group around it.
Note that you may also combine protected routes with redirects when appropriate.
Logged in modals
Another benefit of protected routes is being able to easily configure which modals should be visible for logged in vs logged out users. For example if we added a modal directly in the app folder, all we need to do now to ensure only logged in users can access it, is to add it to the logged in group:
import { Stack } from "expo-router";import { useAuthState } from "@/utils/authState";export default function RootLayout() {const { isLoggedIn, shouldCreateAccount } = useAuthState();return (<Stack><Stack.Protected guard={isLoggedIn}><Stack.Screen name="(tabs)" /><Stack.Screen name="modal" /></Stack.Protected><Stack.Protected guard={!isLoggedIn}><Stack.Protected guard={shouldCreateAccount}><Stack.Screen name="create-account" /></Stack.Protected><Stack.Screen name="sign-in" /></Stack.Protected></Stack>);}
This ensures that unauthenticated users cannot navigate to it, even if they deep link to the modal directly.
Conditional bottom tabs
For a bottom tabs layout, it is now very straightforward to show and hide a tab for some users based on a condition. For example, let’s create a VIP tab bar to show to our VIP users only.
Now we'll wrap it in a Tabs.Protected and ensure it is only shown when the user is a VIP:
import { Stack } from "expo-router";import { useAuthState } from "@/utils/authState";export default function TabsLayout() {const { isVip } = useAuthState();return (<Tabs><Tabs.Screen name="index" /><Tabs.Protected guard={isVip}><Tabs.Screen name="vip" /></Tabs.Protected><Tabs.Screen name="settings" /></Tabs>);}
This ensures that the vip tab only appears for users with the right permissions.
Summary
Expo Router’s new protected routes offer a cleaner, more declarative way to manage access in your navigation structure. By using Stack.Protected and Tabs.Protected directly in your layout files, you can:
- Define auth flows clearly and predictably
- Avoid imperative redirects
- Easily control nested routes and modals
- Dynamically show/hide bottom tabs
This simplifies your codebase and brings your navigation closer to your app’s logic.
Refer to the protected routes documentation for more details.



