# How to Protect Routes in Expo Router with Clerk

_This is Part 1 of a two-part series on protecting routes in Expo Router with Clerk. In this part, we cover the core setup, route group architecture, and building essential authentication screens. In [Part 2](https://clerk.com/articles/how-to-protect-routes-in-expo-router-with-clerk-2.md), we explore advanced patterns like role-based access control, tab navigators, and deep linking._

To protect routes in Expo Router with Clerk, split your `app/` directory into `(auth)` and `(app)` route groups, wrap the root layout in `<ClerkProvider>` with Clerk's `tokenCache`, then add a layout-level guard in each group's `_layout.tsx` that checks `isSignedIn` from `useAuth()`. Use `<Redirect>` to send unauthenticated users to `/sign-in` and signed-in users away from auth screens. Always wait for `isLoaded === true` before redirecting to avoid the flash-of-wrong-screen problem.

Expo SDK 53+ also offers `Stack.Protected`, a declarative alternative that automatically cleans up navigation history when a guard fails. This guide walks through both patterns while building an Expo Router app with Clerk [authentication](https://clerk.com/docs/guides/how-clerk-works/overview.md). Part 1 covers the foundation: public and private route groups, the `<ClerkProvider>` setup, and the core sign-in, sign-up, and protected screens. [Part 2](https://clerk.com/articles/how-to-protect-routes-in-expo-router-with-clerk-2.md) builds on it with [role-based access control](https://clerk.com/docs/guides/organizations/control-access/roles-and-permissions.md), feature-based [authorization](https://clerk.com/docs/guides/secure/authorization-checks.md), deep linking, and the most common pitfalls that trip up mobile developers.

### What you'll build

In this part, you'll build the foundation of an Expo Router app with:

- Sign-up and sign-in screens using Clerk's Core 3 Signal API
- Route protection with both `Stack.Protected` and the `useAuth()` + `<Redirect>` pattern
- A protected dashboard and user profile screen

[Part 2](https://clerk.com/articles/how-to-protect-routes-in-expo-router-with-clerk-2.md) extends this foundation with role-based access control, protected tab navigators, and deep linking.

**Tech stack**: Expo SDK 53+, Expo Router v5+, `@clerk/expo` v3+, TypeScript

### Prerequisites

- React and React Native fundamentals
- Node.js 18+ (20 LTS recommended)
- An [Expo development environment](https://docs.expo.dev/get-started/set-up-your-environment/)
- A [Clerk account](https://clerk.com/pricing) (free tier works)

> Expo SDK 55+ requires New Architecture (Legacy Architecture was removed). New Architecture has been the default since SDK 53.

## How Expo Router's file-based routing works

Expo Router maps files in the `app/` directory to navigation routes. Each file becomes a screen. Layout files (`_layout.tsx`) define the navigation structure for their directory and all child routes.

**Route groups** use parentheses to organize routes without affecting URLs. A file at `app/(app)/dashboard.tsx` produces the URL `/dashboard`, not `/(app)/dashboard`. This is the key feature that makes auth-based routing work: you can split your app into `(auth)` and `(app)` groups with different navigation rules.

```
app/
├── _layout.tsx              # Root layout: ClerkProvider + route guards
├── (auth)/
│   ├── _layout.tsx          # Stack navigator for auth screens
│   ├── sign-in.tsx          # Sign-in screen
│   └── sign-up.tsx          # Sign-up screen
└── (app)/
    ├── _layout.tsx          # Tab navigator with auth guard
    ├── index.tsx            # Dashboard (Home tab)
    └── profile.tsx          # User profile tab
```

Expo Router supports **Stack** navigators for hierarchical push/pop navigation, **Tab** navigators for top-level sections, and nesting them together. The `<Redirect>` component handles declarative navigation, while `useRouter()` gives you programmatic control with `router.push()`, `router.replace()`, and `router.back()`.

## Setting up Clerk with Expo Router

### Install dependencies

Create a new Expo project and install the required packages.

```bash
npx create-expo-app@latest my-auth-app
cd my-auth-app
npx expo install @clerk/expo expo-secure-store
```

### Configure [environment variables](https://clerk.com/docs/guides/development/clerk-environment-variables.md)

Create a `.env` file in the project root with your Clerk publishable key.

```
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your-key-here
```

Find your key on the [API keys](https://dashboard.clerk.com/~/api-keys) page in the Clerk Dashboard. You also need to enable the Native API on the [Native applications](https://dashboard.clerk.com/~/native-applications) page — a commonly missed step that the native `@clerk/expo` sign-in flows depend on.

### Configure ClerkProvider in the root layout

Add `ClerkProvider` to `app/_layout.tsx`. The `tokenCache` from `@clerk/expo/token-cache` encrypts and persists [session tokens](https://clerk.com/docs/guides/sessions/customize-session-tokens.md) on-device using `expo-secure-store` (iOS Keychain, Android Keystore). This means authentication state survives app restarts without requiring the user to sign in again.

**`app/_layout.tsx`**

```typescript
import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { Slot } from 'expo-router'
import * as SplashScreen from 'expo-splash-screen'

// Call at module scope (inside a component risks being too late)
SplashScreen.preventAutoHideAsync()

export default function RootLayout() {
  return (
    <ClerkProvider
      publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!}
      tokenCache={tokenCache}
    >
      <Slot />
    </ClerkProvider>
  )
}
```

Calling `SplashScreen.preventAutoHideAsync()` at **module scope** (outside the component function) is important. Calling it inside the component risks running after the splash screen has already been dismissed.

### Handle authentication loading state

Clerk's SDK needs time to restore the session from secure storage. During this window, `isLoaded` from `useAuth()` is `false`, and checking `isSignedIn` would give unreliable results. You can use the `ClerkLoaded` and `ClerkLoading` components as an alternative to checking `isLoaded` directly.

```typescript
import { ClerkLoaded, ClerkLoading, ClerkProvider } from '@clerk/expo'
import { ActivityIndicator, View } from 'react-native'

export default function RootLayout() {
  return (
    <ClerkProvider>
      <ClerkLoading>
        <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
          <ActivityIndicator size="large" />
        </View>
      </ClerkLoading>
      <ClerkLoaded>
        <Slot />
      </ClerkLoaded>
    </ClerkProvider>
  )
}
```

## Protecting routes: public vs private

### Route group architecture

Split your app into two route groups:

- `(auth)/` contains sign-in, sign-up, and other public screens
- `(app)/` contains the dashboard, profile, and all other protected screens

Each group has its own `_layout.tsx` that enforces access rules. The parentheses mean these group names never appear in URLs.

### Stack.Protected: the recommended approach

`Stack.Protected` (available since Expo SDK 53 / Router v5) accepts a boolean `guard` prop. When `guard` is `false`, those screens become inaccessible and the user redirects to the **anchor route**, the nearest accessible screen. It also automatically cleans up navigation history when a screen becomes protected.

**`app/_layout.tsx`** (with route guards)

```typescript
import { ClerkProvider, useAuth } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { Stack } from 'expo-router'
import * as SplashScreen from 'expo-splash-screen'

SplashScreen.preventAutoHideAsync()

function RootNavigator() {
  const { isSignedIn, isLoaded } = useAuth()

  if (!isLoaded) return null

  SplashScreen.hideAsync()

  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Protected guard={isSignedIn === true}>
        <Stack.Screen name="(app)" />
      </Stack.Protected>
      <Stack.Protected guard={isSignedIn === false}>
        <Stack.Screen name="(auth)" />
      </Stack.Protected>
    </Stack>
  )
}

export default function RootLayout() {
  return (
    <ClerkProvider
      publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!}
      tokenCache={tokenCache}
    >
      <RootNavigator />
    </ClerkProvider>
  )
}
```

When `isSignedIn` is `true`, the `(app)` group is accessible and `(auth)` is blocked. When `isSignedIn` is `false`, the opposite applies. This dual-guard pattern handles both directions: preventing unauthenticated users from reaching protected screens, and preventing authenticated users from seeing login screens.

> `Stack.Protected` is client-side only. It controls navigation, not data access. Always validate authentication and authorization on your server.

> `Stack.Protected` has known platform-specific issues. On iOS, the protected screen may briefly appear before the guard redirects ([expo/expo #37305](https://github.com/expo/expo/issues/37305)). On web, routes with the same name in different protected groups can conflict ([expo/expo #37816](https://github.com/expo/expo/issues/37816)), and redirects may leave the wrong path in the browser address bar ([expo/expo #38387](https://github.com/expo/expo/issues/38387)). If you hit these, the `useAuth()` + `Redirect` approach described next is a reliable alternative.

### Alternative: useAuth + Redirect

For more control over redirect behavior, or for projects on older SDK versions, use `useAuth()` with the `<Redirect>` component in each group's layout.

In the `(app)` layout, redirect unauthenticated users to sign-in:

**`app/(app)/_layout.tsx`** (alternative approach)

```typescript
import { useAuth } from '@clerk/expo'
import { Redirect, Tabs } from 'expo-router'

export default function AppLayout() {
  const { isSignedIn, isLoaded } = useAuth()

  if (!isLoaded) return null

  if (!isSignedIn) {
    return <Redirect href="/(auth)/sign-in" />
  }

  return (
    <Tabs screenOptions={{ headerShown: true }}>
      <Tabs.Screen name="index" options={{ title: 'Home' }} />
      <Tabs.Screen name="profile" options={{ title: 'Profile' }} />
    </Tabs>
  )
}
```

In the `(auth)` layout, redirect authenticated users to the dashboard:

**`app/(auth)/_layout.tsx`**

```typescript
import { useAuth } from '@clerk/expo'
import { Redirect, Stack } from 'expo-router'

export default function AuthLayout() {
  const { isSignedIn, isLoaded } = useAuth()

  if (!isLoaded) return null

  if (isSignedIn) {
    return <Redirect href="/(app)" />
  }

  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="sign-in" />
      <Stack.Screen name="sign-up" />
    </Stack>
  )
}
```

The critical detail: **always check `isLoaded` before `isSignedIn`**. Skipping this check causes premature redirects on every cold start.

Here's how the two approaches compare side by side:

|                  | Stack.Protected          | useAuth + Redirect        |
| ---------------- | ------------------------ | ------------------------- |
| SDK requirement  | Expo SDK 53+             | Any version               |
| History cleanup  | Automatic                | Manual (`router.replace`) |
| Code location    | Root layout              | Each group layout         |
| Redirect control | Anchor route (automatic) | Full control over target  |
| Platform issues  | Known iOS/web bugs       | Stable across platforms   |

### Show component for conditional UI

The `<Show>` component from `@clerk/expo` conditionally renders UI elements based on authentication or authorization state. It handles rendering within a screen, **not route-level protection**. Always use layout guards for route protection.

```typescript
import { Show } from '@clerk/expo'

export default function Header() {
  return (
    <View>
      <Show when="signed-in">
        <UserAvatar />
      </Show>
      <Show when="signed-out">
        <SignInButton />
      </Show>
    </View>
  )
}
```

`Show` also supports authorization checks: `when={{ role: 'org:admin' }}`, `when={{ permission: 'org:posts:edit' }}`, `when={{ plan: 'premium' }}`, and `when={{ feature: 'premium_access' }}`. Plans and features use plain strings. Roles and permissions require an active Organization with the `org:` prefix.

The `treatPendingAsSignedOut` prop controls what happens during **pending sessions** (when a user has authenticated but hasn't completed required session tasks like selecting an organization). By default it's `true`, meaning pending users see the signed-out fallback content. Set it to `false` to show the signed-in content for pending users instead.

> `<Show>` only controls rendering on the client. It does not enforce access at the data level. For sensitive data, perform authorization checks on the server.

## Building the authentication screens

All examples use Clerk's **Core 3 Signal API**. Many existing tutorials online show the legacy `signIn.create()` / `setActive()` pattern, which is deprecated. The examples below use the current API with `signIn.password()` / `finalize()`.

The `finalize()` method accepts an optional `navigate` callback that controls where the user goes after authentication completes. Clerk passes `{ session, decorateUrl }` to the callback, where `session` is the newly created session and `decorateUrl` handles web-specific token management. In Expo apps, you can ignore these parameters and navigate directly.

### Sign-up screen

The sign-up flow has two phases: registration and email verification.

**`app/(auth)/sign-up.tsx`**

```typescript
import { useSignUp } from '@clerk/expo'
import { useRouter } from 'expo-router'
import { useState } from 'react'
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  ActivityIndicator,
  StyleSheet,
} from 'react-native'

export default function SignUpScreen() {
  const { signUp, errors, fetchStatus } = useSignUp()
  const router = useRouter()
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [code, setCode] = useState('')
  const [pendingVerification, setPendingVerification] = useState(false)

  const handleSignUp = async () => {
    await signUp.password({ emailAddress: email, password })
    await signUp.verifications.sendEmailCode()
    setPendingVerification(true)
  }

  const handleVerification = async () => {
    await signUp.verifications.verifyEmailCode({ code })
    await signUp.finalize({ navigate: () => router.replace('/(app)') })
  }

  if (pendingVerification) {
    return (
      <View style={styles.container}>
        <Text style={styles.title}>Verify your email</Text>
        <TextInput
          value={code}
          onChangeText={setCode}
          placeholder="Enter verification code"
          keyboardType="number-pad"
          style={styles.input}
        />
        {errors?.fields?.code && <Text style={styles.error}>{errors.fields.code.message}</Text>}
        {fetchStatus === 'fetching' ? (
          <ActivityIndicator />
        ) : (
          <TouchableOpacity style={styles.button} onPress={handleVerification}>
            <Text style={styles.buttonText}>Verify</Text>
          </TouchableOpacity>
        )}
      </View>
    )
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Create an account</Text>
      <TextInput
        value={email}
        onChangeText={setEmail}
        placeholder="Email"
        autoCapitalize="none"
        keyboardType="email-address"
        style={styles.input}
      />
      {errors?.fields?.emailAddress && (
        <Text style={styles.error}>{errors.fields.emailAddress.message}</Text>
      )}
      <TextInput
        value={password}
        onChangeText={setPassword}
        placeholder="Password"
        secureTextEntry
        style={styles.input}
      />
      {errors?.fields?.password && (
        <Text style={styles.error}>{errors.fields.password.message}</Text>
      )}
      {fetchStatus === 'fetching' ? (
        <ActivityIndicator />
      ) : (
        <TouchableOpacity style={styles.button} onPress={handleSignUp}>
          <Text style={styles.buttonText}>Sign Up</Text>
        </TouchableOpacity>
      )}
      <TouchableOpacity onPress={() => router.push('/(auth)/sign-in')}>
        <Text style={styles.link}>Already have an account? Sign in</Text>
      </TouchableOpacity>
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24, justifyContent: 'center' },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 24 },
  input: {
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 8,
    padding: 12,
    marginBottom: 12,
    fontSize: 16,
  },
  button: {
    backgroundColor: '#6C47FF',
    padding: 16,
    borderRadius: 8,
    alignItems: 'center',
  },
  buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
  error: { color: '#ef4444', marginBottom: 8, fontSize: 14 },
  link: { color: '#6C47FF', marginTop: 16, textAlign: 'center' },
})
```

The Core 3 API handles errors reactively through the `errors.fields` object. When `signUp.password()` encounters validation issues (like an invalid email or weak password), `errors.fields` updates automatically and the component re-renders with the error messages displayed. No try/catch needed for validation errors.

The `fetchStatus` value switches between `'idle'` and `'fetching'`, so you can show a loading indicator while the API call is in flight.

### Sign-in screen

**`app/(auth)/sign-in.tsx`**

```typescript
import { useSignIn } from '@clerk/expo'
import { useRouter } from 'expo-router'
import { useState } from 'react'
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  ActivityIndicator,
  StyleSheet,
} from 'react-native'

export default function SignInScreen() {
  const { signIn, errors, fetchStatus } = useSignIn()
  const router = useRouter()
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  const handleSignIn = async () => {
    await signIn.password({ identifier: email, password })
    await signIn.finalize({ navigate: () => router.replace('/(app)') })
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Sign in</Text>
      <TextInput
        value={email}
        onChangeText={setEmail}
        placeholder="Email"
        autoCapitalize="none"
        keyboardType="email-address"
        style={styles.input}
      />
      {errors?.fields?.identifier && (
        <Text style={styles.error}>{errors.fields.identifier.message}</Text>
      )}
      <TextInput
        value={password}
        onChangeText={setPassword}
        placeholder="Password"
        secureTextEntry
        style={styles.input}
      />
      {errors?.fields?.password && (
        <Text style={styles.error}>{errors.fields.password.message}</Text>
      )}
      {fetchStatus === 'fetching' ? (
        <ActivityIndicator />
      ) : (
        <TouchableOpacity style={styles.button} onPress={handleSignIn}>
          <Text style={styles.buttonText}>Sign In</Text>
        </TouchableOpacity>
      )}
      <TouchableOpacity onPress={() => router.push('/(auth)/sign-up')}>
        <Text style={styles.link}>Don't have an account? Sign up</Text>
      </TouchableOpacity>
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24, justifyContent: 'center' },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 24 },
  input: {
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 8,
    padding: 12,
    marginBottom: 12,
    fontSize: 16,
  },
  button: {
    backgroundColor: '#6C47FF',
    padding: 16,
    borderRadius: 8,
    alignItems: 'center',
  },
  buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
  error: { color: '#ef4444', marginBottom: 8, fontSize: 14 },
  link: { color: '#6C47FF', marginTop: 16, textAlign: 'center' },
})
```

Using `router.replace()` in the `navigate` callback prevents the sign-in screen from remaining in the navigation stack. The callback receives `{ session, decorateUrl }` from Clerk, but in Expo apps you can navigate directly since `decorateUrl` is primarily for web cookie management. With `Stack.Protected`, `router.push()` is also safe because the guard automatically redirects authenticated users away from auth screens. Without route guards, `router.replace()` is the safer choice.

### Sign-out flow

**`components/SignOutButton.tsx`**

```typescript
import { useClerk } from '@clerk/expo'
import { useRouter } from 'expo-router'
import { TouchableOpacity, Text, StyleSheet } from 'react-native'

export function SignOutButton() {
  const { signOut } = useClerk()
  const router = useRouter()

  const handleSignOut = async () => {
    await signOut()
    router.replace('/(auth)/sign-in')
  }

  return (
    <TouchableOpacity style={styles.button} onPress={handleSignOut}>
      <Text style={styles.text}>Sign Out</Text>
    </TouchableOpacity>
  )
}

const styles = StyleSheet.create({
  button: { padding: 12 },
  text: { color: '#ef4444', fontSize: 16 },
})
```

With `Stack.Protected`, signing out triggers the guard change (`isSignedIn` flips to `false`), which automatically cleans up navigation history and redirects to the auth screens. The explicit `router.replace()` call acts as a fallback for setups that don't use `Stack.Protected`.

### [OAuth](https://clerk.com/docs/guides/configure/auth-strategies/oauth/overview.md) and social sign-in

For social [single sign-on](https://clerk.com/glossary/single-sign-on-sso.md), use the `useSSO()` hook (which replaces the deprecated `useOAuth()`):

```typescript
import { useSSO } from '@clerk/expo'

const { startSSOFlow } = useSSO()

const result = await startSSOFlow({ strategy: 'oauth_google' })

// SSO completion uses setActive(), not finalize()
if (result.createdSessionId) {
  await result.setActive({ session: result.createdSessionId })
}
```

Native OAuth (Google Sign-In, Apple Sign-In) and native Clerk components (`AuthView`, `UserButton`) require a [development build](https://docs.expo.dev/develop/development-builds/introduction/). Browser-based OAuth and the JavaScript-only flows shown above work in Expo Go. See the [full SSO documentation](https://clerk.com/docs/expo/reference/native-hooks/use-sso.md) for complete implementation details.

> The SSO flow uses `setActive({ session: createdSessionId })` to activate the session. This differs from the email/password flows, which use `finalize({ navigate })`. The `useSSO` hook uses legacy patterns internally. Don't try to use `finalize()` with SSO results.

## Building protected screens

### Dashboard

The dashboard sits behind the auth guard. Once the user is signed in, they land here.

**`app/(app)/index.tsx`**

```typescript
import { useAuth, useUser } from '@clerk/expo'
import { View, Text, StyleSheet } from 'react-native'
import { SignOutButton } from '../../components/SignOutButton'

export default function DashboardScreen() {
  const { userId, sessionId, getToken } = useAuth()
  const { user } = useUser()

  const fetchProtectedData = async () => {
    const token = await getToken()
    const response = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/data`, {
      headers: { Authorization: `Bearer ${token}` },
    })
    return response.json()
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>
        Welcome, {user?.firstName || user?.primaryEmailAddress?.emailAddress}
      </Text>
      <Text style={styles.detail}>User ID: {userId}</Text>
      <Text style={styles.detail}>Session: {sessionId}</Text>
      <SignOutButton />
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24 },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 16 },
  detail: { fontSize: 14, color: '#666', marginBottom: 8 },
})
```

Use `getToken()` from `useAuth()` to attach Clerk session tokens to API requests. Your backend validates these tokens using Clerk's backend SDK to ensure only authenticated users access your API.

### User profile

**`app/(app)/profile.tsx`**

```typescript
import { useUser } from '@clerk/expo'
import { View, Text, Image, StyleSheet } from 'react-native'
import { SignOutButton } from '../../components/SignOutButton'

export default function ProfileScreen() {
  const { user } = useUser()

  return (
    <View style={styles.container}>
      {user?.imageUrl && <Image source={{ uri: user.imageUrl }} style={styles.avatar} />}
      <Text style={styles.name}>{user?.fullName}</Text>
      <Text style={styles.email}>{user?.primaryEmailAddress?.emailAddress}</Text>
      <SignOutButton />
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24, alignItems: 'center' },
  avatar: { width: 100, height: 100, borderRadius: 50, marginBottom: 16 },
  name: { fontSize: 24, fontWeight: 'bold', marginBottom: 4 },
  email: { fontSize: 16, color: '#666', marginBottom: 16 },
})
```

For richer profile management, Clerk provides a native `UserProfileView` component from `@clerk/expo/native`. It renders a full profile editing UI using SwiftUI on iOS and Jetpack Compose on Android, but requires a development build.

## Next steps

You now have a solid foundation for protecting routes in your Expo Router application using Clerk. You've set up the `ClerkProvider`, implemented route guards using `Stack.Protected`, and built functional sign-in, sign-up, and protected dashboard screens.

In **[Part 2](https://clerk.com/articles/how-to-protect-routes-in-expo-router-with-clerk-2.md)** of this series, we build on this foundation to implement role-based access control (RBAC), secure tab navigators, handle deep links, and manage offline token persistence. We also cover the most common mistakes developers make when implementing authentication in Expo Router.

## In this series

1. **How to Protect Routes in Expo Router with Clerk** (you are here)
2. [How to Protect Routes in Expo Router with Clerk - Part 2](https://clerk.com/articles/how-to-protect-routes-in-expo-router-with-clerk-2.md)


## Series

- Part 2 of 2: https://clerk.com/articles/how-to-protect-routes-in-expo-router-with-clerk-2.md
