返回 Skill 列表
extension
分类: 开发与工程无需 API Key

t4-stack

T4 Stack - 一个基于TypeScript的全栈启动项目,适用于React Native和Web,使用了Tamagui、tRPC、Cloudflare边缘部署,并支持iOS、Android和PWA之间的通用代码共享

person作者: jakexiaohubgithub

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