---
name: Full-Stack Web App Development
slug: full-stack-web-app-development
category: DevOps
description: Full-Stack Web App Development guides developers through scaffolding, AI features, auth, payments, i18n, deployment, and launch for modern web apps. Use it when building a SaaS or shipping a Next.js product from idea to production.
github: "https://github.com/24kchengYe/human-skill-tree/tree/master/skills/04-fullstack-webapp"
language: TypeScript
stars: 536
forks: 28
install: "npx degit https://github.com/24kchengYe/human-skill-tree/tree/master/skills/04-fullstack-webapp ~/.claude/skills/04-fullstack-webapp"
installs_to: ~/.claude/skills/04-fullstack-webapp
source_path: skills/04-fullstack-webapp/SKILL.md
collection_size: 25
category_size: 798
collection_url: "https://dirskills.com/collections/24kchengYe/human-skill-tree"
added: 2026-08-26T05:11:27.635Z
last_synced: 2026-08-26T05:11:27.635Z
canonical_url: "https://dirskills.com/skills/full-stack-web-app-development"
---

# Full-Stack Web App Development

Full-Stack Web App Development guides developers through scaffolding, AI features, auth, payments, i18n, deployment, and launch for modern web apps. Use it when building a SaaS or shipping a Next.js product from idea to production.

**Install:**

```bash
npx degit https://github.com/24kchengYe/human-skill-tree/tree/master/skills/04-fullstack-webapp ~/.claude/skills/04-fullstack-webapp
```

## README

# Full-Stack Web Application Development

## Description

A comprehensive full-stack web application development coach that guides developers through the complete lifecycle of building, deploying, and monetizing a modern web application. Based on real-world experience shipping a Next.js 16 AI SaaS product from zero to production in 2 weeks (Human Skill Tree project), this skill covers: project scaffolding, AI integration, authentication, payment systems, internationalization, cloud deployment, China accessibility via Cloudflare, and launch strategy. The AI agent acts as a pragmatic tech lead who prioritizes shipping over perfection and makes architecture decisions based on actual production tradeoffs — not theoretical best practices.

## Triggers

Activate this skill when the user:
- Wants to build a web application from scratch
- Asks "how do I deploy my Next.js app" or "how do I add authentication"
- Mentions building a SaaS, AI tool, or web product
- Says "I want to build something like [product]" or "I have an idea for an app"
- Asks about Vercel, Supabase, payment integration, or i18n setup
- Wants to add AI chat/features to their web app (OpenRouter, Vercel AI SDK)
- Asks about making their site accessible in China without ICP filing
- Says "I'm a solo developer" or "independent developer" or "indie hacker"
- Wants to add a subscription/payment system (LemonSqueezy, Stripe, 爱发电)
- Asks about technical architecture decisions for a web project
- Encounters deployment errors, auth issues, or payment webhook problems

## Methodology

- **Ship First, Polish Later**: Get the core user flow working before adding auth, payments, i18n, or animations. Validate the idea with a working MVP before investing in infrastructure.
- **Progressive Enhancement**: Start with `localStorage`, add Supabase cloud sync later. Start without auth, add it when you need user accounts. Each layer is independent and can be added or removed without breaking others.
- **Pragmatic Architecture**: Choose boring, proven technology that works. Optimize for developer velocity, not theoretical purity. One person shipping beats a team debating architecture.
- **Phase-Based Development**: Never build everything at once. Each phase produces a deployable product. Deploy after every phase, not just at the end.
- **Fail Fast, Fix Fast**: Deploy early, monitor errors, iterate. A deployed MVP with bugs teaches you more than a perfect local prototype.
- **Decision Documentation**: Record WHY you chose each technology (tradeoffs), not just WHAT. Future-you needs the reasoning to make changes confidently.

## Instructions

You are a Full-Stack Web Development Coach. Your mission is to help developers ship real products — not just write code, but make architectural decisions, avoid common pitfalls, and navigate the full journey from idea to deployed, monetized application.

### Phase-Based Development Order

**Never skip phases. Each phase produces a deployable product.**

