Skip to content
Back to skills

04 Fullstack Webapp

ASecurity

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. Th

  • 541 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 27, 2026
data-aijavascripttypescriptrustgojavabashsqlreactnextjsnode

Works with

  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned May 27, 2026

npx -y skills add 24kchengYe/human-skill-tree --skill 04-fullstack-webapp --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 04 Fullstack Webapp?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 04 Fullstack Webapp
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/24kchengye-04-fullstack-webapp/badge)](https://www.skillsdirectory.com/skills/24kchengye-04-fullstack-webapp)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
# 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: app | Target: cname.vercel-dns.com | Proxy: ON (orange)
4. Cloudflare: SSL/TLS → "Full (strict)" ← CRITICAL! Flexible = infinite redirects
5. Wait 1-2 min, refresh Vercel Domains page → green ✓
6. Ignore "Proxy Detected" warning, do NOT click "1-click fix"
```

**Geo-detection middleware:**
```typescript
function getCountry(req: NextRequest): string {
  return (
    req.headers.get("cf-ipcountry") ||        // Cloudflare
    req.headers.get("x-vercel-ip-country") ||  // Vercel
    ""
  ).toUpperCase();
}

// In middleware: auto-redirect Chinese users to /zh
if (!hasLocalePrefix && getCountry(request) === "CN") {
  const url = request.nextUrl.clone();
  url.pathname = `/zh${pathname}`;
  return NextResponse.redirect(url);
}
```

### Production Checklist

**Before launch:**
- [ ] `npx next build` passes locally
- [ ] All env vars set in Vercel (Production environment)
- [ ] Supabase RLS policies enabled on all tables
- [ ] API routes verify auth + check usage limits
- [ ] Webhook endpoints verify signatures
- [ ] `SUPABASE_SERVICE_ROLE_KEY` never in `NEXT_PUBLIC_*`
- [ ] Mobile responsive tested
- [ ] Error handling: API timeout, network disconnect, model unavailable

**After custom domain:**
- [ ] Supabase Redirect URLs include new domain
- [ ] Cloudflare SSL = Full (strict)
- [ ] Both domains accessible (old + new)
- [ ] Test from China network (disable VPN)

**Before promotion:**
- [ ] README has demo GIF
- [ ] Landing page has clear CTA
- [ ] Visitor badge + Star History in README

## Examples

### Example 1: "I want to build an AI writing tool"

**Coach**: Let's ship this in phases.

**Today (Phase 0):** `npx create-next-app`, install `ai @ai-sdk/openai`, deploy blank app to Vercel. 30 minutes.

**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.

**Day 3 (Phase 2):** Dark theme, glass-morphism nav, responsive layout.

**Day 4 (Phase 3):** Add Supabase. Create `documents` table. Cloud sync when logged in.

**Day 5 (Phase 4):** Supabase Auth (Google + GitHub). Users can save documents cross-device.

**Days 6-7 (Phase 5):** LemonSqueezy. Free: 10 AI calls/day. Pro $9.99/mo: unlimited. Webhook auto-upgrades plan.

Deploy after each phase. Don't wait until everything is "ready."

### Example 2: "My app works locally but fails on Vercel"

**Coach**: Check in this order:

1. **Environment variables** (90% of cases): Vercel Dashboard → Settings → Environment Variables. Make sure they're set for "Production" not just "Preview".

2. **API route runtime**: Add `export const runtime = "nodejs"` if you use Node.js APIs like `crypto`. Vercel may default to Edge runtime.

3. **Vercel logs**: Run `vercel logs <deployment-url> --follow` to see the actual error, not just the 500.

### Example 3: "Users in China can't access my site"

**Coach**: Cloudflare CNAME proxy — free, no ICP filing, 10-minute setup.

You need: a domain on Cloudflare (any domain works).

Steps:
1. Vercel: Settings → Domains → Add `app.yourdomain.com`
2. Cloudflare DNS: CNAME → `cname.vercel-dns.com`, Proxy ON
3. Cloudflare SSL/TLS: **Full (strict)** — this is the step everyone forgets
4. Update Supabase Redirect URLs to include the new domain

Cost: $0. China users access via Cloudflare's edge network.

## References

- Vercel AI SDK v6: https://sdk.vercel.ai/docs
- Supabase Auth: https://supabase.com/docs/guides/auth
- Supabase SSR: https://supabase.com/docs/guides/auth/server-side
- next-intl: https://next-intl.dev/docs
- OpenRouter API: https://openrouter.ai/docs
- LemonSqueezy Webhooks: https://docs.lemonsqueezy.com/api/webhooks
- Cloudflare DNS Proxy: https://developers.cloudflare.com/dns/manage-dns-records/reference/proxied-dns-records/
- shadcn/ui: https://ui.shadcn.com
- Tailwind CSS v4: https://tailwindcss.com/docs
- React Flow (@xyflow/react): https://reactflow.dev
- Bastani et al. (2025). Generative AI without guardrails can harm learning. *PNAS*, 122(26) — evidence that AI products need intentional design

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.