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

PropertyMobile Architecture
BackendGo API (Gin + GORM) -- same as all architectures
ClientExpo React Native (iOS + Android)
NavigationExpo Router (file-based, like Next.js App Router)
Token storageSecureStore (encrypted, not localStorage)
MonorepoTurborepo + pnpm workspaces
Shared typespackages/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/
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

grit.json
{
"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.

FeatureWeb (localStorage)Mobile (SecureStore)
EncryptionNone (plaintext)AES-256 (hardware-backed)
StorageBrowser storageiOS Keychain / Android Keystore
AccessAny JS in the pageOnly your app (sandboxed)
apps/expo/lib/secure-store.ts
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.

apps/expo/lib/api-client.ts
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 URL
export 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

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.

example: FlatList
<FlatList
data={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.

apps/expo/app/(tabs)/_layout.tsx
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.Screen
name="index"
options={{ title: 'Home', tabBarIcon: ({ color }) => <Home color={color} size={22} /> }}
/>
<Tabs.Screen
name="profile"
options={{ title: 'Profile', tabBarIcon: ({ color }) => <User color={color} size={22} /> }}
/>
<Tabs.Screen
name="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 FileLocation
Go modelapps/api/internal/models/post.go
Go serviceapps/api/internal/services/post_service.go
Go handlerapps/api/internal/handlers/post_handler.go
Route injectionapps/api/internal/routes/routes.go
Zod schemapackages/shared/schemas/post.ts
TypeScript typespackages/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

build for stores
# Install EAS CLI
npm install -g eas-cli
# Configure build profiles
cd apps/expo && eas build:configure
# Build for iOS (requires Apple Developer account)
eas build --platform ios
# Build for Android
eas build --platform android
# Submit to stores
eas submit --platform ios
eas 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 users
eas 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 API
cd 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 →