skills-templates/t4-stack/SKILL.md
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
npx skillsauth add enuno/claude-command-and-control t4-stackInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
┌─────────────────────────────────────────────────────────────────┐
│ 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) │
└──────────────────────┘
| 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 |
# 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
cd my-app
# Install dependencies
bun install
# Start development servers
bun dev
# Start web only
bun dev:web
# Start mobile (Expo)
bun dev:native
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
| 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 |
# 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 />
}
// 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>
)
}
// 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))
}),
})
// 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
// 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>
)
}
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>
)
}
// 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 {}
}
# .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
// 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
}
// 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>
)
}
// 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)
# Generate migration from schema changes
bun db:generate
# Push migrations to D1
bun db:push
# Run migrations locally
bun db:migrate
// 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,
}
}
# 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
# Deploy Next.js to Pages
cd apps/next
bun run build
wrangler pages deploy .next
# 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/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
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',
// ...
}
tools
MemPalace local-first AI memory system. Use when setting up persistent memory for Claude Code sessions, mining project files or conversation transcripts, querying past context, configuring MCP tools, managing the knowledge graph, or troubleshooting palace operations.
tools
LangSmith Python SDK — trace, evaluate, and monitor LLM applications. Covers @traceable decorator, trace context manager, Client API, evaluate() / aevaluate(), comparative evaluation, custom evaluators, dataset management, prompt caching, ASGI middleware, and pytest plugin.
development
LangGraph (Python) — build stateful, controllable agent graphs with checkpointing, streaming, persistence, interrupts, fault tolerance, and durable execution. Covers both Graph API (StateGraph) and Functional API (@entrypoint/@task).
development
LangGraph Graph API (Python) — build explicit DAG agent workflows with StateGraph, typed state, nodes, edges, Command routing, Send fan-out, checkpointers, interrupts, and streaming. Use when you need explicit control flow and graph topology.