```
Phase 0: Project Initialization (scaffolding, git, first deploy)
Phase 1: MVP Core Feature (one user flow, no auth, no payment, localStorage)
Phase 2: UI/UX Polish (theme, responsive, animations, micro-interactions)
Phase 3: Data Persistence (localStorage → Supabase, cloud sync)
Phase 4: Authentication (OAuth + email via Supabase Auth)
Phase 5: Payment System (subscription tiers, webhooks, plan enforcement)
Phase 6: Internationalization (next-intl, multi-language)
Phase 7: Deployment + Custom Domain (Vercel CLI, DNS)
Phase 8: Regional Accessibility (Cloudflare CDN for China, geo-detection)
Phase 9: Launch + Promotion (README, social media, Product Hunt)
```

### Phase 0: Project Initialization

```bash
npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir
cd my-app

# Core dependencies (install what you need)
npm install ai @ai-sdk/openai              # Vercel AI SDK (for AI features)
npm install next-intl                       # i18n (if multi-language)
npm install next-themes                     # Theme toggle
npm install @supabase/supabase-js @supabase/ssr  # Auth + Database

# UI components
npx shadcn@latest init                      # Component library
# Common components: button, card, dialog, input, badge, tabs, toast, scroll-area

# Visualization (if needed)
npm install @xyflow/react                   # Node graphs, flow charts

# Dev setup
git init && git add -A && git commit -m "init"
npx vercel                                  # First deploy (blank app)
```

**Environment variables template** (.env.local.example):
```bash
# AI (OpenRouter - one key for 18+ models)
OPENAI_API_KEY=sk-or-v1-xxxx
OPENAI_BASE_URL=https://openrouter.ai/api/v1

# Supabase (add when needed in Phase 3-4)
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxx
SUPABASE_SERVICE_ROLE_KEY=eyJxxx            # Server-only, never expose to client

# Payment (add when needed in Phase 5)
NEXT_PUBLIC_LS_BASIC_CHECKOUT=https://xxx.lemonsqueezy.com/buy/xxx
NEXT_PUBLIC_LS_PRO_CHECKOUT=https://xxx.lemonsqueezy.com/buy/xxx
LEMONSQUEEZY_WEBHOOK_SECRET=xxx
NEXT_PUBLIC_AFDIAN_URL=https://afdian.com/a/xxx

# Admin
NEXT_PUBLIC_ADMIN_EMAILS=your@email.com
```

**File structure convention** (establish early):
```
src/
├── app/
│   ├── [locale]/              # i18n route prefix
│   │   ├── page.tsx           # Landing / home
│   │   ├── dashboard/         # Main feature pages
│   │   └── layout.tsx         # Nav + Context Providers
│   └── api/
│       ├── chat/route.ts      # AI streaming endpoint
│       ├── auth/callback/     # OAuth callback
│       └── webhooks/          # Payment webhooks
├── components/
│   ├── ui/                    # shadcn/ui base components
│   ├── auth/                  # Login, auth provider, profile
│   ├── landing/               # Landing page sections
│   └── [feature]/             # Feature-specific components
├── lib/
│   ├── supabase/
│   │   ├── client.ts          # Browser client (createBrowserClient)
│   │   ├── server.ts          # Server client (createServerClient)
│   │   └── middleware.ts      # Session refresh (updateSession)
│   ├── models.ts              # AI model config + plan restrictions
│   └── constants.ts           # Global constants
├── i18n/
│   ├── routing.ts             # Locales, default locale
│   ├── request.ts             # Message loading
│   └── navigation.ts          # Locale-aware Link/redirect
└── middleware.ts               # Global: i18n routing + auth session
messages/
├── en.json
├── zh.json
└── ja.json
```

### Phase 1: MVP Core Feature

**Principle: Build ONE user flow end-to-end. No auth. No payment. No i18n.**

#### AI Chat API (if building an AI product)

```typescript
// src/app/api/chat/route.ts
import { streamText } from "ai";
import { createOpenAI } from "@ai-sdk/openai";

const openai = createOpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: process.env.OPENAI_BASE_URL,
  compatibility: "compatible",  // CRITICAL for OpenRouter
});

export async function POST(request: Request) {
  const { messages, model } = await request.json();

  const result = streamText({
    model: openai.chatModel(model || "deepseek/deepseek-chat-v3-0324"),
    // ↑ MUST use .chatModel() not openai() directly
    // openai() defaults to Responses API which OpenRouter doesn't support
    messages,
    system: "Your system prompt here",
  });

  return result.toDataStreamResponse();
}
```

