{"id":"9098f02c-19f3-4bba-a2e4-872b79d0efb7","shortId":"7MLYhK","kind":"skill","title":"04-fullstack-webapp","tagline":"🌳 AI-Powered Skill Tree for Lifelong Human Learning. 30+ skills from K-12 to career & social intelligence, built on cognitive science. | 人类养成记：AI 驱动的终身学习技能树","description":"# Full-Stack Web Application Development\n\n## Description\n\nA 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.\n\n## Triggers\n\nActivate this skill when the user:\n- Wants to build a web application from scratch\n- Asks \"how do I deploy my Next.js app\" or \"how do I add authentication\"\n- Mentions building a SaaS, AI tool, or web product\n- Says \"I want to build something like [product]\" or \"I have an idea for an app\"\n- Asks about Vercel, Supabase, payment integration, or i18n setup\n- Wants to add AI chat/features to their web app (OpenRouter, Vercel AI SDK)\n- Asks about making their site accessible in China without ICP filing\n- Says \"I'm a solo developer\" or \"independent developer\" or \"indie hacker\"\n- Wants to add a subscription/payment system (LemonSqueezy, Stripe, 爱发电)\n- Asks about technical architecture decisions for a web project\n- Encounters deployment errors, auth issues, or payment webhook problems\n\n## Methodology\n\n- **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.\n- **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.\n- **Pragmatic Architecture**: Choose boring, proven technology that works. Optimize for developer velocity, not theoretical purity. One person shipping beats a team debating architecture.\n- **Phase-Based Development**: Never build everything at once. Each phase produces a deployable product. Deploy after every phase, not just at the end.\n- **Fail Fast, Fix Fast**: Deploy early, monitor errors, iterate. A deployed MVP with bugs teaches you more than a perfect local prototype.\n- **Decision Documentation**: Record WHY you chose each technology (tradeoffs), not just WHAT. Future-you needs the reasoning to make changes confidently.\n\n## Instructions\n\nYou 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.\n\n### Phase-Based Development Order\n\n**Never skip phases. Each phase produces a deployable product.**\n\n```\nPhase 0: Project Initialization (scaffolding, git, first deploy)\nPhase 1: MVP Core Feature (one user flow, no auth, no payment, localStorage)\nPhase 2: UI/UX Polish (theme, responsive, animations, micro-interactions)\nPhase 3: Data Persistence (localStorage → Supabase, cloud sync)\nPhase 4: Authentication (OAuth + email via Supabase Auth)\nPhase 5: Payment System (subscription tiers, webhooks, plan enforcement)\nPhase 6: Internationalization (next-intl, multi-language)\nPhase 7: Deployment + Custom Domain (Vercel CLI, DNS)\nPhase 8: Regional Accessibility (Cloudflare CDN for China, geo-detection)\nPhase 9: Launch + Promotion (README, social media, Product Hunt)\n```\n\n### Phase 0: Project Initialization\n\n```bash\nnpx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir\ncd my-app\n\n# Core dependencies (install what you need)\nnpm install ai @ai-sdk/openai              # Vercel AI SDK (for AI features)\nnpm install next-intl                       # i18n (if multi-language)\nnpm install next-themes                     # Theme toggle\nnpm install @supabase/supabase-js @supabase/ssr  # Auth + Database\n\n# UI components\nnpx shadcn@latest init                      # Component library\n# Common components: button, card, dialog, input, badge, tabs, toast, scroll-area\n\n# Visualization (if needed)\nnpm install @xyflow/react                   # Node graphs, flow charts\n\n# Dev setup\ngit init && git add -A && git commit -m \"init\"\nnpx vercel                                  # First deploy (blank app)\n```\n\n**Environment variables template** (.env.local.example):\n```bash\n# AI (OpenRouter - one key for 18+ models)\nOPENAI_API_KEY=sk-or-v1-xxxx\nOPENAI_BASE_URL=https://openrouter.ai/api/v1\n\n# Supabase (add when needed in Phase 3-4)\nNEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co\nNEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxx\nSUPABASE_SERVICE_ROLE_KEY=eyJxxx            # Server-only, never expose to client\n\n# Payment (add when needed in Phase 5)\nNEXT_PUBLIC_LS_BASIC_CHECKOUT=https://xxx.lemonsqueezy.com/buy/xxx\nNEXT_PUBLIC_LS_PRO_CHECKOUT=https://xxx.lemonsqueezy.com/buy/xxx\nLEMONSQUEEZY_WEBHOOK_SECRET=xxx\nNEXT_PUBLIC_AFDIAN_URL=https://afdian.com/a/xxx\n\n# Admin\nNEXT_PUBLIC_ADMIN_EMAILS=your@email.com\n```\n\n**File structure convention** (establish early):\n```\nsrc/\n├── app/\n│   ├── [locale]/              # i18n route prefix\n│   │   ├── page.tsx           # Landing / home\n│   │   ├── dashboard/         # Main feature pages\n│   │   └── layout.tsx         # Nav + Context Providers\n│   └── api/\n│       ├── chat/route.ts      # AI streaming endpoint\n│       ├── auth/callback/     # OAuth callback\n│       └── webhooks/          # Payment webhooks\n├── components/\n│   ├── ui/                    # shadcn/ui base components\n│   ├── auth/                  # Login, auth provider, profile\n│   ├── landing/               # Landing page sections\n│   └── [feature]/             # Feature-specific components\n├── lib/\n│   ├── supabase/\n│   │   ├── client.ts          # Browser client (createBrowserClient)\n│   │   ├── server.ts          # Server client (createServerClient)\n│   │   └── middleware.ts      # Session refresh (updateSession)\n│   ├── models.ts              # AI model config + plan restrictions\n│   └── constants.ts           # Global constants\n├── i18n/\n│   ├── routing.ts             # Locales, default locale\n│   ├── request.ts             # Message loading\n│   └── navigation.ts          # Locale-aware Link/redirect\n└── middleware.ts               # Global: i18n routing + auth session\nmessages/\n├── en.json\n├── zh.json\n└── ja.json\n```\n\n### Phase 1: MVP Core Feature\n\n**Principle: Build ONE user flow end-to-end. No auth. No payment. No i18n.**\n\n#### AI Chat API (if building an AI product)\n\n```typescript\n// src/app/api/chat/route.ts\nimport { streamText } from \"ai\";\nimport { createOpenAI } from \"@ai-sdk/openai\";\n\nconst openai = createOpenAI({\n  apiKey: process.env.OPENAI_API_KEY,\n  baseURL: process.env.OPENAI_BASE_URL,\n  compatibility: \"compatible\",  // CRITICAL for OpenRouter\n});\n\nexport async function POST(request: Request) {\n  const { messages, model } = await request.json();\n\n  const result = streamText({\n    model: openai.chatModel(model || \"deepseek/deepseek-chat-v3-0324\"),\n    // ↑ MUST use .chatModel() not openai() directly\n    // openai() defaults to Responses API which OpenRouter doesn't support\n    messages,\n    system: \"Your system prompt here\",\n  });\n\n  return result.toDataStreamResponse();\n}\n```\n\n```typescript\n// Client-side: use useChat hook\nimport { useChat } from \"ai/react\";\n\nconst { messages, input, handleInputChange, handleSubmit, isLoading, stop } = useChat({\n  api: \"/api/chat\",\n  body: { model: selectedModel },\n});\n```\n\n**Critical AI integration lessons:**\n1. `openai.chatModel(id)` NOT `openai(id)` — the latter uses Responses API, fails on OpenRouter\n2. `compatibility: \"compatible\"` is required for OpenRouter\n3. OpenRouter gives you 18+ models with one API key — offer model switching to users\n4. For structured output without JSON mode: embed data in HTML comments `<!--KP: concept1 | concept2-->` and parse client-side\n\n#### Data Storage (MVP: localStorage)\n\n```typescript\nfunction saveData(key: string, data: unknown) {\n  try { localStorage.setItem(key, JSON.stringify(data)); } catch {}\n}\nfunction loadData<T>(key: string, fallback: T): T {\n  try {\n    const v = localStorage.getItem(key);\n    return v ? JSON.parse(v) : fallback;\n  } catch { return fallback; }\n}\n```\n\n**Why localStorage first:** No backend needed. No registration. No database setup. Pure frontend. You can add cloud sync later without changing data structures.\n\n### Phase 2: UI/UX Polish\n\n**Tailwind CSS v4 setup** (postcss, NOT tailwind.config.js):\n```javascript\n// postcss.config.mjs\nexport default { plugins: { \"@tailwindcss/postcss\": {} } };\n```\n\n**Key UI patterns:**\n\n```html\n<!-- Ambient glow background -->\n<div class=\"pointer-events-none absolute top-[-20%] left-1/2 -translate-x-1/2\n  h-[500px] w-[800px] rounded-full bg-purple-600/10 blur-[120px]\" />\n\n<!-- Glass navigation bar -->\n<nav class=\"sticky top-0 z-50 flex h-16 items-center border-b\n  border-border/50 bg-background/70 backdrop-blur-xl\">\n\n<!-- Gradient CTA button -->\n<button class=\"bg-gradient-to-r from-purple-600 to-pink-600\n  hover:from-purple-500 hover:to-pink-500 text-white shadow-lg\n  shadow-purple-500/25 transition-all hover:scale-105\">\n```\n\n**z-index layer convention:**\n```\nz-10    Floating elements\nz-20    Dropdowns (add CSS `isolate` on parent container!)\nz-50    Modal overlays, mobile sidebars\nz-[200] Full-screen modals (pricing, onboarding)\n```\n\n**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.\n\n### Phase 3: Data Persistence (Supabase)\n\n**Migration strategy:** localStorage (Phase 1) → dual-write (Phase 3) → Supabase primary (Phase 4+)\n\n**Three Supabase clients:**\n\n```typescript\n// Browser client (components)\nimport { createBrowserClient } from \"@supabase/ssr\";\nexport function createClient() {\n  return createBrowserClient(\n    process.env.NEXT_PUBLIC_SUPABASE_URL!,\n    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!\n  );\n}\n\n// Server client (server components, API routes)\nimport { createServerClient } from \"@supabase/ssr\";\nimport { cookies } from \"next/headers\";\nexport async function createClient() {\n  const cookieStore = await cookies();\n  return createServerClient(url, anonKey, {\n    cookies: {\n      getAll: () => cookieStore.getAll(),\n      setAll: (c) => c.forEach(({ name, value, options }) =>\n        cookieStore.set(name, value, options)),\n    },\n  });\n}\n\n// Service role client (webhooks only — full admin access)\nimport { createClient as createSupabaseClient } from \"@supabase/supabase-js\";\nconst supabaseAdmin = createSupabaseClient(url, serviceRoleKey);\n```\n\n**Database schema (typical SaaS):**\n\n```sql\n-- User profiles with plan info\nCREATE TABLE profiles (\n  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,\n  username TEXT UNIQUE,\n  avatar_url TEXT,\n  email TEXT,\n  plan TEXT DEFAULT 'free' CHECK (plan IN ('free','basic','pro','admin')),\n  plan_expires_at TIMESTAMPTZ,\n  created_at TIMESTAMPTZ DEFAULT NOW()\n);\n\n-- Usage tracking (for rate limiting)\nCREATE TABLE usage_logs (\n  id BIGSERIAL PRIMARY KEY,\n  user_id UUID REFERENCES profiles(id),\n  action TEXT NOT NULL,       -- 'message', 'export', etc.\n  created_at TIMESTAMPTZ DEFAULT NOW()\n);\n\n-- Generic KV store for cloud sync (replaces localStorage)\nCREATE TABLE user_data (\n  user_id UUID REFERENCES profiles(id),\n  data_key TEXT NOT NULL,\n  data_value JSONB NOT NULL,\n  updated_at TIMESTAMPTZ DEFAULT NOW(),\n  PRIMARY KEY (user_id, data_key)\n);\n\n-- RLS: users can only access own data\nALTER TABLE profiles ENABLE ROW LEVEL SECURITY;\nCREATE POLICY \"own_profile\" ON profiles\n  FOR ALL USING (auth.uid() = id);\n\n-- Auto-create profile on signup\nCREATE OR REPLACE FUNCTION handle_new_user()\nRETURNS TRIGGER AS $$ BEGIN\n  INSERT INTO profiles (id, email)\n  VALUES (NEW.id, NEW.email);\n  RETURN NEW;\nEND; $$ LANGUAGE plpgsql SECURITY DEFINER;\n\nCREATE TRIGGER on_auth_user_created\n  AFTER INSERT ON auth.users\n  FOR EACH ROW EXECUTE FUNCTION handle_new_user();\n```\n\n**Cloud sync pattern:**\n```typescript\n// Upload: localStorage → Supabase (on sign-out, periodic)\nasync function uploadToCloud(userId: string) {\n  const keys = [\"chat-history\", \"learning-data\", \"settings\"];\n  for (const key of keys) {\n    const data = localStorage.getItem(key);\n    if (data) {\n      await supabase.from(\"user_data\").upsert({\n        user_id: userId, data_key: key,\n        data_value: JSON.parse(data),\n        updated_at: new Date().toISOString(),\n      });\n    }\n  }\n}\n\n// Download: Supabase → localStorage (on sign-in)\nasync function downloadFromCloud(userId: string) {\n  const { data } = await supabase.from(\"user_data\")\n    .select(\"data_key, data_value\").eq(\"user_id\", userId);\n  data?.forEach(({ data_key, data_value }) => {\n    localStorage.setItem(data_key, JSON.stringify(data_value));\n  });\n}\n```\n\n**Sync timing:** Login → download. Logout → upload. Background → every 5 min upload.\n\n### Phase 4: Authentication\n\n**Supabase Dashboard setup:**\n```\nAuthentication → Providers:\n  ✅ Email (disable \"Confirm email\" unless you have custom SMTP domain)\n  ✅ Google (Client ID + Secret from Google Cloud Console)\n  ✅ GitHub (Client ID + Secret from GitHub Developer Settings)\n\nAuthentication → URL Configuration:\n  Site URL: https://your-domain.com\n  Redirect URLs:\n    https://your-domain.com/**\n    https://your-custom-domain.com/**\n    http://localhost:3000/**\n```\n\n**Google OAuth setup:**\n```\n1. console.cloud.google.com → APIs & Services → Credentials\n2. Create OAuth 2.0 Client → Web application\n3. Authorized redirect URI: https://xxx.supabase.co/auth/v1/callback\n4. Copy Client ID + Secret → Supabase → Providers → Google\n```\n\n**GitHub OAuth setup:**\n```\n1. github.com/settings/developers → New OAuth App\n2. Callback URL: https://xxx.supabase.co/auth/v1/callback\n3. Copy Client ID + Secret → Supabase → Providers → GitHub\n```\n\n**Auth callback route:**\n```typescript\n// src/app/api/auth/callback/route.ts\nexport async function GET(request: Request) {\n  const { searchParams, origin } = new URL(request.url);\n  const code = searchParams.get(\"code\");\n  if (code) {\n    const supabase = await createClient();\n    const { error } = await supabase.auth.exchangeCodeForSession(code);\n    if (!error) return NextResponse.redirect(`${origin}/?auth=confirmed`);\n  }\n  return NextResponse.redirect(`${origin}/?auth=error`);\n}\n```\n\n**Middleware session refresh (CRITICAL — without this, auth breaks on page refresh):**\n```typescript\n// src/lib/supabase/middleware.ts\nexport async function updateSession(request: NextRequest, response: NextResponse) {\n  const supabase = createServerClient(url, anonKey, {\n    cookies: {\n      getAll: () => request.cookies.getAll(),\n      setAll: (cookiesToSet) => {\n        cookiesToSet.forEach(({ name, value, options }) => {\n          response.cookies.set(name, value, options);\n        });\n      },\n    },\n  });\n  await supabase.auth.getUser();  // This refreshes the session cookie\n  return response;\n}\n```\n\n**Email service (Resend) caveat:**\n- Resend free tier without custom domain: can ONLY send to your own account email\n- Workaround: disable email confirmation in Supabase until you have a custom domain\n- With custom domain: configure Resend SMTP in Supabase (smtp.resend.com, port 465)\n\n**China network issue with Supabase:**\n- `supabase.auth.getSession()` may hang from China (network timeout)\n- Fix: wrap all auth calls in `Promise.race` with 3-5 second timeout\n- Clear local state immediately before async network calls (so UI updates even if network fails)\n\n### Phase 5: Payment System\n\n**Dual payment channels** (for China + international):\n\n| Channel | Region | Method | Automation |\n|---------|--------|--------|------------|\n| LemonSqueezy | International | Credit card, PayPal | Webhook (automatic) |\n| 爱发电 (Afdian) | China | WeChat Pay, Alipay | Manual verification |\n\n**LemonSqueezy webhook:**\n```typescript\n// src/app/api/webhooks/lemonsqueezy/route.ts\nimport crypto from \"crypto\";\n\nexport async function POST(request: Request) {\n  const body = await request.text();\n  const signature = request.headers.get(\"x-signature\");\n\n  // Verify HMAC signature (ALWAYS do this)\n  const hmac = crypto.createHmac(\"sha256\", process.env.LEMONSQUEEZY_WEBHOOK_SECRET!);\n  const digest = hmac.update(body).digest(\"hex\");\n  if (digest !== signature) return new Response(\"Unauthorized\", { status: 401 });\n\n  const event = JSON.parse(body);\n  const { meta, data } = event;\n\n  if (meta.event_name === \"order_created\") {\n    const email = data.attributes.user_email;\n    const variantId = String(data.attributes.first_order_item?.variant_id);\n\n    // Map variant ID to plan\n    const plan = variantId === process.env.LS_PRO_VARIANT_ID ? \"pro\" : \"basic\";\n    const expiresAt = new Date();\n    expiresAt.setMonth(expiresAt.getMonth() + 1);\n\n    // Find user by email, update plan\n    const { data: profile } = await supabaseAdmin\n      .from(\"profiles\").select(\"id\").eq(\"email\", email).single();\n\n    if (profile) {\n      await supabaseAdmin.from(\"profiles\").update({\n        plan, plan_expires_at: expiresAt.toISOString()\n      }).eq(\"id\", profile.id);\n    }\n  }\n\n  return new Response(\"OK\");\n}\n```\n\n**Frontend plan refresh (critical — payment happens on external site):**\n```typescript\n// In auth-provider.tsx\nuseEffect(() => {\n  if (!user) return;\n  const refresh = () => fetchPlanInfo(user.id, user.email);\n\n  // Poll every 60 seconds\n  const interval = setInterval(refresh, 60_000);\n\n  // Refresh when tab becomes visible (user returns from payment page)\n  const onVisible = () => {\n    if (document.visibilityState === \"visible\") refresh();\n  };\n  document.addEventListener(\"visibilitychange\", onVisible);\n\n  return () => {\n    clearInterval(interval);\n    document.removeEventListener(\"visibilitychange\", onVisible);\n  };\n}, [user]);\n```\n\n**API-level plan enforcement (NEVER trust frontend only):**\n```typescript\n// In API route: check plan + usage before processing\nconst plan = profile?.plan || \"free\";\n\n// Check model access\nif (!canAccessModel(requestedModel, plan)) {\n  return new Response(\"Upgrade required\", { status: 403 });\n}\n\n// Check daily usage limit\nconst LIMITS = { free: 10, basic: 100, pro: Infinity, admin: Infinity };\nconst todayUsage = await countTodayUsage(userId);\nif (todayUsage >= LIMITS[plan]) {\n  return new Response(\"Daily limit reached\", { status: 429 });\n}\n\n// Log usage\nawait supabase.from(\"usage_logs\").insert({ user_id: userId, action: \"message\" });\n```\n\n### Phase 6: Internationalization (i18n)\n\n```typescript\n// src/i18n/routing.ts\nimport { defineRouting } from \"next-intl/routing\";\nexport const routing = defineRouting({\n  locales: [\"en\", \"zh\", \"ja\"],\n  defaultLocale: \"en\",\n  localeDetection: false,  // We handle detection manually in middleware\n});\n```\n\n**Translation files:** ~200-300 keys per language for a medium app. Use AI to batch-translate — provide context/glossary for consistent terminology.\n\n**Namespace organization:**\n```json\n{\n  \"nav\": { \"home\": \"Home\", \"dashboard\": \"Dashboard\" },\n  \"auth\": { \"login\": \"Log In\", \"logout\": \"Log Out\" },\n  \"pricing\": { \"title\": \"Upgrade Plan\", \"month\": \"month\" },\n  \"chat\": { \"placeholder\": \"Type a message...\", \"send\": \"Send\" }\n}\n```\n\n### Phase 7-8: Deployment + China Access\n\n**Vercel CLI deployment:**\n```bash\nnpx next build          # Verify locally first\nnpx vercel --prod       # Deploy to production\n```\n\n**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.\n\n**Cloudflare CDN for China access (free, no ICP filing):**\n```\n1. Have a domain on Cloudflare (e.g., yourdomain.com)\n2. Vercel: Settings → Domains → Add \"app.yourdomain.com\"\n3. Cloudflare: DNS → Add Record:\n   Type: CNAME | Name: app | Target: cname.vercel-dns.com | Proxy: ON (orange)\n4. Cloudflare: SSL/TLS → \"Full (strict)\" ← CRITICAL! Flexible = infinite redirects\n5. Wait 1-2 min, refresh Vercel Domains page → green ✓\n6. Ignore \"Proxy Detected\" warning, do NOT click \"1-click fix\"\n```\n\n**Geo-detection middleware:**\n```typescript\nfunction getCountry(req: NextRequest): string {\n  return (\n    req.headers.get(\"cf-ipcountry\") ||        // Cloudflare\n    req.headers.get(\"x-vercel-ip-country\") ||  // Vercel\n    \"\"\n  ).toUpperCase();\n}\n\n// In middleware: auto-redirect Chinese users to /zh\nif (!hasLocalePrefix && getCountry(request) === \"CN\") {\n  const url = request.nextUrl.clone();\n  url.pathname = `/zh${pathname}`;\n  return NextResponse.redirect(url);\n}\n```\n\n### Production Checklist\n\n**Before launch:**\n- [ ] `npx next build` passes locally\n- [ ] All env vars set in Vercel (Production environment)\n- [ ] Supabase RLS policies enabled on all tables\n- [ ] API routes verify auth + check usage limits\n- [ ] Webhook endpoints verify signatures\n- [ ] `SUPABASE_SERVICE_ROLE_KEY` never in `NEXT_PUBLIC_*`\n- [ ] Mobile responsive tested\n- [ ] Error handling: API timeout, network disconnect, model unavailable\n\n**After custom domain:**\n- [ ] Supabase Redirect URLs include new domain\n- [ ] Cloudflare SSL = Full (strict)\n- [ ] Both domains accessible (old + new)\n- [ ] Test from China network (disable VPN)\n\n**Before promotion:**\n- [ ] README has demo GIF\n- [ ] Landing page has clear CTA\n- [ ] Visitor badge + Star History in README\n\n## Examples\n\n### Example 1: \"I want to build an AI writing tool\"\n\n**Coach**: Let's ship this in phases.\n\n**Today (Phase 0):** `npx create-next-app`, install `ai @ai-sdk/openai`, deploy blank app to Vercel. 30 minutes.\n\n**Days 1-2 (Phase 1):** One screen — editor on left, AI panel on right. Use `useChat` hook for streaming. Store drafts in localStorage. No login, no payment.\n\n**Day 3 (Phase 2):** Dark theme, glass-morphism nav, responsive layout.\n\n**Day 4 (Phase 3):** Add Supabase. Create `documents` table. Cloud sync when logged in.\n\n**Day 5 (Phase 4):** Supabase Auth (Google + GitHub). Users can save documents cross-device.\n\n**Days 6-7 (Phase 5):** LemonSqueezy. Free: 10 AI calls/day. Pro $9.99/mo: unlimited. Webhook auto-upgrades plan.\n\nDeploy after each phase. Don't wait until everything is \"ready.\"\n\n### Example 2: \"My app works locally but fails on Vercel\"\n\n**Coach**: Check in this order:\n\n1. **Environment variables** (90% of cases): Vercel Dashboard → Settings → Environment Variables. Make sure they're set for \"Production\" not just \"Preview\".\n\n2. **API route runtime**: Add `export const runtime = \"nodejs\"` if you use Node.js APIs like `crypto`. Vercel may default to Edge runtime.\n\n3. **Vercel logs**: Run `vercel logs <deployment-url> --follow` to see the actual error, not just the 500.\n\n### Example 3: \"Users in China can't access my site\"\n\n**Coach**: Cloudflare CNAME proxy — free, no ICP filing, 10-minute setup.\n\nYou need: a domain on Cloudflare (any domain works).\n\nSteps:\n1. Vercel: Settings → Domains → Add `app.yourdomain.com`\n2. Cloudflare DNS: CNAME → `cname.vercel-dns.com`, Proxy ON\n3. Cloudflare SSL/TLS: **Full (strict)** — this is the step everyone forgets\n4. Update Supabase Redirect URLs to include the new domain\n\nCost: $0. China users access via Cloudflare's edge network.\n\n## References\n\n- Vercel AI SDK v6: https://sdk.vercel.ai/docs\n- Supabase Auth: https://supabase.com/docs/guides/auth\n- Supabase SSR: https://supabase.com/docs/guides/auth/server-side\n- next-intl: https://next-intl.dev/docs\n- OpenRouter API: https://openrouter.ai/docs\n- LemonSqueezy Webhooks: https://docs.lemonsqueezy.com/api/webhooks\n- Cloudflare DNS Proxy: https://developers.cloudflare.com/dns/manage-dns-records/reference/proxied-dns-records/\n- shadcn/ui: https://ui.shadcn.com\n- Tailwind CSS v4: https://tailwindcss.com/docs\n- React Flow (@xyflow/react): https://reactflow.dev\n- Bastani et al. (2025). Generative AI without guardrails can harm learning. *PNAS*, 122(26) — evidence that AI products need intentional design","tags":["fullstack","webapp","human","skill","tree","24kchengye","agent-skills","ai-education","ai-learning","ai-tutor","chatgpt","claude-code"],"capabilities":["skill","source-24kchengye","skill-04-fullstack-webapp","topic-agent-skills","topic-ai-education","topic-ai-learning","topic-ai-tutor","topic-chatgpt","topic-claude-code","topic-claude-skills","topic-cognitive-science","topic-copilot","topic-cursor","topic-deepseek","topic-education"],"categories":["human-skill-tree"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/24kchengYe/human-skill-tree/04-fullstack-webapp","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add 24kchengYe/human-skill-tree","source_repo":"https://github.com/24kchengYe/human-skill-tree","install_from":"skills.sh"}},"qualityScore":"0.700","qualityRationale":"deterministic score 0.70 from registry signals: · indexed on github topic:agent-skills · 515 github stars · SKILL.md body (24,056 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-02T18:53:25.032Z","embedding":null,"createdAt":"2026-04-18T21:57:52.584Z","updatedAt":"2026-05-02T18:53:25.032Z","lastSeenAt":"2026-05-02T18:53:25.032Z","tsv":"'-10':1136 '-12':18 '-2':2362,2563 '-20':1140 '-300':2216 '-4':707 '-5':1863 '-50':1149 '-7':2631 '-8':2265 '/**':1649,1652 '/a/xxx':764 '/api/chat':988 '/api/v1':699 '/api/webhooks':2841 '/auth/v1/callback':1676,1700 '/buy/xxx':745,753 '/dns/manage-dns-records/reference/proxied-dns-records/':2847 '/docs':2815,2831,2836,2855 '/docs/guides/auth':2820 '/docs/guides/auth/server-side':2825 '/mo':2641 '/openai':597,909,2553 '/routing':2194 '/settings/developers':1691 '/zh':2412,2422 '0':468,561,2542,2799 '000':2075 '04':1 '1':476,870,996,1209,1658,1688,2007,2322,2361,2377,2524,2562,2565,2674,2764 '10':2146,2636,2751 '100':2148 '122':2872 '16':71 '18':684,1021 '2':80,489,1010,1110,1663,1695,2330,2591,2660,2695,2770 '2.0':1666 '200':1155,2215 '2025':2863 '26':2873 '3':499,706,1017,1201,1214,1670,1701,1862,2336,2589,2603,2717,2734,2777 '30':14,2559 '3000':1654 '4':507,1032,1218,1606,1677,2350,2601,2617,2788 '401':1961 '403':2138 '429':2169 '465':1841 '5':515,737,1602,1882,2359,2615,2633 '500':2732 '6':524,2183,2369,2630 '60':2068,2074 '7':533,2264 '8':541 '9':552 '9.99':2640 '90':2677 'access':100,214,543,1290,1427,2127,2268,2317,2496,2740,2802 'account':307,1817,2295,2300 'act':109 'action':1372,2180 'activ':134 'actual':126,2727 'ad':271,315 'add':160,198,234,293,301,662,701,732,1101,1142,1176,2334,2339,2604,2699,2768 'admin':765,768,1289,1343,2151 'afdian':760,1903 'afdian.com':763 'afdian.com/a/xxx':762 'agent':108 'ai':6,28,72,91,107,166,199,207,593,595,599,602,679,795,838,889,895,902,907,993,2225,2530,2549,2551,2571,2637,2810,2865,2876 'ai-pow':5 'ai-sdk':594,906,2550 'ai/react':978 'al':2862 'alipay':1907 'alter':1430 'alway':1937 'anim':276,494 'anon':716,1242 'anonkey':1269,1778 'api':687,793,891,915,954,987,1006,1025,1248,1660,2103,2113,2451,2475,2696,2708,2833 'api-level':2102 'apikey':913 'app':155,186,204,569,573,577,584,673,777,1694,2223,2344,2547,2556,2662 'app.yourdomain.com':2335,2769 'applic':34,43,61,145,452,1669 'architectur':122,244,322,343,437 'area':646 'ask':148,187,209,241 'async':927,1259,1510,1562,1715,1767,1871,1919 'auth':253,272,300,484,513,625,809,811,863,884,1483,1709,1746,1751,1759,1857,2243,2454,2619,2817 'auth-provider.tsx':2056 'auth.uid':1446 'auth.users':1320,1489 'auth/callback':798 'authent':93,161,508,1607,1611,1639 'author':1671 'auto':1449,2304,2407,2645 'auto-cr':1448 'auto-deploy':2303 'auto-redirect':2406 'auto-upgrad':2644 'autom':1894 'automat':1901 'avatar':1328 'avoid':439 'await':935,1264,1535,1569,1734,1738,1792,1926,2017,2029,2155,2172 'awar':857 'backend':1090 'background':1600 'badg':641,2517 'base':62,124,346,455,695,807,919 'baseurl':917 'bash':564,678,2272 'basic':741,1341,2000,2147 'bastani':2860 'batch':2228 'batch-transl':2227 'beat':339 'becom':2079 'begin':1464 'behind':1173 'best':131 'bigseri':1363 'blank':672,2555 'bodi':989,1925,1950,1965 'bore':324 'break':319,1760 'browser':826,1223 'bug':381 'build':54,142,163,175,349,875,893,2275,2433,2528 'built':23 'button':637 'c':1274 'c.foreach':1275 'call':1858,1873 'callback':800,1696,1710 'calls/day':2638 'canaccessmodel':2129 'card':638,1898 'career':20 'cascad':1324 'case':2679 'catch':1065,1083 'caus':2296 'caveat':1804 'cd':581 'cdn':545,2314 'cf':2393 'cf-ipcountri':2392 'chang':410,1106 'channel':1887,1891 'chart':656 'chat':890,1518,2256 'chat-histori':1517 'chat/features':200 'chat/route.ts':794 'chatmodel':946 'check':1337,2115,2125,2139,2455,2670 'checklist':2428 'checkout':742,750 'china':99,216,547,1842,1851,1889,1904,2267,2316,2501,2737,2800 'chines':2409 'choos':323 'chose':395 'class':1178 'clear':1866,2514 'clearinterv':2096 'cli':538,2270,2286,2309 'click':2376,2378 'client':730,827,831,970,1047,1221,1224,1245,1285,1624,1632,1667,1679,1703 'client-sid':969,1046 'client.ts':825 'cloud':97,295,504,1102,1388,1498,1629,2609 'cloudflar':102,544,2313,2327,2337,2351,2395,2490,2744,2759,2771,2778,2804,2842 'cn':2417 'cname':2342,2745,2773 'cname.vercel-dns.com':2346,2774 'coach':45,421,2533,2669,2743 'code':434,1727,1729,1731,1740 'cognit':25 'comment':1043 'commit':665 'common':440,635 'compat':921,922,1011,1012 'complet':51 'compon':628,633,636,804,808,822,1225,1247 'comprehens':38 'confid':411 'config':840 'configur':1641,1834 'confirm':1615,1747,1822 'confus':2297 'consist':2233 'consol':1630 'console.cloud.google.com':1659 'const':910,932,937,979,1074,1262,1297,1515,1525,1529,1567,1720,1726,1732,1736,1774,1924,1928,1940,1947,1962,1966,1975,1979,1992,2001,2014,2061,2070,2086,2120,2143,2153,2196,2418,2701 'constant':845 'constants.ts':843 'contain':1147 'content':1175 'context':791,1189 'context/glossary':2231 'control':2312 'convent':773,1134 'cooki':1255,1265,1270,1779,1798 'cookiestor':1263 'cookiestore.getall':1272 'cookiestore.set':1279 'cookiestoset':1783 'cookiestoset.foreach':1784 'copi':1678,1702 'core':266,478,585,872 'correct':1191 'cost':2798 'countri':2401 'counttodayusag':2156 'cover':88 'creat':567,1185,1312,1348,1358,1379,1392,1437,1450,1454,1480,1485,1664,1974,2545,2606 'create-next-app':566,2544 'createbrowsercli':828,1227,1234 'createcli':1232,1261,1292,1735 'createopenai':904,912 'createservercli':832,1251,1267,1776 'createsupabasecli':1294,1299 'credenti':1662 'credit':1897 'critic':923,992,1756,2048,2355 'cross':2627 'cross-devic':2626 'crypto':1915,1917,2710 'crypto.createhmac':1942 'css':1114,1143,2851 'cta':2515 'custom':535,1620,1809,1829,1832,2482 'daili':2140,2165 'dark':2592 'dashboard':785,1609,2241,2242,2681 'data':500,1040,1049,1058,1064,1107,1202,1395,1402,1407,1421,1429,1522,1530,1534,1538,1543,1546,1549,1568,1572,1574,1576,1582,1584,1586,1589,1592,1968,2015 'data.attributes.first':1982 'data.attributes.user':1977 'databas':626,1095,1302 'date':1553,2004 'day':2561,2588,2600,2614,2629 'debat':342 'decis':123,245,390,438 'deepseek/deepseek-chat-v3-0324':943 'default':849,951,1123,1335,1351,1382,1415,2713 'defaultlocal':2203 'defin':1479 'definerout':2189,2198 'delet':1323 'demo':2509 'depend':586 'deploy':55,98,152,251,357,359,372,378,450,465,474,534,671,2266,2271,2282,2305,2554,2648 'descript':36 'design':2880 'detect':550,2209,2372,2382 'dev':657 'develop':35,44,48,225,228,331,347,420,427,456,1637 'developers.cloudflare.com':2846 'developers.cloudflare.com/dns/manage-dns-records/reference/proxied-dns-records/':2845 'devic':2628 'dialog':639 'digest':1948,1951,1954 'dir':580 'direct':949 'disabl':1614,1820,2503 'disconnect':2478 'dns':539,2338,2772,2843 'docs.lemonsqueezy.com':2840 'docs.lemonsqueezy.com/api/webhooks':2839 'document':391,2607,2625 'document.addeventlistener':2092 'document.removeeventlistener':2098 'document.visibilitystate':2089 'doesn':957 'domain':536,1622,1810,1830,1833,2325,2333,2366,2483,2489,2495,2757,2761,2767,2797 'download':1555,1597 'downloadfromcloud':1564 'draft':2581 'dropdown':1141,1167 'dual':1211,1885 'dual-writ':1210 'e.g':2328 'earli':373,775 'edg':2715,2806 'editor':2568 'element':1138 'email':510,769,1331,1469,1613,1616,1801,1818,1821,1976,1978,2011,2024,2025 'emb':1039 'en':2200,2204 'en.json':866 'enabl':1433,2447 'encount':250 'end':367,880,882,1475 'end-to-end':879 'endpoint':797,2459 'enforc':522,2106 'enhanc':289 'env':2437 'env.local.example':677 'environ':674,2443,2675,2683 'eq':1578,2023,2038 'error':252,375,1737,1742,1752,2473,2728 'eslint':576 'establish':774 'et':2861 'etc':1378 'even':1877 'event':1963,1969 'everi':361,1601,2067,2307 'everyon':2786 'everyth':350,2656 'evid':2874 'exampl':2522,2523,2659,2733 'execut':1493 'experi':67 'expir':1345,2035 'expiresat':2002 'expiresat.getmonth':2006 'expiresat.setmonth':2005 'expiresat.toisostring':2037 'export':926,1122,1230,1258,1377,1714,1766,1918,2195,2700 'expos':728 'extern':2052 'eyjxxx':718,723 'fail':368,1007,1880,2666 'fallback':1070,1082,1085 'fals':2206 'fast':369,371 'featur':479,603,787,818,820,873 'feature-specif':819 'fetchplaninfo':2063 'file':219,771,2214,2321,2750 'find':2008 'first':261,473,670,1088,2278 'fix':370,1164,1854,2379 'flexibl':2356 'float':1137 'flow':268,482,655,878,2857 'follow':2723 'forc':1190 'foreach':1583 'forget':2787 'free':1336,1340,1806,2124,2145,2318,2635,2747 'frontend':1098,2045,2109 'full':31,40,417,445,1157,1288,2311,2353,2492,2780 'full-screen':1156 'full-stack':30,39,416 'fullstack':3 'fullstack-webapp':2 'function':928,1054,1066,1231,1260,1457,1494,1511,1563,1716,1768,1920,2385 'futur':403 'future-you':402 'generat':2864 'generic':1384 'geo':549,2381 'geo-detect':548,2380 'get':264,1717 'getal':1271,1780 'getcountri':2386,2415 'gif':2510 'git':472,659,661,664,2288,2290,2294 'github':1631,1636,1685,1708,2621 'github.com':1690 'github.com/settings/developers':1689 'give':1019,2310 'glass':2595 'glass-morph':2594 'global':844,860 'googl':1623,1628,1655,1684,2620 'graph':654 'green':2368 'guardrail':2867 'guid':47 'hacker':231 'handl':1458,1495,2208,2474 'handleinputchang':982 'handlesubmit':983 'hang':1849 'happen':2050 'harm':2869 'haslocaleprefix':2414 'header':1170,1181 'help':426 'hex':1952 'hidden':1172 'histori':1519,2519 'hmac':1935,1941 'hmac.update':1949 'home':784,2239,2240 'hook':974,2577 'html':1042,1129 'human':12,82 'hunt':559 'i18n':194,274,609,779,846,861,888,2185 'icp':218,2320,2749 'id':998,1001,1315,1321,1362,1367,1371,1397,1401,1420,1447,1468,1541,1580,1625,1633,1680,1704,1986,1989,1998,2022,2039,2178 'idea':183,279,448 'ignor':2370 'immedi':1869 'import':899,903,975,1226,1250,1254,1291,1914,2188 'includ':2487,2794 'independ':227,311 'index':1132,1198 'indi':230 'infin':2150,2152 'infinit':2357 'info':1311 'infrastructur':287 'init':632,660,667 'initi':470,563 'input':640,981 'insert':1465,1487,2176 'instal':587,592,605,615,622,651,2548 'instruct':412 'integr':92,192,994,2289,2291 'intellig':22 'intent':2879 'interact':497 'intern':1890,1896 'internation':96,525,2184 'interv':2071,2097 'intl':528,608,2193,2828 'invest':285 'ip':2400 'ipcountri':2394 'isload':984 'isol':1144,1163,1177 'issu':254,1844 'item':1984 'iter':376 'ja':2202 'ja.json':868 'javascript':1120 'journey':446 'json':1037,2237 'json.parse':1080,1548,1964 'json.stringify':1063,1591 'jsonb':1409 'k':17 'key':682,688,717,722,916,1026,1056,1062,1068,1077,1126,1243,1318,1365,1403,1418,1422,1516,1526,1528,1532,1544,1545,1575,1585,1590,2217,2465 'kv':1385 'land':783,814,815,2511 'languag':531,613,1476,2219 'later':263,297,1104 'latest':570,631 'latter':1003 'launch':104,553,2430 'layer':309,1133 'layout':2599 'layout.tsx':789 'lead':114 'learn':13,1521,2870 'learning-data':1520 'left':2570 'lemonsqueezi':238,754,1895,1910,2634,2837 'lesson':995 'let':2534 'level':1435,2104 'lib':823 'librari':634 'lifecycl':52 'lifelong':11 'like':177,2709 'limit':1357,2142,2144,2160,2166,2457 'link':2293 'link/redirect':858 'load':853 'loaddata':1067 'local':388,778,848,850,856,1867,2199,2277,2435,2664 'locale-awar':855 'localedetect':2205 'localhost':1653 'localstorag':292,487,502,1052,1087,1207,1391,1503,1557,2583 'localstorage.getitem':1076,1531 'localstorage.setitem':1061,1588 'log':1361,2170,2175,2245,2248,2612,2719,2722 'login':810,1596,2244,2585 'logout':1598,2247 'ls':740,748 'm':222,666 'main':786 'make':121,211,409,436,2685 'manual':1908,2210 'map':1987 'may':1848,2712 'media':557 'medium':2222 'mention':162 'messag':852,865,933,960,980,1376,2181,2260 'meta':1967 'meta.event':1971 'method':1893 'methodolog':259 'micro':496 'micro-interact':495 'middlewar':1753,2212,2383,2405 'middleware.ts':833,859 'migrat':1205 'min':1603,2363 'minut':2560,2752 'mission':423 'mobil':1152,2470 'modal':1150,1159 'mode':1038 'model':685,839,934,940,942,990,1022,1028,2126,2479 'models.ts':837 'modern':59 'monet':57,451 'monitor':374 'month':2254,2255 'morphism':2596 'multi':530,612 'multi-languag':529,611 'multipl':2299 'must':944 'mvp':283,379,477,871,1051 'my-app':571,582 'name':1276,1280,1785,1789,1972,2343 'namespac':2235 'nav':790,2238,2597 'navig':443 'navigation.ts':854 'need':305,405,590,649,703,734,1091,2755,2878 'network':1843,1852,1872,1879,2477,2502,2807 'never':348,458,727,2107,2466 'new':1187,1459,1474,1496,1552,1692,1723,1957,2003,2042,2133,2163,2488,2498,2796 'new.email':1472 'new.id':1471 'next':527,568,607,617,708,713,738,746,758,766,2192,2274,2432,2468,2546,2827 'next-intl':526,606,2191,2826 'next-intl.dev':2830 'next-intl.dev/docs':2829 'next-them':616 'next.js':70,154 'next/headers':1257 'nextrequest':1771,2388 'nextrespons':1773 'nextresponse.redirect':1744,1749,2425 'node':653 'node.js':2707 'nodej':2703 'npm':591,604,614,621,650 'npx':565,629,668,2273,2279,2431,2543 'null':1375,1406,1411 'oauth':509,799,1656,1665,1686,1693 'offer':1027 'ok':2044 'old':2497 'onboard':1161 'one':336,480,681,876,1024,2566 'onvis':2087,2094,2100 'openai':686,694,911,948,950,1000 'openai.chatmodel':941,997 'openrout':205,680,925,956,1009,1016,1018,2832 'openrouter.ai':698,2835 'openrouter.ai/api/v1':697 'openrouter.ai/docs':2834 'optim':329 'option':1278,1282,1787,1791 'orang':2349 'order':457,1194,1973,1983,2673 'organ':2236 'origin':1722,1745,1750 'other':320 'output':1035 'overlay':1151 'page':788,816,1174,1762,2085,2367,2512 'page.tsx':782 'panel':2572 'parent':1146,1183 'pars':1045 'pass':2434 'pathnam':2423 'pattern':1128,1500 'pay':1906 'payment':94,191,256,273,486,516,731,802,886,1883,1886,2049,2084,2587 'paypal':1899 'per':2218 'perfect':119,387 'period':1509 'persist':501,1203 'person':337 'phase':345,354,362,454,460,462,467,475,488,498,506,514,523,532,540,551,560,705,736,869,1109,1200,1208,1213,1217,1605,1881,2182,2263,2539,2541,2564,2590,2602,2616,2632,2651 'phase-bas':344,453 'pitfal':441 'placehold':2257 'plan':521,841,1310,1333,1338,1344,1991,1993,2013,2033,2034,2046,2105,2116,2121,2123,2131,2161,2253,2647 'plpgsql':1477 'plugin':1124 'pnas':2871 'polici':1438,2446 'polish':262,491,1112 'poll':2066 'port':1840 'post':929,1921 'postcss':1117 'postcss.config.mjs':1121 'power':7 'practic':132 'pragmat':112,321 'prefix':781 'preview':2694 'price':1160,2250 'primari':1216,1317,1364,1417 'principl':874 'priorit':116 'pro':749,1342,1996,1999,2149,2639 'problem':258 'process':2119 'process.env.lemonsqueezy':1944 'process.env.ls':1995 'process.env.next':1235,1239 'process.env.openai':914,918 'prod':2281 'produc':355,463 'product':74,78,127,170,178,358,430,466,558,896,2284,2427,2442,2691,2877 'profil':813,1308,1314,1370,1400,1432,1440,1442,1451,1467,2016,2020,2028,2031,2122 'profile.id':2040 'progress':288 'project':85,89,249,469,562 'promise.race':1860 'promot':554,2506 'prompt':964 'prototyp':389 'proven':325 'provid':792,812,1612,1683,1707,2230 'proxi':2347,2371,2746,2775,2844 'public':709,714,739,747,759,767,1236,1240,2469 'pure':1097 'puriti':335 'push':2308 'rate':1356 're':2688 'reach':2167 'react':2856 'reactflow.dev':2859 'readi':2658 'readm':555,2507,2521 'real':65,429 'real-world':64 'reason':407 'record':392,2340 'redirect':1645,1672,2358,2408,2485,2791 'refer':1319,1369,1399,2808 'refresh':835,1755,1763,1795,2047,2062,2073,2076,2091,2364 'region':542,1892 'registr':1093 'remov':317 'replac':1390,1456 'req':2387 'req.headers.get':2391,2396 'request':930,931,1718,1719,1770,1922,1923,2416 'request.cookies.getall':1781 'request.headers.get':1930 'request.json':936 'request.nexturl.clone':2420 'request.text':1927 'request.ts':851 'request.url':1725 'requestedmodel':2130 'requir':1014,2136,2292 'resend':1803,1805,1835 'respons':493,953,1005,1772,1800,1958,2043,2134,2164,2471,2598 'response.cookies.set':1788 'restrict':842 'result':938 'result.todatastreamresponse':967 'return':966,1078,1084,1233,1266,1461,1473,1743,1748,1799,1956,2041,2060,2082,2095,2132,2162,2390,2424 'right':2574 'rls':1423,2445 'role':721,1284,2464 'rout':780,862,1249,1711,2114,2197,2452,2697 'routing.ts':847 'row':1434,1492 'run':2720 'runtim':2698,2702,2716 'saa':73,165,1305 'save':2624 'savedata':1055 'say':171,220 'scaffold':90,471 'schema':1303 'scienc':26 'scratch':147 'screen':1158,2567 'scroll':645 'scroll-area':644 'sdk':208,596,600,908,2552,2811 'sdk.vercel.ai':2814 'sdk.vercel.ai/docs':2813 'searchparam':1721 'searchparams.get':1728 'second':1864,2069 'secret':756,1626,1634,1681,1705,1946 'section':817 'secur':1436,1478 'see':2725 'select':1573,2021 'selectedmodel':991 'send':1813,2261,2262 'server':725,830,1244,1246 'server-on':724 'server.ts':829 'servic':720,1283,1661,1802,2463 'servicerolekey':1301 'session':834,864,1754,1797 'set':1523,1638,2332,2439,2682,2689,2766 'setal':1273,1782 'setinterv':2072 'setup':195,658,1096,1116,1610,1657,1687,2753 'sha256':1943 'shadcn':630 'shadcn/ui':806,2848 'ship':68,117,260,338,428,2536 'side':971,1048 'sidebar':1153 'sign':1507,1560 'sign-in':1559 'sign-out':1506 'signatur':1929,1933,1936,1955,2461 'signup':1453 'singl':2026 'site':213,1642,2053,2742 'sk':690 'sk-or-v1-xxxx':689 'skill':8,15,83,87,136 'skill-04-fullstack-webapp' 'skip':459 'smtp':1621,1836 'smtp.resend.com':1839 'social':21,556 'solo':224 'someth':176 'source-24kchengye' 'specif':821 'sql':1306 'src':579,776 'src-dir':578 'src/app/api/auth/callback/route.ts':1713 'src/app/api/chat/route.ts':898 'src/app/api/webhooks/lemonsqueezy/route.ts':1913 'src/i18n/routing.ts':2187 'src/lib/supabase/middleware.ts':1765 'ssl':2491 'ssl/tls':2352,2779 'ssr':2822 'stack':32,41,418,1188 'star':2518 'start':290,298 'state':1868 'status':1960,2137,2168 'step':2763,2785 'stop':985 'storag':1050 'store':1386,2580 'strategi':105,1206 'stream':796,2579 'streamtext':900,939 'strict':2354,2493,2781 'string':1057,1069,1514,1566,1981,2389 'stripe':239 'structur':772,1034,1108 'subscript':518 'subscription/payment':236 'supabas':190,294,503,512,700,710,715,719,824,1204,1215,1220,1237,1241,1504,1556,1608,1682,1706,1733,1775,1824,1838,1846,2444,2462,2484,2605,2618,2790,2816,2821 'supabase.auth.exchangecodeforsession':1739 'supabase.auth.getsession':1847 'supabase.auth.getuser':1793 'supabase.com':2819,2824 'supabase.com/docs/guides/auth':2818 'supabase.com/docs/guides/auth/server-side':2823 'supabase.from':1536,1570,2173 'supabase/ssr':624,1229,1253 'supabase/supabase-js':623,1296 'supabaseadmin':1298,2018 'supabaseadmin.from':2030 'support':959 'sure':2686 'switch':1029 'sync':296,505,1103,1389,1499,1594,2610 'system':95,237,517,961,963,1884 'tab':642,2078 'tabl':1313,1359,1393,1431,2450,2608 'tailwind':575,1113,2850 'tailwind.config.js':1119 'tailwindcss.com':2854 'tailwindcss.com/docs':2853 'tailwindcss/postcss':1125 'target':2345 'teach':382 'team':341 'tech':113 'technic':243 'technolog':326,397 'templat':676 'terminolog':2234 'test':2472,2499 'text':1326,1330,1332,1334,1373,1404 'theme':492,618,619,2593 'theoret':130,334 'three':1219 'tier':519,1807 'time':1595 'timeout':1853,1865,2476 'timestamptz':1347,1350,1381,1414 'titl':2251 'toast':643 'today':2540 'todayusag':2154,2159 'toggl':620 'toisostr':1554 'tool':167,2532 'topic-agent-skills' 'topic-ai-education' 'topic-ai-learning' 'topic-ai-tutor' 'topic-chatgpt' 'topic-claude-code' 'topic-claude-skills' 'topic-cognitive-science' 'topic-copilot' 'topic-cursor' 'topic-deepseek' 'topic-education' 'touppercas':2403 'track':1354 'tradeoff':128,398 'translat':2213,2229 'tree':9,84 'tri':1060,1073 'trigger':133,1462,1481,2302 'trust':2108 'type':2258,2341 'typescript':574,897,968,1053,1222,1501,1712,1764,1912,2054,2111,2186,2384 'typic':1304 'ui':627,805,1127,1875 'ui.shadcn.com':2849 'ui/ux':490,1111 'unauthor':1959 'unavail':2480 'uniqu':1327 'unknown':1059 'unless':1617 'unlimit':2642 'updat':1412,1550,1876,2012,2032,2789 'updatesess':836,1769 'upgrad':2135,2252,2646 'upload':1502,1599,1604 'uploadtocloud':1512 'upsert':1539 'uri':1673 'url':696,711,761,920,1238,1268,1300,1329,1640,1643,1646,1697,1724,1777,2419,2426,2486,2792 'url.pathname':2421 'usag':1353,1360,2117,2141,2171,2174,2456 'use':945,972,1004,1445,2224,2575,2706 'usechat':973,976,986,2576 'useeffect':2057 'user':139,267,306,481,877,1031,1307,1366,1394,1396,1419,1424,1460,1484,1497,1537,1540,1571,1579,2009,2059,2081,2101,2177,2410,2622,2735,2801 'user.email':2065 'user.id':2064 'userid':1513,1542,1565,1581,2157,2179 'usernam':1325 'uuid':1316,1368,1398 'v':1075,1079,1081 'v1':692 'v4':1115,2852 'v6':2812 'valid':277 'valu':1277,1281,1408,1470,1547,1577,1587,1593,1786,1790 'var':2438 'variabl':675,2676,2684 'variant':1985,1988,1997 'variantid':1980,1994 'veloc':332 'vercel':189,206,537,598,669,2269,2280,2331,2365,2399,2402,2441,2558,2668,2680,2711,2718,2721,2765,2809 'verif':1909 'verifi':1934,2276,2453,2460 'via':101,511,2803 'visibilitychang':2093,2099 'visibl':2080,2090 'visitor':2516 'visual':647 'vpn':2504 'wait':2360,2654 'want':140,173,196,232,2526 'war':1199 'warn':2373 'web':33,42,60,144,169,203,248,419,1668 'webapp':4 'webhook':257,520,755,801,803,1286,1900,1911,1945,2458,2643,2838 'wechat':1905 'week':81 'without':217,299,318,1036,1105,1195,1757,1808,2866 'work':269,282,328,2663,2762 'workaround':1819 'world':66 'wrap':1855 'write':433,1212,2531 'x':1932,2398 'x-signatur':1931 'x-vercel-ip-countri':2397 'xxx':757 'xxx.lemonsqueezy.com':744,752 'xxx.lemonsqueezy.com/buy/xxx':743,751 'xxx.supabase.co':712,1675,1699 'xxx.supabase.co/auth/v1/callback':1674,1698 'xxxx':693 'xyflow/react':652,2858 'your-custom-domain.com':1651 'your-custom-domain.com/**':1650 'your-domain.com':1644,1648 'your-domain.com/**':1647 'your@email.com':770 'yourdomain.com':2329 'z':1131,1135,1139,1148,1154,1193,1197 'z-index':1130,1196 'z-order':1192 'zero':76 'zh':2201 'zh.json':867 '人类养成记':27 '爱发电':240,1902 '驱动的终身学习技能树':29","prices":[{"id":"243f9df6-6bcf-493c-a3f9-6e8cbb904aa5","listingId":"9098f02c-19f3-4bba-a2e4-872b79d0efb7","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"24kchengYe","category":"human-skill-tree","install_from":"skills.sh"},"createdAt":"2026-04-18T21:57:52.584Z"}],"sources":[{"listingId":"9098f02c-19f3-4bba-a2e4-872b79d0efb7","source":"github","sourceId":"24kchengYe/human-skill-tree/04-fullstack-webapp","sourceUrl":"https://github.com/24kchengYe/human-skill-tree/tree/master/skills/04-fullstack-webapp","isPrimary":false,"firstSeenAt":"2026-04-18T21:57:52.584Z","lastSeenAt":"2026-05-02T18:53:25.032Z"}],"details":{"listingId":"9098f02c-19f3-4bba-a2e4-872b79d0efb7","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"24kchengYe","slug":"04-fullstack-webapp","github":{"repo":"24kchengYe/human-skill-tree","stars":515,"topics":["agent-skills","ai-education","ai-learning","ai-tutor","chatgpt","claude-code","claude-skills","cognitive-science","copilot","cursor","deepseek","education","gemini","k12","learning-science","lifelong-learning","mcp-server","skill-tree","social-intelligence","spaced-repetition"],"license":"other","html_url":"https://github.com/24kchengYe/human-skill-tree","pushed_at":"2026-03-25T05:22:57Z","description":"🌳 AI-Powered Skill Tree for Lifelong Human Learning. 30+ skills from K-12 to career & social intelligence, built on cognitive science. | 人类养成记：AI 驱动的终身学习技能树","skill_md_sha":"3ce75f610416b6730a84f52a048c9eba78ca3e5b","skill_md_path":"skills/04-fullstack-webapp/SKILL.md","default_branch":"master","skill_tree_url":"https://github.com/24kchengYe/human-skill-tree/tree/master/skills/04-fullstack-webapp"},"layout":"multi","source":"github","category":"human-skill-tree","frontmatter":{},"skills_sh_url":"https://skills.sh/24kchengYe/human-skill-tree/04-fullstack-webapp"},"updatedAt":"2026-05-02T18:53:25.032Z"}}