Build a Cross-Platform Mobile App with Expo, React Native, and TypeScript in 2026

One codebase, two platforms. Expo has become the default way to build React Native apps in 2026. With file-based routing, native module support, and over-the-air updates, it removes the complexity of mobile development while keeping full native capabilities. In this tutorial, you'll build a complete task management app that runs on both iOS and Android.
What You'll Build
A full-featured TaskFlow app — a task management application with:
- File-based navigation using Expo Router v4
- Beautiful UI with NativeWind (Tailwind CSS for React Native)
- Local data persistence with Expo SQLite
- Camera integration for task attachments
- Push notifications for task reminders
- Dark mode support
- Deployment-ready configuration
Prerequisites
Before starting, ensure you have:
- Node.js 20+ installed (
node --version) - A smartphone (iOS or Android) with the Expo Go app installed
- Basic knowledge of React and TypeScript
- VS Code with the Expo Tools extension (recommended)
- An Expo account (free at expo.dev)
Step 1: Create the Expo Project
Start by creating a new Expo project with the latest SDK:
npx create-expo-app@latest TaskFlow --template tabs
cd TaskFlowThis scaffolds a project with file-based routing already configured. Let's examine the structure:
TaskFlow/
├── app/ # File-based routes
│ ├── (tabs)/ # Tab layout group
│ │ ├── _layout.tsx # Tab navigator config
│ │ ├── index.tsx # Home tab
│ │ └── explore.tsx # Explore tab
│ ├── _layout.tsx # Root layout
│ └── +not-found.tsx # 404 page
├── assets/ # Images, fonts
├── components/ # Reusable components
├── constants/ # Theme, config
└── package.json
Install additional dependencies we'll need:
npx expo install expo-sqlite expo-camera expo-notifications expo-image-picker nativewind tailwindcssStep 2: Configure NativeWind (Tailwind CSS)
NativeWind brings Tailwind CSS utility classes to React Native. Set it up:
npx expo install nativewind tailwindcss react-native-reanimatedCreate tailwind.config.js:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,jsx,ts,tsx}",
"./components/**/*.{js,jsx,ts,tsx}",
],
presets: [require("nativewind/preset")],
theme: {
extend: {
colors: {
primary: "#6366f1",
secondary: "#8b5cf6",
},
},
},
plugins: [],
};Create global.css in the project root:
@tailwind base;
@tailwind components;
@tailwind utilities;Update babel.config.js to include the NativeWind plugin:
module.exports = function (api) {
api.cache(true);
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
};
};Now update app/_layout.tsx to import the global styles:
import "../global.css";
import { Stack } from "expo-router";
import { useColorScheme } from "react-native";
import {
DarkTheme,
DefaultTheme,
ThemeProvider,
} from "@react-navigation/native";
export default function RootLayout() {
const colorScheme = useColorScheme();
return (
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
</ThemeProvider>
);
}Step 3: Set Up the Database with Expo SQLite
Create lib/database.ts for local data persistence:
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabaseSync("taskflow.db");
export interface Task {
id: number;
title: string;
description: string;
completed: boolean;
priority: "low" | "medium" | "high";
imageUri?: string;
createdAt: string;
dueDate?: string;
}
export function initDatabase(): void {
db.execSync(`
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT DEFAULT '',
completed INTEGER DEFAULT 0,
priority TEXT DEFAULT 'medium',
imageUri TEXT,
createdAt TEXT DEFAULT (datetime('now')),
dueDate TEXT
);
`);
}
export function getTasks(): Task[] {
const results = db.getAllSync(
"SELECT * FROM tasks ORDER BY completed ASC, createdAt DESC"
);
return results.map((row: any) => ({
...row,
completed: Boolean(row.completed),
}));
}
export function addTask(
title: string,
description: string,
priority: string,
dueDate?: string
): void {
db.runSync(
"INSERT INTO tasks (title, description, priority, dueDate) VALUES (?, ?, ?, ?)",
[title, description, priority, dueDate ?? null]
);
}
export function toggleTask(id: number, completed: boolean): void {
db.runSync("UPDATE tasks SET completed = ? WHERE id = ?", [
completed ? 1 : 0,
id,
]);
}
export function deleteTask(id: number): void {
db.runSync("DELETE FROM tasks WHERE id = ?", [id]);
}
export function updateTaskImage(id: number, imageUri: string): void {
db.runSync("UPDATE tasks SET imageUri = ? WHERE id = ?", [imageUri, id]);
}Initialize the database in app/_layout.tsx:
import { useEffect } from "react";
import { initDatabase } from "@/lib/database";
export default function RootLayout() {
const colorScheme = useColorScheme();
useEffect(() => {
initDatabase();
}, []);
// ... rest of the layout
}Step 4: Build the Tab Navigation
Update app/(tabs)/_layout.tsx for our task app tabs:
import { Tabs } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { useColorScheme } from "react-native";
export default function TabLayout() {
const colorScheme = useColorScheme();
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: "#6366f1",
tabBarStyle: {
backgroundColor: colorScheme === "dark" ? "#1a1a2e" : "#ffffff",
},
headerStyle: {
backgroundColor: colorScheme === "dark" ? "#1a1a2e" : "#ffffff",
},
headerTintColor: colorScheme === "dark" ? "#ffffff" : "#000000",
}}
>
<Tabs.Screen
name="index"
options={{
title: "Tasks",
tabBarIcon: ({ color, size }) => (
<Ionicons name="list" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="add"
options={{
title: "Add Task",
tabBarIcon: ({ color, size }) => (
<Ionicons name="add-circle" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="stats"
options={{
title: "Stats",
tabBarIcon: ({ color, size }) => (
<Ionicons name="stats-chart" size={size} color={color} />
),
}}
/>
</Tabs>
);
}Step 5: Create the Task List Screen
Replace app/(tabs)/index.tsx:
import { View, Text, FlatList, Pressable, RefreshControl } from "react-native";
import { useState, useCallback } from "react";
import { useFocusEffect } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { getTasks, toggleTask, deleteTask, Task } from "@/lib/database";
function PriorityBadge({ priority }: { priority: string }) {
const colors = {
high: "bg-red-100 text-red-700",
medium: "bg-yellow-100 text-yellow-700",
low: "bg-green-100 text-green-700",
};
return (
<View className={`px-2 py-1 rounded-full ${colors[priority as keyof typeof colors]}`}>
<Text className={`text-xs font-medium ${colors[priority as keyof typeof colors]}`}>
{priority}
</Text>
</View>
);
}
function TaskItem({
task,
onToggle,
onDelete,
}: {
task: Task;
onToggle: () => void;
onDelete: () => void;
}) {
return (
<View className="bg-white dark:bg-gray-800 mx-4 mb-3 p-4 rounded-2xl shadow-sm flex-row items-center">
<Pressable onPress={onToggle} className="mr-3">
<Ionicons
name={task.completed ? "checkmark-circle" : "ellipse-outline"}
size={28}
color={task.completed ? "#22c55e" : "#9ca3af"}
/>
</Pressable>
<View className="flex-1">
<Text
className={`text-base font-semibold dark:text-white ${
task.completed ? "line-through text-gray-400" : ""
}`}
>
{task.title}
</Text>
{task.description ? (
<Text className="text-sm text-gray-500 mt-1">{task.description}</Text>
) : null}
<View className="flex-row items-center mt-2 gap-2">
<PriorityBadge priority={task.priority} />
{task.dueDate && (
<Text className="text-xs text-gray-400">
Due: {new Date(task.dueDate).toLocaleDateString()}
</Text>
)}
</View>
</View>
<Pressable onPress={onDelete} className="ml-2 p-2">
<Ionicons name="trash-outline" size={20} color="#ef4444" />
</Pressable>
</View>
);
}
export default function TasksScreen() {
const [tasks, setTasks] = useState<Task[]>([]);
const [refreshing, setRefreshing] = useState(false);
const loadTasks = useCallback(() => {
const data = getTasks();
setTasks(data);
}, []);
useFocusEffect(
useCallback(() => {
loadTasks();
}, [loadTasks])
);
const onRefresh = useCallback(() => {
setRefreshing(true);
loadTasks();
setRefreshing(false);
}, [loadTasks]);
const handleToggle = (id: number, completed: boolean) => {
toggleTask(id, !completed);
loadTasks();
};
const handleDelete = (id: number) => {
deleteTask(id);
loadTasks();
};
const completedCount = tasks.filter((t) => t.completed).length;
return (
<View className="flex-1 bg-gray-50 dark:bg-gray-900">
<View className="px-4 pt-4 pb-2">
<Text className="text-2xl font-bold dark:text-white">My Tasks</Text>
<Text className="text-sm text-gray-500 mt-1">
{completedCount}/{tasks.length} completed
</Text>
</View>
<FlatList
data={tasks}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<TaskItem
task={item}
onToggle={() => handleToggle(item.id, item.completed)}
onDelete={() => handleDelete(item.id)}
/>
)}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
contentContainerStyle={{ paddingTop: 8, paddingBottom: 100 }}
ListEmptyComponent={
<View className="items-center justify-center pt-20">
<Ionicons name="clipboard-outline" size={64} color="#d1d5db" />
<Text className="text-gray-400 text-lg mt-4">No tasks yet</Text>
<Text className="text-gray-400 text-sm mt-1">
Tap the + tab to add your first task
</Text>
</View>
}
/>
</View>
);
}Step 6: Create the Add Task Screen
Create app/(tabs)/add.tsx:
import { View, Text, TextInput, Pressable, ScrollView, Alert } from "react-native";
import { useState } from "react";
import { router } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { addTask } from "@/lib/database";
const priorities = [
{ value: "low", label: "Low", color: "bg-green-500", icon: "arrow-down" },
{ value: "medium", label: "Medium", color: "bg-yellow-500", icon: "remove" },
{ value: "high", label: "High", color: "bg-red-500", icon: "arrow-up" },
] as const;
export default function AddTaskScreen() {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [priority, setPriority] = useState<string>("medium");
const handleSubmit = () => {
if (!title.trim()) {
Alert.alert("Error", "Task title is required");
return;
}
addTask(title.trim(), description.trim(), priority);
Alert.alert("Success", "Task created!", [
{ text: "OK", onPress: () => router.navigate("/(tabs)") },
]);
setTitle("");
setDescription("");
setPriority("medium");
};
return (
<ScrollView className="flex-1 bg-gray-50 dark:bg-gray-900">
<View className="p-4">
<Text className="text-2xl font-bold mb-6 dark:text-white">
New Task
</Text>
{/* Title Input */}
<View className="mb-4">
<Text className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Title *
</Text>
<TextInput
value={title}
onChangeText={setTitle}
placeholder="What needs to be done?"
placeholderTextColor="#9ca3af"
className="bg-white dark:bg-gray-800 p-4 rounded-xl text-base dark:text-white border border-gray-200 dark:border-gray-700"
/>
</View>
{/* Description Input */}
<View className="mb-4">
<Text className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Description
</Text>
<TextInput
value={description}
onChangeText={setDescription}
placeholder="Add details..."
placeholderTextColor="#9ca3af"
multiline
numberOfLines={4}
textAlignVertical="top"
className="bg-white dark:bg-gray-800 p-4 rounded-xl text-base dark:text-white border border-gray-200 dark:border-gray-700 min-h-[120px]"
/>
</View>
{/* Priority Selector */}
<View className="mb-6">
<Text className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Priority
</Text>
<View className="flex-row gap-3">
{priorities.map((p) => (
<Pressable
key={p.value}
onPress={() => setPriority(p.value)}
className={`flex-1 flex-row items-center justify-center p-3 rounded-xl border-2 ${
priority === p.value
? "border-primary bg-primary/10"
: "border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800"
}`}
>
<Ionicons
name={p.icon as any}
size={16}
color={priority === p.value ? "#6366f1" : "#9ca3af"}
/>
<Text
className={`ml-1 font-medium ${
priority === p.value
? "text-primary"
: "text-gray-500"
}`}
>
{p.label}
</Text>
</Pressable>
))}
</View>
</View>
{/* Submit Button */}
<Pressable
onPress={handleSubmit}
className="bg-primary p-4 rounded-xl items-center active:opacity-80"
>
<Text className="text-white font-bold text-base">Create Task</Text>
</Pressable>
</View>
</ScrollView>
);
}Step 7: Create the Statistics Screen
Create app/(tabs)/stats.tsx:
import { View, Text, ScrollView } from "react-native";
import { useState, useCallback } from "react";
import { useFocusEffect } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { getTasks, Task } from "@/lib/database";
function StatCard({
icon,
label,
value,
color,
}: {
icon: string;
label: string;
value: number | string;
color: string;
}) {
return (
<View className="bg-white dark:bg-gray-800 p-4 rounded-2xl flex-1 items-center shadow-sm">
<Ionicons name={icon as any} size={28} color={color} />
<Text className="text-2xl font-bold mt-2 dark:text-white">{value}</Text>
<Text className="text-xs text-gray-500 mt-1">{label}</Text>
</View>
);
}
export default function StatsScreen() {
const [tasks, setTasks] = useState<Task[]>([]);
useFocusEffect(
useCallback(() => {
setTasks(getTasks());
}, [])
);
const total = tasks.length;
const completed = tasks.filter((t) => t.completed).length;
const pending = total - completed;
const highPriority = tasks.filter(
(t) => t.priority === "high" && !t.completed
).length;
const completionRate = total > 0 ? Math.round((completed / total) * 100) : 0;
return (
<ScrollView className="flex-1 bg-gray-50 dark:bg-gray-900 p-4">
<Text className="text-2xl font-bold mb-6 dark:text-white">
Statistics
</Text>
<View className="flex-row gap-3 mb-3">
<StatCard
icon="list"
label="Total"
value={total}
color="#6366f1"
/>
<StatCard
icon="checkmark-circle"
label="Done"
value={completed}
color="#22c55e"
/>
</View>
<View className="flex-row gap-3 mb-6">
<StatCard
icon="time"
label="Pending"
value={pending}
color="#f59e0b"
/>
<StatCard
icon="alert-circle"
label="High Priority"
value={highPriority}
color="#ef4444"
/>
</View>
{/* Completion Rate */}
<View className="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-sm">
<Text className="text-lg font-semibold mb-4 dark:text-white">
Completion Rate
</Text>
<View className="h-4 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<View
className="h-full bg-primary rounded-full"
style={{ width: `${completionRate}%` }}
/>
</View>
<Text className="text-center text-2xl font-bold mt-3 text-primary">
{completionRate}%
</Text>
</View>
{/* Priority Breakdown */}
<View className="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-sm mt-4">
<Text className="text-lg font-semibold mb-4 dark:text-white">
Priority Breakdown
</Text>
{(["high", "medium", "low"] as const).map((p) => {
const count = tasks.filter((t) => t.priority === p).length;
const pct = total > 0 ? Math.round((count / total) * 100) : 0;
const colors = {
high: "#ef4444",
medium: "#f59e0b",
low: "#22c55e",
};
return (
<View key={p} className="flex-row items-center mb-3">
<Text className="w-20 capitalize text-gray-600 dark:text-gray-400">
{p}
</Text>
<View className="flex-1 h-3 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden mx-3">
<View
className="h-full rounded-full"
style={{
width: `${pct}%`,
backgroundColor: colors[p],
}}
/>
</View>
<Text className="text-sm text-gray-500 w-8">{count}</Text>
</View>
);
})}
</View>
</ScrollView>
);
}Step 8: Add Camera Integration for Task Attachments
Create app/camera.tsx for a camera modal:
import { View, Text, Pressable } from "react-native";
import { CameraView, useCameraPermissions } from "expo-camera";
import { useRef } from "react";
import { router, useLocalSearchParams } from "expo-router";
import { updateTaskImage } from "@/lib/database";
export default function CameraScreen() {
const [permission, requestPermission] = useCameraPermissions();
const cameraRef = useRef<CameraView>(null);
const { taskId } = useLocalSearchParams<{ taskId: string }>();
if (!permission) return <View />;
if (!permission.granted) {
return (
<View className="flex-1 items-center justify-center bg-black">
<Text className="text-white text-lg mb-4">
Camera permission is required
</Text>
<Pressable
onPress={requestPermission}
className="bg-primary px-6 py-3 rounded-xl"
>
<Text className="text-white font-bold">Grant Permission</Text>
</Pressable>
</View>
);
}
const takePicture = async () => {
if (!cameraRef.current) return;
const photo = await cameraRef.current.takePictureAsync();
if (photo && taskId) {
updateTaskImage(parseInt(taskId), photo.uri);
router.back();
}
};
return (
<View className="flex-1">
<CameraView ref={cameraRef} className="flex-1" facing="back">
<View className="flex-1 justify-end items-center pb-10">
<Pressable
onPress={takePicture}
className="w-20 h-20 rounded-full bg-white border-4 border-gray-300 active:opacity-70"
/>
</View>
</CameraView>
</View>
);
}Add the camera route to the root layout stack:
<Stack.Screen
name="camera"
options={{
presentation: "fullScreenModal",
headerShown: false,
}}
/>Step 9: Set Up Push Notifications
Create lib/notifications.ts:
import * as Notifications from "expo-notifications";
import { Platform } from "react-native";
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
export async function registerForPushNotifications(): Promise<string | null> {
const { status: existingStatus } =
await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== "granted") {
return null;
}
if (Platform.OS === "android") {
await Notifications.setNotificationChannelAsync("default", {
name: "default",
importance: Notifications.AndroidImportance.MAX,
});
}
const token = (await Notifications.getExpoPushTokenAsync()).data;
return token;
}
export async function scheduleTaskReminder(
taskTitle: string,
dueDate: Date
): Promise<string> {
const trigger = new Date(dueDate);
trigger.setHours(trigger.getHours() - 1); // Remind 1 hour before
const id = await Notifications.scheduleNotificationAsync({
content: {
title: "Task Reminder",
body: `"${taskTitle}" is due soon!`,
sound: true,
},
trigger: {
type: Notifications.SchedulableTriggerInputTypes.DATE,
date: trigger,
},
});
return id;
}Step 10: Configure for Production
Update app.json for production deployment:
{
"expo": {
"name": "TaskFlow",
"slug": "taskflow",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "taskflow",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"splash": {
"image": "./assets/images/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#6366f1"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.yourcompany.taskflow",
"infoPlist": {
"NSCameraUsageDescription": "TaskFlow uses the camera to attach photos to tasks."
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#6366f1"
},
"package": "com.yourcompany.taskflow",
"permissions": ["CAMERA", "RECEIVE_BOOT_COMPLETED", "SCHEDULE_EXACT_ALARM"]
},
"plugins": [
"expo-router",
"expo-camera",
"expo-notifications",
"expo-sqlite"
]
}
}Step 11: Build and Deploy with EAS
Install EAS CLI and configure builds:
npm install -g eas-cli
eas login
eas build:configureThis creates eas.json:
{
"cli": {
"version": ">= 3.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {}
},
"submit": {
"production": {}
}
}Build for both platforms:
# Development build (for testing on real devices)
eas build --profile development --platform all
# Production build
eas build --profile production --platform allSubmit to app stores:
# iOS App Store
eas submit --platform ios
# Google Play Store
eas submit --platform androidStep 12: Enable Over-the-Air Updates
One of Expo's most powerful features is OTA updates — push JavaScript updates without going through app store review:
npx expo install expo-updatesAdd to app.json:
{
"expo": {
"updates": {
"url": "https://u.expo.dev/your-project-id"
},
"runtimeVersion": {
"policy": "appVersion"
}
}
}Push an update:
eas update --branch production --message "Fix task sorting bug"Users receive the update on next app launch — no app store review needed.
Testing Your Implementation
Start the development server:
npx expo startTest on your device:
- iOS: Scan the QR code with your Camera app
- Android: Scan the QR code with the Expo Go app
Test the following flows:
- Create a new task with different priorities
- Mark tasks as complete/incomplete
- Delete a task
- Check the statistics page updates correctly
- Test dark mode by toggling your device settings
- Pull to refresh the task list
Troubleshooting
"Metro bundler is not running"
npx expo start --clearNativeWind styles not applying
Make sure global.css is imported in app/_layout.tsx and babel.config.js includes the NativeWind preset.
SQLite errors on web
Expo SQLite only works on iOS and Android. For web support, consider expo-sqlite/next or a different storage solution.
Camera not working in Expo Go
Some native modules require a development build. Run:
eas build --profile development --platform iosNext Steps
Now that you have a working mobile app, consider these enhancements:
- Cloud sync — Add Supabase or Firebase for cross-device synchronization
- Biometric auth — Use
expo-local-authenticationfor fingerprint/face ID - Widgets — Create home screen widgets with
expo-widgets - Animations — Add transitions with
react-native-reanimated - Offline-first — Implement a sync queue for when the device is offline
Conclusion
You've built a complete cross-platform mobile app using Expo SDK 52, React Native, and TypeScript. The app includes file-based routing with Expo Router, local data persistence with SQLite, camera integration, push notifications, and is ready for deployment to both app stores.
Expo has matured significantly in 2026, making it the go-to choice for React developers entering the mobile space. With EAS Build and OTA updates, the deployment workflow is as smooth as deploying a web application. The combination of React Native's performance with Expo's developer experience makes building production mobile apps faster than ever.
Discuss Your Project with Us
We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.
Let's find the best solutions for your needs.
Related Articles

Building an Autonomous AI Agent with Agentic RAG and Next.js
Learn how to build an AI agent that autonomously decides when and how to retrieve information from vector databases. A comprehensive hands-on guide using Vercel AI SDK and Next.js with executable examples.

Building AI Agents from Scratch with TypeScript: Master the ReAct Pattern Using the Vercel AI SDK
Learn how to build AI agents from the ground up using TypeScript. This tutorial covers the ReAct pattern, tool calling, multi-step reasoning, and production-ready agent loops with the Vercel AI SDK.

Better Auth with Next.js 15: The Complete Authentication Guide for 2026
Learn how to implement full-featured authentication in Next.js 15 using Better Auth. This tutorial covers email/password, OAuth, sessions, middleware protection, and role-based access control.