```typescript
// Client-side: use useChat hook
import { useChat } from "ai/react";

const { messages, input, handleInputChange, handleSubmit, isLoading, stop } = useChat({
  api: "/api/chat",
  body: { model: selectedModel },
});
```

**Critical AI integration lessons:**
1. `openai.chatModel(id)` NOT `openai(id)` — the latter uses Responses API, fails on OpenRouter
2. `compatibility: "compatible"` is required for OpenRouter
3. OpenRouter gives you 18+ models with one API key — offer model switching to users
4. For structured output without JSON mode: embed data in HTML comments `<!--KP: concept1 | concept2-->` and parse client-side

#### Data Storage (MVP: localStorage)

```typescript
function saveData(key: string, data: unknown) {
  try { localStorage.setItem(key, JSON.stringify(data)); } catch {}
}
function loadData<T>(key: string, fallback: T): T {
  try {
    const v = localStorage.getItem(key);
    return v ? JSON.parse(v) : fallback;
  } catch { return fallback; }
}
```

**Why localStorage first:** No backend needed. No registration. No database setup. Pure frontend. You can add cloud sync later without changing data structures.

### Phase 2: UI/UX Polish

**Tailwind CSS v4 setup** (postcss, NOT tailwind.config.js):
```javascript
// postcss.config.mjs
export default { plugins: { "@tailwindcss/postcss": {} } };
```

**Key UI patterns:**

```html
<!-- Ambient glow background -->
<div class="pointer-events-none absolute top-[-20%] left-1/2 -translate-x-1/2
  h-[500px] w-[800px] rounded-full bg-purple-600/10 blur-[120px]" />

<!-- Glass navigation bar -->
<nav class="sticky top-0 z-50 flex h-16 items-center border-b
  border-border/50 bg-background/70 backdrop-blur-xl">

<!-- Gradient CTA button -->
<button class="bg-gradient-to-r from-purple-600 to-pink-600
  hover:from-purple-500 hover:to-pink-500 text-white shadow-lg
  shadow-purple-500/25 transition-all hover:scale-105">
```

**z-index layer convention:**
```
z-10    Floating elements
z-20    Dropdowns (add CSS `isolate` on parent container!)
z-50    Modal overlays, mobile sidebars
z-[200] Full-screen modals (pricing, onboarding)
```

**The `isolate` fix:** If a dropdown in the header is hidden behind page content, add `isolate` class to the header's parent. This creates a new stacking context, forcing correct z-order without z-index wars.

### Phase 3: Data Persistence (Supabase)

**Migration strategy:** localStorage (Phase 1) → dual-write (Phase 3) → Supabase primary (Phase 4+)

**Three Supabase clients:**

```typescript
// Browser client (components)
import { createBrowserClient } from "@supabase/ssr";
export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );
}

// Server client (server components, API routes)
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createClient() {
  const cookieStore = await cookies();
  return createServerClient(url, anonKey, {
    cookies: {
      getAll: () => cookieStore.getAll(),
      setAll: (c) => c.forEach(({ name, value, options }) =>
        cookieStore.set(name, value, options)),
    },
  });
}

// Service role client (webhooks only — full admin access)
import { createClient as createSupabaseClient } from "@supabase/supabase-js";
const supabaseAdmin = createSupabaseClient(url, serviceRoleKey);
```

**Database schema (typical SaaS):**

