Architecture Modes
Mobile Architecture: API + Expo React Native
Go API paired with an Expo React Native app in a Turborepo monorepo. Shared types between backend and mobile, with encrypted token storage and file-based navigation.
Overview
The mobile architecture gives you a Go backend and an Expo React Native app inside a Turborepo monorepo. Types and schemas are shared through a packages/shared/ directory, just like the double and triple architectures. The mobile app uses Expo Router for file-based navigation (similar to Next.js App Router but for native screens) and SecureStore for encrypted token storage.
The Go API is identical to every other architecture -- same handler-service-model pattern, same batteries, same code generation. The difference is on the client side: instead of a web browser, your users interact with a native iOS/Android app.
Scaffold command
grit new myapp --mobile
Key Characteristics
| Property | Mobile Architecture |
|---|---|
| Backend | Go API (Gin + GORM) -- same as all architectures |
| Client | Expo React Native (iOS + Android) |
| Navigation | Expo Router (file-based, like Next.js App Router) |
| Token storage | SecureStore (encrypted, not localStorage) |
| Monorepo | Turborepo + pnpm workspaces |
| Shared types | packages/shared/ (Zod schemas + TypeScript types) |
Full Folder Structure
A Turborepo monorepo with the Go API in apps/api/, the Expo app in apps/expo/, and shared types in packages/shared/.
myapp/├── apps/│ ├── api/ # Go backend (same as other architectures)│ │ ├── go.mod # Module: myapp/apps/api│ │ ├── go.sum│ │ ├── Dockerfile│ │ ├── cmd/│ │ │ ├── server/main.go│ │ │ ├── migrate/main.go│ │ │ └── seed/main.go│ │ └── internal/ # Full Go backend│ │ ├── config/│ │ ├── database/│ │ ├── models/│ │ ├── handlers/│ │ ├── services/│ │ ├── middleware/│ │ ├── routes/│ │ ├── mail/│ │ ├── storage/│ │ ├── jobs/│ │ ├── cache/│ │ ├── ai/│ │ └── auth/│ └── expo/ # Expo React Native app│ ├── package.json│ ├── app.json # Expo configuration│ ├── babel.config.js│ ├── tsconfig.json│ ├── app/ # Expo Router screens│ │ ├── _layout.tsx # Root layout│ │ ├── login.tsx # Login screen│ │ ├── register.tsx # Register screen│ │ └── (tabs)/ # Tab navigation│ │ ├── _layout.tsx # Tab bar layout│ │ ├── index.tsx # Home tab│ │ ├── profile.tsx # Profile tab│ │ └── settings.tsx # Settings tab│ ├── components/ # React Native components│ ├── hooks/ # useAuth, useQuery hooks│ ├── lib/ # API client, SecureStore helpers│ └── assets/ # Images, fonts├── packages/│ └── shared/ # Shared types + schemas│ ├── package.json│ ├── tsconfig.json│ ├── schemas/ # Zod validation schemas│ ├── types/ # TypeScript interfaces│ └── constants/ # API routes, config├── .env├── .env.example├── .gitignore├── docker-compose.yml # PostgreSQL, Redis, MinIO, Mailhog├── docker-compose.prod.yml├── turbo.json├── pnpm-workspace.yaml├── grit.json # architecture: "mobile"└── .claude/skills/grit/├── SKILL.md└── reference.md
Directory Breakdown
apps/api/ -- Go Backend
Identical to every other Grit architecture. The full Go backend with Gin router, GORM models, handler-service-model pattern, and all batteries (auth, storage, email, jobs, cache, AI). The API does not know or care whether the client is a web browser or a mobile app -- it just serves JSON over REST.
apps/expo/ -- Expo React Native App
The Expo app uses the managed workflow with Expo Router for file-based navigation. Screens live in the app/ directory and follow the same conventions as Next.js App Router: _layout.tsx for layout wrappers, group folders like (tabs)/ for navigation groups, and regular .tsx files for individual screens.
apps/expo/app/(tabs)/ -- Tab Navigation
The tab group provides the main navigation structure. The _layout.tsx inside this folder defines the tab bar with icons. Each file becomes a tab: index.tsx is Home, profile.tsx is Profile, and settings.tsx is Settings.
packages/shared/ -- Shared Types
Zod schemas and TypeScript interfaces shared between the API types (via grit sync) and the Expo app. Import with @shared/schemas or @shared/types from anywhere in the monorepo.
grit.json -- Project Config
{"architecture": "mobile","go_module": "myapp/apps/api"}
SecureStore vs localStorage
On the web, JWT tokens are stored in localStorage. On mobile, this is insecure because any app with root access can read localStorage. Expo provides expo-secure-store, which uses the iOS Keychain and Android Keystore to encrypt tokens at rest.
| Feature | Web (localStorage) | Mobile (SecureStore) |
|---|---|---|
| Encryption | None (plaintext) | AES-256 (hardware-backed) |
| Storage | Browser storage | iOS Keychain / Android Keystore |
| Access | Any JS in the page | Only your app (sandboxed) |
import * as SecureStore from 'expo-secure-store'const TOKEN_KEY = 'auth_token'const REFRESH_KEY = 'refresh_token'export async function saveTokens(access: string, refresh: string) {await SecureStore.setItemAsync(TOKEN_KEY, access)await SecureStore.setItemAsync(REFRESH_KEY, refresh)}export async function getAccessToken(): Promise<string | null> {return SecureStore.getItemAsync(TOKEN_KEY)}export async function clearTokens() {await SecureStore.deleteItemAsync(TOKEN_KEY)await SecureStore.deleteItemAsync(REFRESH_KEY)}
API URL Configuration
On the web, localhost:8080 works fine because the browser and the API are on the same machine. On a physical mobile device, localhost refers to the phone itself, not your development machine. You need to use your machine's local IP address instead.
import Constants from 'expo-constants'import { getAccessToken } from './secure-store'// In development, use your machine's local IP (not localhost)// Find it with: ipconfig (Windows) or ifconfig (macOS/Linux)const API_URL = __DEV__? 'http://192.168.1.100:8080' // Your machine's IP: 'https://api.yourapp.com' // Production URLexport async function apiFetch(path: string, options: RequestInit = {}) {const token = await getAccessToken()const res = await fetch(API_URL + path, {...options,headers: {'Content-Type': 'application/json',...(token ? { Authorization: 'Bearer ' + token } : {}),...options.headers,},})if (!res.ok) throw new Error(await res.text())return res.json()}
On the iOS simulator and Android emulator, you can use localhost (iOS) or 10.0.2.2 (Android emulator). But on a real device connected via USB or Wi-Fi, use the machine's local IP.
Data Flow
Mobile App (Expo)│├── SecureStore → reads JWT token│├── fetch('https://api.yourapp.com/api/posts')│ ││ └── Go API → JWT middleware → handler → service → PostgreSQL│├── FlatList renders data (not HTML tables)├── Pull-to-refresh triggers refetch└── Infinite scroll loads next page
Mobile-Specific Patterns
Mobile apps use different UI patterns than web apps. Here are the key differences in the Expo scaffold.
FlatList Instead of HTML Tables
React Native doesn't have <table> or <div>. Data lists use FlatList which virtualizes rendering for performance -- only visible items are rendered.
<FlatListdata={posts}keyExtractor={(item) => item.id.toString()}renderItem={({ item }) => (<Pressable onPress={() => router.push('/post/' + item.id)}><Text style={styles.title}>{item.title}</Text><Text style={styles.date}>{item.created_at}</Text></Pressable>)}onRefresh={refetch}refreshing={isRefetching}onEndReached={fetchNextPage}onEndReachedThreshold={0.5}/>
Tab Navigation Layout
The tab bar is the primary navigation pattern on mobile. Expo Router makes this declarative with a (tabs)/ group folder.
import { Tabs } from 'expo-router'import { Home, User, Settings } from 'lucide-react-native'export default function TabLayout() {return (<Tabs screenOptions={{tabBarActiveTintColor: '#6c5ce7',tabBarStyle: { backgroundColor: '#0a0a0f', borderTopColor: '#2a2a3a' },}}><Tabs.Screenname="index"options={{ title: 'Home', tabBarIcon: ({ color }) => <Home color={color} size={22} /> }}/><Tabs.Screenname="profile"options={{ title: 'Profile', tabBarIcon: ({ color }) => <User color={color} size={22} /> }}/><Tabs.Screenname="settings"options={{ title: 'Settings', tabBarIcon: ({ color }) => <Settings color={color} size={22} /> }}/></Tabs>)}
Push Notifications
Expo Notifications provides a unified API for push notifications on both iOS and Android. Register the device's push token with your Go API, then send notifications from the backend using Expo's push service. This is useful for real-time alerts, chat messages, order updates, and marketing notifications.
Code Generation
Running grit generate resource in a mobile project creates Go backend files and shared TypeScript types. It does not auto-generate mobile screens -- you build those yourself using the shared types.
| Generated File | Location |
|---|---|
| Go model | apps/api/internal/models/post.go |
| Go service | apps/api/internal/services/post_service.go |
| Go handler | apps/api/internal/handlers/post_handler.go |
| Route injection | apps/api/internal/routes/routes.go |
| Zod schema | packages/shared/schemas/post.ts |
| TypeScript types | packages/shared/types/post.ts |
No auto-generated mobile screens
Unlike the triple architecture (which generates admin pages), the mobile architecture does not auto-generate Expo screens. Mobile UIs are too diverse -- a list screen for a social feed looks nothing like a list screen for an e-commerce catalog. Instead, you build screens yourself and import the shared types.
Building and Distribution
Expo provides two ways to distribute your app: EAS Build for full native builds (submitted to App Store / Google Play), and OTA updates for JavaScript-only changes that skip the app store review process.
EAS Build
# Install EAS CLInpm install -g eas-cli# Configure build profilescd apps/expo && eas build:configure# Build for iOS (requires Apple Developer account)eas build --platform ios# Build for Androideas build --platform android# Submit to storeseas submit --platform ioseas submit --platform android
OTA Updates
For JavaScript-only changes (bug fixes, UI tweaks, new screens), you can push updates directly to users without going through the app store review process.
# Push an update to all userseas update --branch production --message "Fix login screen layout"
API Deployment
The Go API is deployed separately using Docker, the same as the API-only architecture.
# Deploy the APIcd apps/api && docker build -t myapp-api .docker push myapp-api:latest
When to Choose Mobile
Good fit
- -Mobile-first products (social, delivery, fitness, etc.)
- -Cross-platform apps (one codebase for iOS + Android)
- -Apps that need push notifications, camera, GPS
- -Teams that know React and want to build native apps
- -Apps where the primary interface is a phone
Not ideal for
- -Web-first apps (use double or triple instead)
- -Apps that need both web and mobile (add web to this scaffold later)
- -Games or heavy GPU apps (use Unity or native SDKs)
- -Projects that need an admin panel (add admin app separately)
Example Project
The Job Portal example built with the mobile architecture. Includes the Go API, Expo app with tab navigation, SecureStore auth, shared types, and EAS build config.
View Job Portal (Mobile + Expo) on GitHub →