npx skills add https://modelscope.cn/skills/@majiayu000/t4-stack
smithery/enuno
t4-stack
T4 Stack - Full-stack TypeScript starter for React Native + Web with Tamagui, tRPC, Cloudflare edge deployment, and universal code sharing across iOS, Android, and PWA
Similar popular skills
Related neighbors and high-traction skills in the same topics — useful to compare before installing.
Provides React Native performance optimization guidelines for FPS, TTI, bundle size, memory lea…
25K installsFramework (OSS). Integrate Expo and React Native into an existing native iOS or Android app. Us…
18.7K installsAlso in this package
Other skills from smithery/enuno · top by installs.
npx skills add smithery/enuno
More details
Agent compatibility
Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.
Also listed on
Alternate registries and mirrors of this skill.
Skill metadata
Parsed from SKILL.md frontmatter.
Package contents
Files included with this skill beyond the listing page.
-
skill md
SKILL.md20,036 B -
docs
SUMMARY.md183 B
History
- First recorded snapshot · 0 installs
SKILL.md
T4 Stack Skill
The T4 Stack is a full-stack, type-safe starter kit for building universal applications across iOS, Android, Web, and Desktop from a single TypeScript codebase. Created by Tim Miller, it emphasizes developer experience, rapid deployment, and edge-first architecture with Cloudflare.
Key Value Proposition: Build once, deploy everywhere - iOS, Android, Web (PWA), macOS, Windows, and Linux with 6-second installs, 30-second backend deployments, and end-to-end type safety.
When to Use This Skill
- Building cross-platform apps with shared codebase (React Native + Web)
- Creating full-stack TypeScript applications with tRPC
- Deploying to Cloudflare Workers and D1 edge database
- Setting up Expo + Next.js monorepo projects
- Implementing Tamagui UI components across platforms
- Configuring Supabase authentication for mobile + web
- Working with Drizzle ORM and SQLite at the edge
- Building Progressive Web Apps with native-like experience
When NOT to Use This Skill
- For backend-only Node.js projects (use standard Node.js patterns)
- For React-only web apps without mobile (use Next.js directly)
- For React Native-only apps without web (use Expo directly)
- For non-TypeScript projects (T4 is TypeScript-first)
- For AWS/GCP deployment (T4 is Cloudflare-focused)
Core Concepts
Technology Stack Overview
┌─────────────────────────────────────────────────────────────────┐
│ T4 Stack │
└─────────────────────────────────────────────────────────────────┘
┌──────────────────────┐
│ Shared Codebase │
│ /packages/app │
│ /packages/ui │
└──────────┬───────────┘
│
┌──────────────────────┼──────────────────────┐
│ │ │
┌───────▼───────┐ ┌────────▼────────┐ ┌───────▼───────┐
│ Next.js │ │ Expo │ │ Tauri │
│ (Web/PWA) │ │ (iOS/Android) │ │ (Desktop) │
└───────────────┘ └─────────────────┘ └───────────────┘
│ │ │
└──────────────────────┼──────────────────────┘
│
┌──────────▼───────────┐
│ Cloudflare Edge │
│ - Workers (API) │
│ - D1 (SQLite DB) │
│ - Pages (Frontend) │
└──────────────────────┘
Stack Components
| Layer | Technology | Purpose |
|---|---|---|
| UI Framework | Tamagui | Cross-platform components with design system |
| Web | Next.js | React framework for web + PWA |
| Mobile | Expo + Expo Router | React Native for iOS/Android |
| Desktop | Tauri (optional) | Native desktop apps |
| Navigation | Solito | Unified navigation across platforms |
| API | tRPC + Hono | Type-safe API with edge-compatible server |
| Data Fetching | TanStack Query | Server state management |
| State | Jotai | Lightweight global state |
| Database | Cloudflare D1 + Drizzle | SQLite at the edge with ORM |
| Validation | Valibot | Lightweight runtime type checking |
| Auth | Supabase Auth | Authentication across platforms |
| Performance | Million.js, PattyCake | React optimization, pattern matching |
| Code Quality | Biome | Fast linting and formatting |
Installation
Prerequisites
- Bun v1.0+ (required)
- Node.js 20+ (for some tooling)
- Cloudflare account (for deployment)
- Supabase account (for authentication)
Quick Start
# Create new T4 project (interactive)
bun create t4-app
# Create with specific project name
bun create t4-app my-app
# Create with Tauri desktop support (experimental)
bun create t4-app --tauri
# Create with Lucia Auth instead of Supabase
bun create t4-app --lucia
Post-Installation
cd my-app
# Install dependencies
bun install
# Start development servers
bun dev
# Start web only
bun dev:web
# Start mobile (Expo)
bun dev:native
Project Structure
my-app/
├── apps/
│ ├── next/ # Next.js web application
│ │ ├── app/ # App Router pages
│ │ ├── public/ # Static assets
│ │ └── next.config.mjs # Next.js configuration
│ │
│ ├── expo/ # Expo mobile application
│ │ ├── app/ # Expo Router screens
│ │ ├── assets/ # Mobile assets
│ │ └── app.config.ts # Expo configuration
│ │
│ └── tauri/ # Desktop app (if --tauri flag used)
│
├── packages/
│ ├── app/ # Shared application code
│ │ ├── features/ # Feature modules (screens)
│ │ │ ├── home/
│ │ │ │ └── screen.tsx
│ │ │ └── settings/
│ │ │ ├── screen.tsx
│ │ │ ├── screen.native.tsx # Native-specific
│ │ │ └── screen.web.tsx # Web-specific
│ │ ├── provider/ # App providers (auth, theme)
│ │ └── utils/ # Shared utilities
│ │
│ ├── ui/ # Shared UI components
│ │ ├── src/
│ │ │ ├── Button.tsx
│ │ │ ├── Card.tsx
│ │ │ └── index.ts
│ │ └── tamagui.config.ts
│ │
│ ├── api/ # Backend API (Hono + tRPC)
│ │ ├── src/
│ │ │ ├── router/ # tRPC routers
│ │ │ ├── context.ts # tRPC context
│ │ │ └── index.ts # API entry point
│ │ └── wrangler.toml # Cloudflare Workers config
│ │
│ └── db/ # Database schema (Drizzle)
│ ├── schema/
│ │ └── users.ts
│ ├── migrations/
│ └── drizzle.config.ts
│
├── .env.example # Environment template
├── biome.json # Linting/formatting config
├── turbo.json # Turborepo configuration
└── package.json # Root package.json
Platform-Specific File Extensions
| Extension | Target Platform | Example |
|---|---|---|
.tsx |
Shared (all platforms) | screen.tsx |
.native.tsx |
React Native only | screen.native.tsx |
.web.tsx |
Next.js only | screen.web.tsx |
Creating Features
Adding a New Screen
# 1. Create feature folder
mkdir -p packages/app/features/profile
# 2. Create shared screen
touch packages/app/features/profile/screen.tsx
// packages/app/features/profile/screen.tsx
import { YStack, H1, Paragraph, Button } from '@my-app/ui'
import { useRouter } from 'solito/router'
export function ProfileScreen() {
const { push } = useRouter()
return (
<YStack flex={1} padding="$4" space="$4">
<H1>Profile</H1>
<Paragraph>Welcome to your profile!</Paragraph>
<Button onPress={() => push('/settings')}>
Go to Settings
</Button>
</YStack>
)
}
// apps/next/app/profile/page.tsx
import { ProfileScreen } from '@my-app/app/features/profile/screen'
export default function ProfilePage() {
return <ProfileScreen />
}
// apps/expo/app/profile.tsx
import { ProfileScreen } from '@my-app/app/features/profile/screen'
export default function ProfileRoute() {
return <ProfileScreen />
}
Platform-Specific Code
// packages/app/features/camera/screen.native.tsx
import { Camera } from 'expo-camera'
export function CameraScreen() {
return <Camera style={{ flex: 1 }} />
}
// packages/app/features/camera/screen.web.tsx
export function CameraScreen() {
return (
<div>
<video ref={videoRef} autoPlay />
{/* Web camera implementation */}
</div>
)
}
tRPC API Setup
Router Definition
// packages/api/src/router/user.ts
import { router, protectedProcedure, publicProcedure } from '../trpc'
import { v } from 'valibot'
import { users, insertUserSchema } from '@my-app/db/schema'
export const userRouter = router({
// Public procedure
getById: publicProcedure
.input(v.object({ id: v.string() }))
.query(async ({ ctx, input }) => {
return ctx.db.query.users.findFirst({
where: eq(users.id, input.id)
})
}),
// Protected procedure (requires auth)
updateProfile: protectedProcedure
.input(v.object({
name: v.string(),
bio: v.optional(v.string())
}))
.mutation(async ({ ctx, input }) => {
return ctx.db.update(users)
.set(input)
.where(eq(users.id, ctx.user.id))
}),
})
Root Router
// packages/api/src/router/index.ts
import { router } from '../trpc'
import { userRouter } from './user'
import { postRouter } from './post'
export const appRouter = router({
user: userRouter,
post: postRouter,
})
export type AppRouter = typeof appRouter
Client Usage
// packages/app/features/profile/screen.tsx
import { trpc } from '@my-app/app/utils/trpc'
export function ProfileScreen() {
const { data: user, isLoading } = trpc.user.getById.useQuery({
id: 'user-123'
})
const updateProfile = trpc.user.updateProfile.useMutation({
onSuccess: () => {
// Handle success
}
})
if (isLoading) return <Spinner />
return (
<YStack>
<H1>{user?.name}</H1>
<Button onPress={() => updateProfile.mutate({ name: 'New Name' })}>
Update Name
</Button>
</YStack>
)
}
Tamagui UI Components
Basic Component Usage
import {
YStack,
XStack,
H1,
H2,
Paragraph,
Button,
Input,
Card,
Image,
Separator,
Sheet,
Dialog,
} from '@my-app/ui'
function MyComponent() {
return (
<YStack flex={1} padding="$4" space="$4">
<XStack justifyContent="space-between" alignItems="center">
<H1>Title</H1>
<Button size="$3" theme="active">
Action
</Button>
</XStack>
<Card elevate padded>
<Card.Header>
<H2>Card Title</H2>
</Card.Header>
<Paragraph>Card content goes here.</Paragraph>
<Card.Footer>
<XStack space="$2">
<Button flex={1}>Cancel</Button>
<Button flex={1} theme="active">Confirm</Button>
</XStack>
</Card.Footer>
</Card>
<Input placeholder="Enter text..." />
</YStack>
)
}
Theme Configuration
// packages/ui/tamagui.config.ts
import { createTamagui, createTokens } from '@tamagui/core'
import { shorthands } from '@tamagui/shorthands'
import { themes, tokens } from '@tamagui/themes'
export const config = createTamagui({
themes,
tokens,
shorthands,
fonts: {
// Custom fonts
},
})
export type AppConfig = typeof config
declare module '@tamagui/core' {
interface TamaguiCustomConfig extends AppConfig {}
}
Authentication with Supabase
Environment Configuration
# .env.local (Next.js)
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
# .dev.vars (Cloudflare Workers)
JWT_VERIFICATION_KEY=your-jwt-secret-from-supabase
Auth Provider Setup
// packages/app/provider/auth.tsx
import { createContext, useContext, useEffect, useState } from 'react'
import { supabase } from '../utils/supabase'
import type { User, Session } from '@supabase/supabase-js'
type AuthContextType = {
user: User | null
session: Session | null
signIn: (email: string, password: string) => Promise<void>
signUp: (email: string, password: string) => Promise<void>
signOut: () => Promise<void>
}
const AuthContext = createContext<AuthContextType | null>(null)
export function AuthProvider({ children }) {
const [user, setUser] = useState<User | null>(null)
const [session, setSession] = useState<Session | null>(null)
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session)
setUser(session?.user ?? null)
})
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_event, session) => {
setSession(session)
setUser(session?.user ?? null)
}
)
return () => subscription.unsubscribe()
}, [])
const signIn = async (email: string, password: string) => {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) throw error
}
const signUp = async (email: string, password: string) => {
const { error } = await supabase.auth.signUp({
email,
password,
})
if (error) throw error
}
const signOut = async () => {
await supabase.auth.signOut()
}
return (
<AuthContext.Provider value={{ user, session, signIn, signUp, signOut }}>
{children}
</AuthContext.Provider>
)
}
export const useAuth = () => {
const context = useContext(AuthContext)
if (!context) throw new Error('useAuth must be used within AuthProvider')
return context
}
OAuth Login (Google, Apple, Discord)
// packages/app/features/auth/login.tsx
import { Button, YStack } from '@my-app/ui'
import { supabase } from '../../utils/supabase'
import * as WebBrowser from 'expo-web-browser'
import { makeRedirectUri } from 'expo-auth-session'
export function LoginScreen() {
const signInWithGoogle = async () => {
const redirectUrl = makeRedirectUri()
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: redirectUrl,
},
})
if (data?.url) {
await WebBrowser.openAuthSessionAsync(data.url, redirectUrl)
}
}
return (
<YStack space="$4" padding="$4">
<Button onPress={signInWithGoogle} icon={GoogleIcon}>
Sign in with Google
</Button>
<Button onPress={signInWithApple} icon={AppleIcon}>
Sign in with Apple
</Button>
</YStack>
)
}
Database with Drizzle
Schema Definition
// packages/db/schema/users.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
import { createInsertSchema, createSelectSchema } from 'drizzle-valibot'
export const users = sqliteTable('users', {
id: text('id').primaryKey(),
email: text('email').notNull().unique(),
name: text('name'),
avatarUrl: text('avatar_url'),
createdAt: integer('created_at', { mode: 'timestamp' })
.notNull()
.default(sql`(unixepoch())`),
})
// Valibot schemas for validation
export const insertUserSchema = createInsertSchema(users)
export const selectUserSchema = createSelectSchema(users)
Migrations
# Generate migration from schema changes
bun db:generate
# Push migrations to D1
bun db:push
# Run migrations locally
bun db:migrate
Database Context in tRPC
// packages/api/src/context.ts
import { drizzle } from 'drizzle-orm/d1'
import * as schema from '@my-app/db/schema'
export function createContext(env: Env, user?: User) {
const db = drizzle(env.DB, { schema })
return {
db,
user,
}
}
Deployment
Cloudflare Workers (Backend)
# packages/api/wrangler.toml
name = "my-app-api"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "xxx-xxx-xxx"
# Deploy backend
cd packages/api
bun run deploy
# OR
wrangler deploy
Cloudflare Pages (Frontend)
# Deploy Next.js to Pages
cd apps/next
bun run build
wrangler pages deploy .next
Expo (Mobile)
# Build for iOS
eas build --platform ios
# Build for Android
eas build --platform android
# Submit to App Store
eas submit --platform ios
# Submit to Play Store
eas submit --platform android
GitHub Actions (CI/CD)
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy-api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- run: bun install
- run: bun run db:migrate
- run: wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
deploy-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- run: bun install
- run: bun run build:web
- uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
projectName: my-app
directory: apps/next/.next
Troubleshooting
Common Issues
Bun version mismatch:
# Check Bun version (must be 1.0+)
bun --version
# Update Bun
curl -fsSL https://bun.sh/install | bash
Tamagui styles not applying:
# Clear Metro cache
cd apps/expo
bun expo start --clear
# Clear Next.js cache
cd apps/next
rm -rf .next
bun dev
tRPC type errors after schema change:
# Regenerate types
bun turbo build --filter=@my-app/api
# Restart TypeScript server in IDE
D1 database not found:
# Create D1 database
wrangler d1 create my-app-db
# Update wrangler.toml with returned database_id
Supabase auth not working on mobile:
// Ensure deep link handling in app.config.ts
export default {
scheme: 'my-app',
// ...
}
Resources
Official Documentation
Technology Documentation
Community
Version History
- 1.0.0 (2026-01-12): Initial skill release
- Complete T4 Stack framework documentation - Project structure and file conventions - tRPC API patterns with Valibot validation - Tamagui UI component examples - Supabase authentication integration - Drizzle ORM database setup - Cloudflare deployment configuration - Expo mobile development workflow - Troubleshooting guide