```sql
-- User profiles with plan info
CREATE TABLE profiles (
  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  username TEXT UNIQUE,
  avatar_url TEXT,
  email TEXT,
  plan TEXT DEFAULT 'free' CHECK (plan IN ('free','basic','pro','admin')),
  plan_expires_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Usage tracking (for rate limiting)
CREATE TABLE usage_logs (
  id BIGSERIAL PRIMARY KEY,
  user_id UUID REFERENCES profiles(id),
  action TEXT NOT NULL,       -- 'message', 'export', etc.
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Generic KV store for cloud sync (replaces localStorage)
CREATE TABLE user_data (
  user_id UUID REFERENCES profiles(id),
  data_key TEXT NOT NULL,
  data_value JSONB NOT NULL,
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  PRIMARY KEY (user_id, data_key)
);

-- RLS: users can only access own data
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "own_profile" ON profiles
  FOR ALL USING (auth.uid() = id);

-- Auto-create profile on signup
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$ BEGIN
  INSERT INTO profiles (id, email)
  VALUES (NEW.id, NEW.email);
  RETURN NEW;
END; $$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION handle_new_user();
```

**Cloud sync pattern:**
```typescript
// Upload: localStorage → Supabase (on sign-out, periodic)
async function uploadToCloud(userId: string) {
  const keys = ["chat-history", "learning-data", "settings"];
  for (const key of keys) {
    const data = localStorage.getItem(key);
    if (data) {
      await supabase.from("user_data").upsert({
        user_id: userId, data_key: key,
        data_value: JSON.parse(data),
        updated_at: new Date().toISOString(),
      });
    }
  }
}

// Download: Supabase → localStorage (on sign-in)
async function downloadFromCloud(userId: string) {
  const { data } = await supabase.from("user_data")
    .select("data_key, data_value").eq("user_id", userId);
  data?.forEach(({ data_key, data_value }) => {
    localStorage.setItem(data_key, JSON.stringify(data_value));
  });
}
```

**Sync timing:** Login → download. Logout → upload. Background → every 5 min upload.

### Phase 4: Authentication

**Supabase Dashboard setup:**
```
Authentication → Providers:
  ✅ Email (disable "Confirm email" unless you have custom SMTP domain)
  ✅ Google (Client ID + Secret from Google Cloud Console)
  ✅ GitHub (Client ID + Secret from GitHub Developer Settings)

Authentication → URL Configuration:
  Site URL: https://your-domain.com
  Redirect URLs:
    https://your-domain.com/**
    https://your-custom-domain.com/**
    http://localhost:3000/**
```

**Google OAuth setup:**
```
1. console.cloud.google.com → APIs & Services → Credentials
2. Create OAuth 2.0 Client → Web application
3. Authorized redirect URI: https://xxx.supabase.co/auth/v1/callback
4. Copy Client ID + Secret → Supabase → Providers → Google
```

**GitHub OAuth setup:**
```
1. github.com/settings/developers → New OAuth App
2. Callback URL: https://xxx.supabase.co/auth/v1/callback
3. Copy Client ID + Secret → Supabase → Providers → GitHub
```

**Auth callback route:**
```typescript
// src/app/api/auth/callback/route.ts
export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url);
  const code = searchParams.get("code");
  if (code) {
    const supabase = await createClient();
    const { error } = await supabase.auth.exchangeCodeForSession(code);
    if (!error) return NextResponse.redirect(`${origin}/?auth=confirmed`);
  }
  return NextResponse.redirect(`${origin}/?auth=error`);
}
```

**Middleware session refresh (CRITICAL — without this, auth breaks on page refresh):**
```typescript
// src/lib/supabase/middleware.ts
export async function updateSession(request: NextRequest, response: NextResponse) {
  const supabase = createServerClient(url, anonKey, {
    cookies: {
      getAll: () => request.cookies.getAll(),
      setAll: (cookiesToSet) => {
        cookiesToSet.forEach(({ name, value, options }) => {
          response.cookies.set(name, value, options);
        });
      },
    },
  });
  await supabase.auth.getUser();  // This refreshes the session cookie
  return response;
}
```

**Email service (Resend) caveat:**
- Resend free tier without custom domain: can ONLY send to your own account email
- Workaround: disable email confirmation in Supabase until you have a custom domain
- With custom domain: configure Resend SMTP in Supabase (smtp.resend.com, port 465)

**China network issue with Supabase:**
- `supabase.auth.getSession()` may hang from China (network timeout)
- Fix: wrap all auth calls in `Promise.race` with 3-5 second timeout
- Clear local state immediately before async network calls (so UI updates even if network fails)

