Mình đã build một boilerplate Next.js 16 tích hợp AI agent thế nào
TL;DR: Một starter cho web app hiện đại: Next.js 16 (App Router + Turbopack), React 19, Supabase Auth/DB, Tailwind v4 với design tokens, i18n EN+VI, login 2-step chuẩn Apple, atoms/molecules/organisms tách lớp rõ ràng. Điểm khác: AI-first — toàn bộ tài liệu được viết để Claude Code / Cursor / Copilot đọc và làm theo. Mục tiêu: clone về, đăng nhập Supabase, gõ feature là chạy được — không tốn 2 ngày setup boilerplate.
Bài này dài. Mình chia thành các phần để bạn nhảy đến phần cần:
- Tại sao lại thêm một boilerplate nữa?
- Tech stack & lý do chọn từng cái
- Cấu trúc dự án — atoms / molecules / organisms
- AI-first: AGENTS.md, docs/, auto-memory
- Design system với
getdesign+ Tailwind v4@theme - Login chuẩn Apple: 2-step + email verify + middleware proxy
- i18n EN + VI auto-detect từ
Accept-Language - Database & migrations + RLS
run.sh— single entry point cho mọi task- Setup trong 5 phút
- Bài học sau nhiều năm
1. Tại sao lại thêm một boilerplate nữa?
Mỗi lần start project mới mình lặp lại y nguyên:
- Pin Next.js version, set port khác 3000 (vì 3000 luôn busy)
- Setup ESLint, Tailwind, postcss
- Setup Supabase client (browser / server / middleware)
- Viết middleware refresh session + protect route
- Viết login + signup (mà thường UI cẩu thả vì "có cái form là được")
- Tạo
todostable với RLS, viết migration - Set Site URL + Redirect URLs trên Supabase Dashboard
- i18n: hoặc bỏ qua, hoặc cài next-intl xong cấu hình 1 buổi sáng
- README sơ sài → 2 tuần sau quên cách chạy
- 6 tháng sau onboard người mới → onboarding 3 ngày
Boilerplate này gói tất cả lại. Khác biệt lớn nhất: toàn bộ tài liệu được viết để AI agent đọc và làm theo. Khi bạn mở Claude Code / Cursor lên, AI biết:
- Đây là Next.js 16 (không phải 13/14 — APIs đã đổi)
- File phải đặt ở đâu, atoms/molecules/organisms như thế nào
- Đọc
node_modules/next/dist/docs/01-app/trước khi code (training data có thể stale) - Không bypass commit hook, eslint, typecheck
- Khi cần new design token, edit ở đâu, sync với Tailwind ra sao
Kết quả: AI làm việc đúng convention từ prompt đầu tiên thay vì phải "huấn luyện" mỗi session.
2. Tech stack & lý do chọn từng cái
| Phần | Tech | Lý do |
|---|---|---|
| Framework | Next.js 16.2.6 (App Router + Turbopack) | App Router ổn định, Turbopack build nhanh, Server Actions cho auth/mutation cực sạch |
| UI | React 19.2.4 | useActionState, async params / searchParams — clean code không cần thư viện ngoài |
| Language | TypeScript 5 strict | Bắt sớm lỗi runtime, autocomplete tốt hơn cho design tokens |
| Styling | Tailwind CSS 4 | Config trong CSS qua @theme, không còn tailwind.config.js. Pair với design tokens cực ngọt |
| Backend | Supabase (Auth + Postgres + RLS) | Free tier đủ dùng, RLS native, JS client tốt, không cần dựng API server riêng |
| Auth | @supabase/ssr 0.10+ |
Async createClient, cookie-based session, middleware refresh |
| Package manager | Yarn | yarn.lock ổn định cross-machine; không pha trộn npm/pnpm |
| Lint | ESLint 9 flat config + eslint-config-next |
Đúng chuẩn Next 16 |
Mình không add thêm các package "tiện" như clsx, class-variance-authority, next-intl — vì:
clsx: với JSX nhỏ, template string${a} ${b}đủ dùngcva: variant pattern viết tay bằngRecord<Variant, string>rõ ràng hơn, không che giấunext-intl: với ~30 strings, custom dictionary 60 dòng đủ; tránh phụ thuộc 50KB+
Triết lý: ưu tiên đơn giản, hiểu được toàn bộ code, ít magic.
3. Cấu trúc dự án — atoms / molecules / organisms
Phỏng theo Atomic Design (Brad Frost) — đã dùng đi dùng lại nhiều năm ở các dự án thực tế:
ad-manager/
├── app/ # Next.js App Router
│ ├── login/ # /login route + Server Actions
│ │ ├── actions.ts # signIn / signUp / signOut
│ │ └── page.tsx
│ ├── signup/ # /signup route
│ ├── auth/callback/ # Supabase email-confirmation callback
│ │ └── route.ts
│ ├── layout.tsx
│ ├── page.tsx # / (home, protected)
│ └── globals.css # Tailwind v4 @theme tokens
├── components/ # UI building blocks
│ ├── atoms/ # Button, Input, Label, ErrorMessage, TextLink, LocaleSwitcher
│ ├── molecules/ # FormField (Label + Input + ErrorMessage)
│ ├── organisms/ # LoginForm, SignUpForm, SignOutButton
│ └── templates/ # AuthLayout (centered auth page wrapper)
├── lib/i18n/ # Cusom i18n (en + vi)
│ ├── dictionaries.ts
│ ├── server.ts # getLocale (cookie + Accept-Language), getMessages, t
│ ├── client.tsx # IntlProvider + useT
│ └── actions.ts # setLocale Server Action
├── utils/supabase/ # Supabase client helpers
│ ├── client.ts # createBrowserClient
│ ├── server.ts # createClient (async)
│ └── middleware.ts # updateSession + route protection
├── proxy.ts # Next 16 renamed middleware → proxy
├── supabase/
│ ├── migrations/ # SQL migrations
│ └── seed.sql
├── docs/ # Tài liệu cho AI agent / contributor
├── DESIGN.md # Design tokens (managed by `getdesign` CLI)
├── AGENTS.md # AI agent rules entry point
├── CLAUDE.md # → @AGENTS.md
├── README.md # Setup hướng dẫn người mới
├── RUN.md # Hướng dẫn run.sh
└── run.sh # Single entry point
Quy tắc atoms / molecules / organisms:
| Layer | Mục đích | Ví dụ |
|---|---|---|
| atoms | Primitive nhỏ nhất, không compose component khác. Không business logic. | Button, Input, Label |
| molecules | Combine vài atoms. Tái sử dụng được. | FormField = Label + Input + ErrorMessage |
| organisms | Block đặc thù feature. Có thể wire Server Action, hold local state. | LoginForm, SignUpForm |
| templates | Layout wrapper dùng nhiều page. | AuthLayout |
| pages | Route-level (app/<segment>/page.tsx). Mỏng — orchestrate organisms. |
app/login/page.tsx |
Hướng phụ thuộc một chiều: atom → molecule → organism → page. Atom không import molecule, molecule không import organism, v.v. Quy tắc này tránh circular dependency và làm cấu trúc thư mục "nói chuyện" thay cho code.
4. AI-first: AGENTS.md, docs/, auto-memory
Phần khác biệt nhất của boilerplate.
4.1 File entry point cho AI
CLAUDE.md → @AGENTS.md # Claude Code đọc đầu tiên
AGENTS.md → cross-tool # Cursor / Copilot / Codex cũng đọc
AGENTS.md mở đầu bằng cảnh báo:
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure
may all differ from your training data. Read the relevant guide in
`node_modules/next/dist/docs/` before writing any code.
Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
Vì sao quan trọng? Next.js 16 đổi tên middleware.ts → proxy.ts, params/searchParams thành async, một số API bị xóa. Training data của AI thường chưa cập nhật. Bắt AI đọc docs bundle trong node_modules/next/dist/docs/01-app/ trước khi code = đụng đúng API hiện hành.
4.2 docs/ structure mirror acm-web
docs/
├── README.md # Doc index + required reading order
├── ai-agent-guidelines.md # Do/Don't
├── operation.md # No-bypass rules (commit-hook, eslint, type-check, release)
├── project-overview.md
├── technology.md
└── core-principles-and-coding-standards/
├── structure.md # Component hierarchy + folder map
├── coding-conventions.md # Routing, components, fetching, i18n, forms
├── coding-style.md # yarn, eslint, commands
└── instructions-and-work-flows/
└── adding-a-new-page.md # Step-by-step workflow
docs/README.md định nghĩa required reading order — bắt buộc AI đọc tuần tự trước khi đề xuất giải pháp:
1. Start → docs/README.md, project-overview.md
2. Guidelines → ai-agent-guidelines.md, AGENTS.md
3. Structure & tech → technology.md, structure.md
4. Patterns → coding-conventions.md, coding-style.md, DESIGN.md
5. Framework docs → node_modules/next/dist/docs/01-app/<relevant page>
6. Existing code → similar files under app/
4.3 No-bypass rules (operation.md)
| Rule | Nghĩa |
|---|---|
no-bypass-commit-hook |
Không git commit --no-verify |
no-bypass-eslint |
Fix lỗi thay vì disable rule |
no-bypass-type-check |
yarn build phải pass; không any / @ts-ignore lách |
no-auto-release |
Không tự deploy/publish |
AI đọc xong sẽ refuse "fix nhanh" bằng cách bypass — buộc fix gốc.
4.4 Auto-memory cho Claude Code
Claude Code có hệ thống auto-memory (~/.claude/projects/<project>/memory/). Boilerplate này tận dụng để lưu:
- Feedback (vd: "không tạo skill nếu AGENTS.md đã enforce rule")
- Project basics (port 3002, docs layout, getdesign CLI behavior)
- Reference (external CLIs, dashboards, knowledge gaps)
Các session sau Claude tự load → không cần re-explain context.
4.5 Triết lý "no redundant skills"
Bài học đắt giá: ban đầu mình tạo project-level skill nextjs trong .claude/skills/. Người dùng phản hồi: "AGENTS.md đã có rule này rồi, skill này thừa". Đúng — AGENTS.md được load mọi session, skill là thêm context bị overlap. Rút ra rule: nếu AGENTS.md / docs đã enforce, không wrap thành skill.
5. Design system với getdesign + Tailwind v4 @theme
5.1 DESIGN.md quản lý bởi getdesign CLI
npx getdesign@latest add apple
Tool tạo DESIGN.md ở project root với:
- Color tokens (Action Blue, Ink, Canvas, surface tile, …)
- Typography (
hero-display56px / 600 / -0.28px,body17px / 400, …) - Spacing scale (
xxs4 →section80) - Border radius (
xs5 →pill9999) - Component spec (button-primary, product-tile, …)
- Do's & Don'ts
5.2 Sync sang Tailwind v4 @theme
Tailwind v4 không còn tailwind.config.js. Config nằm trong CSS:
/* app/globals.css */
@import "tailwindcss";
@theme {
/* Brand */
--color-primary: #0066cc;
--color-primary-focus: #0071e3;
/* Surfaces */
--color-canvas: #ffffff;
--color-canvas-parchment: #f5f5f7;
--color-ink: #1d1d1f;
/* Font families */
--font-display: "SF Pro Display", system-ui, -apple-system, "Inter", sans-serif;
--font-text: "SF Pro Text", system-ui, -apple-system, "Inter", sans-serif;
/* Spacing tokens */
--spacing-xxs: 4px;
--spacing-section: 80px;
/* Border radius */
--radius-md: 11px;
--radius-lg: 18px;
--radius-pill: 9999px;
/* Typography composite — sets size + line-height + tracking + weight + family */
--text-hero-display: 56px;
--text-hero-display--line-height: 1.07;
--text-hero-display--letter-spacing: -0.28px;
--text-hero-display--font-weight: 600;
--text-hero-display--font-family: var(--font-display);
/* Product shadow — exactly ONE drop-shadow per Apple convention */
--shadow-product: 3px 5px 30px 0 rgba(0, 0, 0, 0.22);
}
Tailwind v4 tự sinh utility cho mỗi token:
| DESIGN.md key | Tailwind utility |
|---|---|
colors.primary |
bg-primary, text-primary |
spacing.section |
p-section, gap-section |
rounded.md |
rounded-md |
typography.hero-display |
text-hero-display (1 class set hết size + weight + line-height + tracking + family) |
| Product shadow | shadow-product |
Code component cuối:
<button className="bg-primary text-on-primary text-body rounded-pill px-[22px] py-[11px] active:scale-95">
Buy
</button>
5.3 Button variants tách lớp
Apple dùng nhiều radius khác nhau — boilerplate có 5 variants atom Button:
type Variant = "primary" | "primary-rect" | "secondary" | "dark-utility" | "pearl";
| variant | radius | text | use case |
|---|---|---|---|
primary |
pill | body 17px | Marketing CTAs ("Buy", "Learn more") |
primary-rect |
md (11px) | button-large 18px | Auth surfaces (Continue / Sign in) |
secondary |
pill | body 17px | Ghost outlined primary |
dark-utility |
md (11px) | utility 14px | Sign Out, nav actions |
pearl |
md (11px) | caption 14px | Card secondary action |
6. Login chuẩn Apple: 2-step + email verify + middleware proxy
6.1 Two-step UX
Apple sign-in nổi tiếng vì chia thành 2 step: email trước, password sau. Boilerplate copy y nguyên pattern này.
// components/organisms/LoginForm.tsx (rút gọn)
"use client";
export function LoginForm({ next }: { next?: string }) {
const [state, formAction, pending] = useActionState(signIn, {});
const [step, setStep] = useState<"email" | "password">("email");
const [email, setEmail] = useState("");
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
if (step === "email") {
e.preventDefault();
if (isValidEmail(email)) setStep("password");
}
// step === "password" → Server Action run natively
};
return (
<form action={formAction} onSubmit={handleSubmit}>
{step === "email" ? (
<>
<FormField name="email" type="email" value={email} onChange={...} />
<Button type="submit" variant="primary-rect" disabled={!isValidEmail(email)}>
Continue
</Button>
</>
) : (
<>
<input type="hidden" name="email" value={email} />
<div>
<span>{email}</span>
<button type="button" onClick={() => setStep("email")}>Edit</button>
</div>
<FormField name="password" type="password" />
<Button type="submit" variant="primary-rect" disabled={pending}>
{pending ? "Signing in…" : "Sign in"}
</Button>
</>
)}
</form>
);
}
Một <form> duy nhất chia sẻ giữa 2 step → useActionState quản lý error/pending sạch. Step 1 onSubmit intercept → flip state. Step 2 không intercept → Server Action chạy. Enter key works tự nhiên ở cả 2 step.
6.2 Server Action signIn / signUp
// app/login/actions.ts
"use server";
export async function signIn(_prev: SignInState, formData: FormData): Promise<SignInState> {
const email = String(formData.get("email") ?? "").trim();
const password = String(formData.get("password") ?? "");
if (!email || !password) {
return { error: await tServer("auth.error_required") };
}
const supabase = await createClient();
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) return { error: error.message };
redirect("/");
}
export async function signUp(_prev: SignUpState, formData: FormData): Promise<SignUpState> {
// … validate
const origin = await siteOrigin();
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/callback`, // QUAN TRỌNG
},
});
// …
}
6.3 Email verify callback
Khi user click link confirm trong email, Supabase redirect về /auth/callback?code=.... Boilerplate có route handler exchange code → session:
// app/auth/callback/route.ts
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
if (!code) return NextResponse.redirect(`${origin}/login?error=missing_code`);
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (error) return NextResponse.redirect(`${origin}/login?error=${encodeURIComponent(error.message)}`);
return NextResponse.redirect(`${origin}/`);
}
Gotcha: Supabase Dashboard → Authentication → URL Configuration mặc định Site URL = http://localhost:3000. Bắt buộc đổi sang http://localhost:3002 (port của boilerplate) và thêm http://localhost:3002/auth/callback vào Redirect URLs allow list. Không là link email vẫn trỏ về port 3000 (sai).
6.4 Middleware (Next 16 gọi là Proxy) refresh session
Next.js 16 đổi tên middleware.ts → proxy.ts. Function proxy():
// proxy.ts
import { updateSession } from "@/utils/supabase/middleware";
export async function proxy(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg)$).*)"],
};
updateSession làm 3 việc:
- Tạo Supabase client với cookie adapter
- Gọi
supabase.auth.getUser()— bắt buộc để refresh expired access token - Redirect logic: chưa login + path không public →
/login?next=<original>; đã login + đang ở/loginhoặc/signup→/
7. i18n EN + VI auto-detect từ Accept-Language
Không cài next-intl (overkill cho ~30 strings). Cusom lib trong lib/i18n/:
// lib/i18n/dictionaries.ts
const en = {
"login.title": "Management",
"login.subtitle": "Sign in to your ad-manager account",
"login.continue": "Continue",
// …
};
const vi: Record<keyof typeof en, string> = {
"login.title": "Quản lý",
"login.subtitle": "Đăng nhập vào tài khoản ad-manager của bạn",
"login.continue": "Tiếp tục",
// …
};
export const dictionaries = { en, vi };
TypeScript guard: nếu vi thiếu key → compile fail. Không có cách nào ship locale thiếu key.
getLocale() resolve theo thứ tự:
export async function getLocale(): Promise<Locale> {
const cookieStore = await cookies();
const cookieValue = cookieStore.get("locale")?.value;
if (isLocale(cookieValue)) return cookieValue;
// Fallback: parse Accept-Language
const h = await headers();
const accept = h.get("accept-language") ?? "";
for (const lang of accept.toLowerCase().split(",")) {
if (lang.trim().startsWith("vi")) return "vi";
if (lang.trim().startsWith("en")) return "en";
}
return "en";
}
Server Components dùng t(messages, key); Client Components wrap trong <IntlProvider> rồi gọi useT(). LocaleSwitcher cố định góc dưới phải, click → setLocale Server Action set cookie + revalidatePath("/", "layout") để re-render toàn bộ với locale mới.
8. Database & migrations + RLS
8.1 Migration đầu tiên: bảng todos chuẩn RLS
-- supabase/migrations/0001_create_todos.sql
create table public.todos (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id) on delete cascade,
name text not null,
created_at timestamptz not null default now()
);
create index todos_user_id_idx on public.todos(user_id);
alter table public.todos enable row level security;
create policy "Users can read own todos"
on public.todos for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "Users can insert own todos"
on public.todos for insert
to authenticated
with check ((select auth.uid()) = user_id);
create policy "Users can update own todos"
on public.todos for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id); -- chống đổi user_id sang user khác
create policy "Users can delete own todos"
on public.todos for delete
to authenticated
using ((select auth.uid()) = user_id);
Pattern owner-only chuẩn Supabase:
- Enable RLS trên mọi table trong
public - Policy split theo từng operation (select / insert / update / delete)
UPDATEphải có cảUSINGvàWITH CHECK(không thì user có thể reassignuser_idsang user khác)to authenticated(khôngauth.role() = 'authenticated'— deprecated)
8.2 Apply migration
bash run.sh login # 1 lần / máy
bash run.sh link <project-ref> # 1 lần / repo
bash run.sh migrate # áp dụng pending migrations
9. run.sh — single entry point cho mọi task
bash run.sh <command>
App lifecycle:
start yarn dev
build yarn build
prod build + start
lint eslint
typecheck tsc --noEmit
verify lint + typecheck
Database (Supabase):
login supabase login (1 lần / máy)
link REF supabase link --project-ref REF (1 lần / repo)
migrate supabase db push
migration:new NAME
seed Apply supabase/seed.sql (psql nếu DATABASE_URL có, else Dashboard instructions)
Deployment:
deploy Placeholder (configure cho Vercel / Netlify / Docker)
help
Lợi:
- 1 cách invoke cho mọi task, không cần nhớ
yarn buildvssupabase db pushvspsql -f - CI / docs / onboarding đều reference cùng một command
- Logic phức tạp (seed auto-detect DATABASE_URL, link refuse nếu chưa login) gói gọn trong shell function
10. Setup trong 5 phút
# 1. Clone
git clone <repo>
cd ad-manager
# 2. Install
yarn install
# 3. Tạo .env.local
cat > .env.local <<EOF
NEXT_PUBLIC_SUPABASE_URL=https://<your-ref>.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=<your-anon-key>
NEXT_PUBLIC_SITE_URL=http://localhost:3002
EOF
# 4. Link + migrate
bash run.sh login # opens browser, generate token
bash run.sh link <your-project-ref>
bash run.sh migrate
# 5. Supabase Dashboard cần config:
# Authentication → URL Configuration:
# Site URL: http://localhost:3002
# Redirect URLs: http://localhost:3002/auth/callback
# 6. Tạo test user (Dashboard → Auth → Users → Add user, bật Auto Confirm)
# 7. Chạy
bash run.sh start
# Mở http://localhost:3002
11. Bài học sau nhiều năm
Một số nguyên tắc mình rút ra qua nhiều dự án — đều cài cứng vào boilerplate này:
11.1 Đừng fight với convention của tool
getdesign ghi DESIGN.md ở project root. Mình từng cố move vào docs/design.md cho gọn — lần update tool đè lại ở root, break references. Theo tool, đừng cãi. Boilerplate document hành vi này trong auto-memory để session AI sau không lặp.
11.2 AGENTS.md / docs/ > Skills
Skill nghe có vẻ chuyên nghiệp, nhưng AGENTS.md được load mặc định mỗi session, skill thì on-demand. Convention thuộc về docs/ để tự động phân phối; skill chỉ dành cho on-demand content (vd: HIG checklist khi review UI). Đừng wrap convention thành skill — overlap = noise.
11.3 DESIGN.md là single source of truth
Mọi token (color, spacing, typography, radius) bắt đầu ở DESIGN.md, sync sang globals.css (Tailwind v4 @theme). Không inline hex, không custom CSS class một-lần-dùng. Sự đồng nhất tại 1 chỗ → toàn app đồng nhất.
11.4 Bỏ tay khỏi next.config.ts cho mấy thứ không thuộc về nó
Port không phải config Next.js — không có option cho nó. Port set qua CLI flag (-p 3002) hoặc env PORT. Mỗi lần thấy AI add port vào next.config.ts mình biết training data đã stale → AGENTS.md đã ghi rõ điều này.
11.5 Two-step login UX > one-step
Apple-style chia email + password vào 2 step → giảm cảm giác "form dài", tăng conversion. Cost: 30 dòng React state. Đáng.
11.6 RLS từ ngày đầu, không phải sau
Mọi table public enable RLS ngay từ migration đầu tiên. Quên một bảng = data exposed qua REST API. Mặc định "deny everything" + add policy explicit > mặc định "allow" rồi siết sau.
11.7 Email verify cần URL Configuration đúng
99% lỗi "click link email không vào được app" đều do Site URL / Redirect URLs trên Supabase Dashboard sai. Document chỉ rõ trong README + giải thích tại sao trong bài này — đỡ tốn giờ debug.
11.8 i18n thì làm sớm
Bookmarking để "add sau" gần như không bao giờ xảy ra. 30 strings × t() lookup không tốn gì, tránh refactor sau khi có 500 strings hardcoded.
11.9 README + RUN.md cho người mới
6 tháng sau bạn cũng là người mới. README ngắn gọn cách setup. RUN.md sâu hơn về run.sh. AGENTS.md cho AI. docs/ cho contributor. Mỗi loại reader có entry point của họ.
11.10 No bypass
Bypass commit hook / eslint / typecheck = nợ kỹ thuật ngay tại commit đó. Fix tận gốc luôn — boilerplate document explicit rule (operation.md), AI bắt buộc tuân theo.
Kết luận
Mục tiêu của boilerplate này không phải "framework mới" — mà là kết tinh practice đã chạy production qua nhiều dự án thực, cộng thêm lớp tài liệu để AI agent làm việc đúng convention từ prompt đầu tiên.
Bạn clone về, làm theo 7 bước setup ở trên, là có:
- Login / signup chuẩn UX
- Email verify hoạt động
- DB với RLS chuẩn
- i18n EN + VI
- Design tokens trong Tailwind
- AI hiểu codebase, làm việc đúng convention
Phần feature business của app — bạn code. Phần boilerplate — đã xong.
Repo: https://github.com/dinhuty/nextjs-16-boilerplate-full-agents-design-apple
Câu hỏi / góp ý → comment dưới bài. Cảm ơn đã đọc đến cuối 🙏
All Rights Reserved