### Phase 5: Payment System

**Dual payment channels** (for China + international):

| Channel | Region | Method | Automation |
|---------|--------|--------|------------|
| LemonSqueezy | International | Credit card, PayPal | Webhook (automatic) |
| 爱发电 (Afdian) | China | WeChat Pay, Alipay | Manual verification |

**LemonSqueezy webhook:**
```typescript
// src/app/api/webhooks/lemonsqueezy/route.ts
import crypto from "crypto";

export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get("x-signature");

  // Verify HMAC signature (ALWAYS do this)
  const hmac = crypto.createHmac("sha256", process.env.LEMONSQUEEZY_WEBHOOK_SECRET!);
  const digest = hmac.update(body).digest("hex");
  if (digest !== signature) return new Response("Unauthorized", { status: 401 });

  const event = JSON.parse(body);
  const { meta, data } = event;

  if (meta.event_name === "order_created") {
    const email = data.attributes.user_email;
    const variantId = String(data.attributes.first_order_item?.variant_id);

    // Map variant ID to plan
    const plan = variantId === process.env.LS_PRO_VARIANT_ID ? "pro" : "basic";
    const expiresAt = new Date();
    expiresAt.setMonth(expiresAt.getMonth() + 1);

    // Find user by email, update plan
    const { data: profile } = await supabaseAdmin
      .from("profiles").select("id").eq("email", email).single();

    if (profile) {
      await supabaseAdmin.from("profiles").update({
        plan, plan_expires_at: expiresAt.toISOString()
      }).eq("id", profile.id);
    }
  }

  return new Response("OK");
}
```

**Frontend plan refresh (critical — payment happens on external site):**
```typescript
// In auth-provider.tsx
useEffect(() => {
  if (!user) return;
  const refresh = () => fetchPlanInfo(user.id, user.email);

  // Poll every 60 seconds
  const interval = setInterval(refresh, 60_000);

  // Refresh when tab becomes visible (user returns from payment page)
  const onVisible = () => {
    if (document.visibilityState === "visible") refresh();
  };
  document.addEventListener("visibilitychange", onVisible);

  return () => {
    clearInterval(interval);
    document.removeEventListener("visibilitychange", onVisible);
  };
}, [user]);
```

**API-level plan enforcement (NEVER trust frontend only):**
```typescript
// In API route: check plan + usage before processing
const plan = profile?.plan || "free";

// Check model access
if (!canAccessModel(requestedModel, plan)) {
  return new Response("Upgrade required", { status: 403 });
}

// Check daily usage limit
const LIMITS = { free: 10, basic: 100, pro: Infinity, admin: Infinity };
const todayUsage = await countTodayUsage(userId);
if (todayUsage >= LIMITS[plan]) {
  return new Response("Daily limit reached", { status: 429 });
}

// Log usage
await supabase.from("usage_logs").insert({ user_id: userId, action: "message" });
```

### Phase 6: Internationalization (i18n)

```typescript
// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
  locales: ["en", "zh", "ja"],
  defaultLocale: "en",
  localeDetection: false,  // We handle detection manually in middleware
});
```

**Translation files:** ~200-300 keys per language for a medium app. Use AI to batch-translate — provide context/glossary for consistent terminology.

**Namespace organization:**
```json
{
  "nav": { "home": "Home", "dashboard": "Dashboard" },
  "auth": { "login": "Log In", "logout": "Log Out" },
  "pricing": { "title": "Upgrade Plan", "month": "month" },
  "chat": { "placeholder": "Type a message...", "send": "Send" }
}
```

### Phase 7-8: Deployment + China Access

**Vercel CLI deployment:**
```bash
npx next build          # Verify locally first
npx vercel --prod       # Deploy to production
```

**Why CLI over Git integration:** Git integration requires linking Git account, causes confusion with multiple accounts, and triggers auto-deploy on every push. CLI gives full control.

**Cloudflare CDN for China access (free, no ICP filing):**
```
1. Have a domain on Cloudflare (e.g., yourdomain.com)
2. Vercel: Settings → Domains → Add "app.yourdomain.com"
3. Cloudflare: DNS → Add Record:
   Type: CNAME | Name
