nextjs-hackathon-stack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/index.js +152 -0
- package/package.json +54 -0
- package/template/.cursor/agents/business-intelligence.md +38 -0
- package/template/.cursor/agents/code-reviewer.md +74 -0
- package/template/.cursor/agents/frontend.md +45 -0
- package/template/.cursor/agents/security-researcher.md +49 -0
- package/template/.cursor/agents/technical-lead.md +36 -0
- package/template/.cursor/agents/test-qa.md +85 -0
- package/template/.cursor/rules/ai.mdc +53 -0
- package/template/.cursor/rules/architecture.mdc +42 -0
- package/template/.cursor/rules/coding-standards.mdc +53 -0
- package/template/.cursor/rules/components.mdc +33 -0
- package/template/.cursor/rules/data-fetching.mdc +63 -0
- package/template/.cursor/rules/forms.mdc +59 -0
- package/template/.cursor/rules/general.mdc +52 -0
- package/template/.cursor/rules/nextjs.mdc +46 -0
- package/template/.cursor/rules/security.mdc +54 -0
- package/template/.cursor/rules/supabase.mdc +36 -0
- package/template/.cursor/rules/testing.mdc +116 -0
- package/template/.cursor/skills/create-api-route/SKILL.md +62 -0
- package/template/.cursor/skills/create-feature/SKILL.md +52 -0
- package/template/.cursor/skills/review-branch/SKILL.md +61 -0
- package/template/.cursor/skills/security-audit/SKILL.md +69 -0
- package/template/.env.example +24 -0
- package/template/.requirements/README.md +15 -0
- package/template/.requirements/auth.md +50 -0
- package/template/.requirements/template.md +25 -0
- package/template/README.md +98 -0
- package/template/components.json +19 -0
- package/template/drizzle.config.ts +10 -0
- package/template/eslint.config.ts +66 -0
- package/template/middleware.ts +10 -0
- package/template/next.config.ts +31 -0
- package/template/package.json.tmpl +62 -0
- package/template/playwright.config.ts +22 -0
- package/template/src/app/(auth)/login/page.tsx +12 -0
- package/template/src/app/(protected)/layout.tsx +19 -0
- package/template/src/app/(protected)/page.tsx +16 -0
- package/template/src/app/api/auth/callback/route.ts +21 -0
- package/template/src/app/globals.css +1 -0
- package/template/src/app/layout.tsx +21 -0
- package/template/src/e2e/chat.spec.ts +8 -0
- package/template/src/e2e/home.spec.ts +8 -0
- package/template/src/e2e/login.spec.ts +21 -0
- package/template/src/features/auth/__tests__/login-form.test.tsx +43 -0
- package/template/src/features/auth/__tests__/use-auth.test.ts +46 -0
- package/template/src/features/auth/actions/login.action.ts +38 -0
- package/template/src/features/auth/actions/logout.action.ts +10 -0
- package/template/src/features/auth/components/login-form.tsx +80 -0
- package/template/src/features/auth/hooks/use-auth.ts +17 -0
- package/template/src/features/chat/__tests__/chat-ui.test.tsx +30 -0
- package/template/src/features/chat/__tests__/route.test.ts +19 -0
- package/template/src/features/chat/api/route.ts +16 -0
- package/template/src/features/chat/components/chat-ui.tsx +47 -0
- package/template/src/features/chat/hooks/use-chat.ts +1 -0
- package/template/src/features/tts/__tests__/route.test.ts +13 -0
- package/template/src/features/tts/__tests__/tts-player.test.tsx +20 -0
- package/template/src/features/tts/api/route.ts +14 -0
- package/template/src/features/tts/components/tts-player.tsx +59 -0
- package/template/src/features/video/__tests__/route.test.ts +13 -0
- package/template/src/features/video/__tests__/video-generator.test.tsx +20 -0
- package/template/src/features/video/api/route.ts +14 -0
- package/template/src/features/video/components/video-generator.tsx +56 -0
- package/template/src/shared/__tests__/ai.test.ts +8 -0
- package/template/src/shared/__tests__/minimax-media.test.ts +57 -0
- package/template/src/shared/__tests__/providers.test.tsx +14 -0
- package/template/src/shared/__tests__/schema.test.ts +18 -0
- package/template/src/shared/__tests__/supabase-client.test.ts +13 -0
- package/template/src/shared/__tests__/supabase-server.test.ts +22 -0
- package/template/src/shared/components/providers.tsx +19 -0
- package/template/src/shared/db/index.ts +7 -0
- package/template/src/shared/db/schema.ts +15 -0
- package/template/src/shared/lib/ai.ts +8 -0
- package/template/src/shared/lib/minimax-media.ts +63 -0
- package/template/src/shared/lib/supabase/client.ts +8 -0
- package/template/src/shared/lib/supabase/middleware.ts +40 -0
- package/template/src/shared/lib/supabase/server.ts +26 -0
- package/template/src/test-setup.ts +1 -0
- package/template/tailwind.css +20 -0
- package/template/tsconfig.json +31 -0
- package/template/vitest.config.ts +33 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: AI SDK + Edge runtime rules. RISK 3 (cold starts).
|
|
3
|
+
globs: ["src/features/chat/**", "src/features/video/**", "src/features/tts/**", "src/shared/lib/ai*"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# AI Rules (Risk 3: Vercel Cold Starts)
|
|
7
|
+
|
|
8
|
+
## Edge Runtime is Mandatory
|
|
9
|
+
```typescript
|
|
10
|
+
// ✅ ALL AI routes must export this
|
|
11
|
+
export const runtime = "edge";
|
|
12
|
+
|
|
13
|
+
// ❌ Never use Node.js runtime for AI routes (1-3s cold start)
|
|
14
|
+
// export const runtime = "nodejs"; // WRONG
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Never Mix Edge + DB
|
|
18
|
+
```typescript
|
|
19
|
+
// ❌ WRONG — DB + Edge in same route
|
|
20
|
+
export const runtime = "edge";
|
|
21
|
+
import { db } from "@/shared/db"; // WRONG — postgres driver needs Node.js
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If you need DB data in an AI route:
|
|
25
|
+
1. Fetch DB data in a Server Component (Node.js)
|
|
26
|
+
2. Pass to client
|
|
27
|
+
3. Client sends to Edge AI route
|
|
28
|
+
|
|
29
|
+
## AI Model via AI Gateway
|
|
30
|
+
```typescript
|
|
31
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
32
|
+
|
|
33
|
+
const gateway = createOpenAI({
|
|
34
|
+
baseURL: process.env.AI_GATEWAY_URL,
|
|
35
|
+
apiKey: process.env.MINIMAX_API_KEY,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const aiModel = gateway("MiniMax-Text-01");
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Streaming Pattern
|
|
42
|
+
```typescript
|
|
43
|
+
import { streamText } from "ai";
|
|
44
|
+
import { aiModel } from "@/shared/lib/ai";
|
|
45
|
+
|
|
46
|
+
export const runtime = "edge";
|
|
47
|
+
|
|
48
|
+
export async function POST(req: Request) {
|
|
49
|
+
const { messages } = await req.json();
|
|
50
|
+
const result = streamText({ model: aiModel, messages });
|
|
51
|
+
return result.toDataStreamResponse();
|
|
52
|
+
}
|
|
53
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Feature-based architecture rules. Always applies.
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Architecture Rules
|
|
7
|
+
|
|
8
|
+
## Feature-Based Structure
|
|
9
|
+
```
|
|
10
|
+
features/
|
|
11
|
+
└── <feature-name>/
|
|
12
|
+
├── components/ # React components
|
|
13
|
+
├── actions/ # Server Actions (*.action.ts)
|
|
14
|
+
├── hooks/ # React hooks (use-*.ts)
|
|
15
|
+
├── api/ # Route handlers (route.ts)
|
|
16
|
+
└── __tests__/ # Colocated tests
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Dependency Direction
|
|
20
|
+
```
|
|
21
|
+
features/* → shared/* ✅
|
|
22
|
+
shared/* → features/* ❌ (circular dependency)
|
|
23
|
+
features/a → features/b ❌ (cross-feature import)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If two features share logic, move it to `shared/`.
|
|
27
|
+
|
|
28
|
+
## Layer Rules
|
|
29
|
+
- `app/` — routing, layouts, RSC data fetching
|
|
30
|
+
- `features/` — feature business logic, UI, actions
|
|
31
|
+
- `shared/lib/` — infrastructure (supabase, ai, db)
|
|
32
|
+
- `shared/components/` — design system components
|
|
33
|
+
|
|
34
|
+
## Error Boundaries
|
|
35
|
+
- Place at route level (`app/(protected)/layout.tsx`)
|
|
36
|
+
- Never let unhandled errors propagate to root
|
|
37
|
+
|
|
38
|
+
## Server Actions
|
|
39
|
+
- File naming: `name.action.ts`
|
|
40
|
+
- Always `"use server"` at top of file
|
|
41
|
+
- Validate input with Zod before any business logic
|
|
42
|
+
- Return typed result objects, use `redirect()` for navigation
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Code quality standards. Always applies to all files.
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Coding Standards
|
|
7
|
+
|
|
8
|
+
## Absolute Rules
|
|
9
|
+
- **Zero `any`** — use `unknown` + type guards or proper generics. `@typescript-eslint/no-explicit-any: error`
|
|
10
|
+
- **Zero comments** — code must be self-documenting through naming. If you need a comment, rename the function
|
|
11
|
+
- **Zero magic numbers/strings** — use typed constants
|
|
12
|
+
- **No `console.log`** in production code — `no-console: warn`
|
|
13
|
+
- **No `!` non-null assertions** — use proper null checks
|
|
14
|
+
|
|
15
|
+
## TDD (Test-Driven Development)
|
|
16
|
+
1. **RED**: Write a failing test first
|
|
17
|
+
2. **GREEN**: Write minimum code to pass the test
|
|
18
|
+
3. **REFACTOR**: Improve code while tests stay green
|
|
19
|
+
|
|
20
|
+
Never write implementation before tests exist.
|
|
21
|
+
|
|
22
|
+
## TDD — Iron Rule
|
|
23
|
+
- Tests define the contract. **NEVER modify a test to make it pass.**
|
|
24
|
+
- If a test fails, the implementation is wrong — fix the code, not the test.
|
|
25
|
+
- Only modify tests to: add new tests (RED), restructure without weakening (REFACTOR).
|
|
26
|
+
- Weakening a test (removing assertions, loosening matchers, adding `.skip`) is a rejection-worthy offense.
|
|
27
|
+
|
|
28
|
+
## SOLID Principles
|
|
29
|
+
- **S**ingle Responsibility: one reason to change per module
|
|
30
|
+
- **O**pen/Closed: extend behavior without modifying existing code
|
|
31
|
+
- **L**iskov Substitution: subtypes must be substitutable for base types
|
|
32
|
+
- **I**nterface Segregation: prefer small, focused interfaces
|
|
33
|
+
- **D**ependency Inversion: depend on abstractions, not concretions
|
|
34
|
+
|
|
35
|
+
## KISS & DRY
|
|
36
|
+
- Simplest solution that works — no over-engineering
|
|
37
|
+
- Three similar lines is better than a premature abstraction
|
|
38
|
+
- Extract abstraction only when you have 3+ real use cases
|
|
39
|
+
|
|
40
|
+
## Size Limits
|
|
41
|
+
- Functions: max 20 lines
|
|
42
|
+
- Files: max 200 lines
|
|
43
|
+
- If exceeded, split by responsibility
|
|
44
|
+
|
|
45
|
+
## Error Handling
|
|
46
|
+
- Use `Result` types for expected failures
|
|
47
|
+
- Never swallow errors silently
|
|
48
|
+
- Always handle Promise rejections
|
|
49
|
+
|
|
50
|
+
## Types
|
|
51
|
+
- Prefer `type` over `interface` for data shapes
|
|
52
|
+
- Use `satisfies` for type-checked literals
|
|
53
|
+
- Use `as const` for immutable literals
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: shadcn/ui and Tailwind v4 component standards.
|
|
3
|
+
globs: ["src/**/components/**"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Component Standards
|
|
7
|
+
|
|
8
|
+
## shadcn/ui
|
|
9
|
+
- Use shadcn/ui components from `@/shared/components/ui/`
|
|
10
|
+
- Never build custom UI primitives when shadcn has one
|
|
11
|
+
- Extend with `className` prop via `cn()` utility
|
|
12
|
+
- New York style, CSS variables for theming
|
|
13
|
+
|
|
14
|
+
## Tailwind v4
|
|
15
|
+
- CSS-native config via `@theme {}` in CSS
|
|
16
|
+
- No `tailwind.config.js` — all config in `tailwind.css`
|
|
17
|
+
- No inline styles, no CSS modules
|
|
18
|
+
- Use Tailwind utility classes only
|
|
19
|
+
|
|
20
|
+
## Accessibility (Required)
|
|
21
|
+
- Semantic HTML elements (`<button>`, `<nav>`, `<main>`, etc.)
|
|
22
|
+
- ARIA labels for interactive elements without visible text
|
|
23
|
+
- Keyboard navigation support
|
|
24
|
+
- Color contrast ratios (WCAG AA minimum)
|
|
25
|
+
- `role="alert"` for error messages
|
|
26
|
+
|
|
27
|
+
## Server vs Client Components
|
|
28
|
+
- Default to Server Components (no `"use client"`)
|
|
29
|
+
- Add `"use client"` only when you need:
|
|
30
|
+
- Event handlers (`onClick`, `onChange`, etc.)
|
|
31
|
+
- Hooks (`useState`, `useEffect`, etc.)
|
|
32
|
+
- Browser APIs
|
|
33
|
+
- Keep Client Components small — push them to leaf nodes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: TanStack Query vs RSC data fetching rules. RISK 1. Always applies.
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Data Fetching Rules (Risk 1: TanStack Query + RSC Boundary)
|
|
7
|
+
|
|
8
|
+
## Hard Boundary
|
|
9
|
+
|
|
10
|
+
| Use Case | Where |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Initial page data | RSC `async` Server Component |
|
|
13
|
+
| Background refresh / polling | TanStack Query |
|
|
14
|
+
| User-triggered client reads | TanStack Query |
|
|
15
|
+
| All mutations | Server Actions ONLY |
|
|
16
|
+
|
|
17
|
+
**Never use TanStack Query mutations. Never use `useMutation`.**
|
|
18
|
+
|
|
19
|
+
## WRONG ❌
|
|
20
|
+
```typescript
|
|
21
|
+
// ❌ Fetching initial data in a Client Component with TanStack Query
|
|
22
|
+
"use client";
|
|
23
|
+
export function UserProfile({ userId }: { userId: string }) {
|
|
24
|
+
const { data } = useQuery({
|
|
25
|
+
queryKey: ["user", userId],
|
|
26
|
+
queryFn: () => fetchUser(userId), // Initial data should come from RSC
|
|
27
|
+
});
|
|
28
|
+
return <div>{data?.name}</div>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ❌ Mutation with TanStack Query
|
|
32
|
+
const mutation = useMutation({
|
|
33
|
+
mutationFn: (data) => updateUser(data), // Use Server Action instead
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## CORRECT ✅
|
|
38
|
+
```typescript
|
|
39
|
+
// ✅ Initial data from RSC, passed as prop
|
|
40
|
+
export default async function UserPage({ params }: { params: { id: string } }) {
|
|
41
|
+
const user = await fetchUser(params.id); // RSC fetch
|
|
42
|
+
return <UserProfile initialUser={user} />;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ✅ TanStack Query for background refresh only
|
|
46
|
+
"use client";
|
|
47
|
+
export function UserProfile({ initialUser }: { initialUser: User }) {
|
|
48
|
+
const { data: user } = useQuery({
|
|
49
|
+
queryKey: ["user", initialUser.id],
|
|
50
|
+
queryFn: () => fetchUser(initialUser.id),
|
|
51
|
+
initialData: initialUser, // Seeded from RSC
|
|
52
|
+
staleTime: 5 * 60 * 1000,
|
|
53
|
+
});
|
|
54
|
+
return <div>{user.name}</div>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ✅ Mutation via Server Action
|
|
58
|
+
async function updateUserAction(formData: FormData) {
|
|
59
|
+
"use server";
|
|
60
|
+
await db.update(users).set({ name: formData.get("name") });
|
|
61
|
+
revalidatePath("/profile");
|
|
62
|
+
}
|
|
63
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: React Hook Form + Server Actions pattern. RISK 4.
|
|
3
|
+
globs: ["**/*form*", "**/*.action.ts"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Forms Pattern (Risk 4: RHF + Server Actions)
|
|
7
|
+
|
|
8
|
+
## The One Pattern
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
Client: RHF + zodResolver → client validation (UX)
|
|
12
|
+
↓ valid
|
|
13
|
+
Server Action → zod validate again (security) → business logic
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Zod validates TWICE:
|
|
17
|
+
1. Client-side: immediate UX feedback via RHF
|
|
18
|
+
2. Server-side: never trust the client
|
|
19
|
+
|
|
20
|
+
## Template
|
|
21
|
+
```typescript
|
|
22
|
+
// form.tsx (client)
|
|
23
|
+
"use client";
|
|
24
|
+
const schema = z.object({ email: z.string().email() });
|
|
25
|
+
type FormValues = z.infer<typeof schema>;
|
|
26
|
+
|
|
27
|
+
export function MyForm() {
|
|
28
|
+
const [state, formAction, isPending] = useActionState(myAction, { status: "idle" });
|
|
29
|
+
const { register, handleSubmit, formState: { errors } } = useForm<FormValues>({
|
|
30
|
+
resolver: zodResolver(schema),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const onSubmit = handleSubmit((data) => {
|
|
34
|
+
const fd = new FormData();
|
|
35
|
+
Object.entries(data).forEach(([k, v]) => fd.set(k, String(v)));
|
|
36
|
+
formAction(fd);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<form onSubmit={onSubmit}>
|
|
41
|
+
{state.status === "error" && <p role="alert">{state.message}</p>}
|
|
42
|
+
<input {...register("email")} />
|
|
43
|
+
{errors.email && <p>{errors.email.message}</p>}
|
|
44
|
+
<button disabled={isPending}>Submit</button>
|
|
45
|
+
</form>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// action.ts (server)
|
|
50
|
+
"use server";
|
|
51
|
+
const schema = z.object({ email: z.string().email() });
|
|
52
|
+
|
|
53
|
+
export async function myAction(_prev: State, formData: FormData): Promise<State> {
|
|
54
|
+
const parsed = schema.safeParse({ email: formData.get("email") });
|
|
55
|
+
if (!parsed.success) return { status: "error", message: parsed.error.issues[0]?.message ?? "Invalid" };
|
|
56
|
+
// business logic...
|
|
57
|
+
redirect("/success");
|
|
58
|
+
}
|
|
59
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Stack overview and project conventions. Always applies.
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Stack Overview
|
|
7
|
+
|
|
8
|
+
## Technology Stack
|
|
9
|
+
- **Framework**: Next.js 15 (App Router)
|
|
10
|
+
- **Database**: Supabase (PostgreSQL) with Drizzle ORM for schema/migrations
|
|
11
|
+
- **Auth**: Supabase Auth via `@supabase/ssr`
|
|
12
|
+
- **ORM**: Drizzle + `drizzle-zod` (schema + migrations ONLY — no runtime queries)
|
|
13
|
+
- **DB Runtime Queries**: `supabase-js` client (RLS active)
|
|
14
|
+
- **Client State**: TanStack Query v5
|
|
15
|
+
- **Validation**: Zod (auto-generated via `drizzle-zod`)
|
|
16
|
+
- **Forms**: React Hook Form + Zod resolver
|
|
17
|
+
- **UI**: shadcn/ui + Tailwind CSS v4
|
|
18
|
+
- **AI**: Vercel AI SDK + MiniMax M2.7 via AI Gateway
|
|
19
|
+
- **Testing**: Vitest + React Testing Library + Playwright
|
|
20
|
+
|
|
21
|
+
## Project Structure
|
|
22
|
+
```
|
|
23
|
+
src/
|
|
24
|
+
├── app/ # Next.js App Router pages
|
|
25
|
+
├── features/ # Feature modules (auth, chat, video, tts)
|
|
26
|
+
│ └── <feature>/
|
|
27
|
+
│ ├── components/
|
|
28
|
+
│ ├── actions/
|
|
29
|
+
│ ├── hooks/
|
|
30
|
+
│ ├── api/
|
|
31
|
+
│ └── __tests__/
|
|
32
|
+
├── shared/ # Shared across features
|
|
33
|
+
│ ├── components/ui/ # shadcn/ui components
|
|
34
|
+
│ ├── lib/ # supabase, ai, utilities
|
|
35
|
+
│ └── db/ # Drizzle schema + migrations
|
|
36
|
+
└── e2e/ # Playwright tests
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Naming Conventions
|
|
40
|
+
- Files: `kebab-case.ts`
|
|
41
|
+
- Components: `PascalCase`
|
|
42
|
+
- Hooks: `useCamelCase`
|
|
43
|
+
- Server Actions: `camelCase.action.ts`
|
|
44
|
+
- API Routes: `route.ts` (in feature `api/` folder)
|
|
45
|
+
- Tests: `*.test.ts` or `*.test.tsx`
|
|
46
|
+
- E2e: `*.spec.ts`
|
|
47
|
+
|
|
48
|
+
## Import Ordering
|
|
49
|
+
1. Node built-ins
|
|
50
|
+
2. External packages
|
|
51
|
+
3. Internal (`@/`)
|
|
52
|
+
4. Relative (`./`, `../`)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Next.js 15 App Router patterns.
|
|
3
|
+
globs: ["src/app/**"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Next.js 15 Rules
|
|
7
|
+
|
|
8
|
+
## App Router Conventions
|
|
9
|
+
- `page.tsx` — public route page component
|
|
10
|
+
- `layout.tsx` — shared layout (wraps child pages)
|
|
11
|
+
- `loading.tsx` — Suspense boundary UI
|
|
12
|
+
- `error.tsx` — Error boundary UI
|
|
13
|
+
- `route.ts` — API route handler
|
|
14
|
+
- `middleware.ts` — Edge middleware (project root)
|
|
15
|
+
|
|
16
|
+
## Route Groups
|
|
17
|
+
- `(auth)` — unauthenticated routes
|
|
18
|
+
- `(protected)` — authenticated routes with RSC auth check
|
|
19
|
+
|
|
20
|
+
## Data Fetching in RSC
|
|
21
|
+
```typescript
|
|
22
|
+
// ✅ Async Server Component — fetch directly
|
|
23
|
+
export default async function Page() {
|
|
24
|
+
const data = await fetchData(); // No useState, no useEffect
|
|
25
|
+
return <ClientComponent initialData={data} />;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Route Handler Patterns
|
|
30
|
+
```typescript
|
|
31
|
+
// app/api/*/route.ts
|
|
32
|
+
export async function POST(request: Request) {
|
|
33
|
+
const body = await request.json() as unknown;
|
|
34
|
+
const parsed = schema.safeParse(body);
|
|
35
|
+
if (!parsed.success) return Response.json({ error: "Invalid input" }, { status: 400 });
|
|
36
|
+
// ...
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Metadata
|
|
41
|
+
```typescript
|
|
42
|
+
export const metadata: Metadata = {
|
|
43
|
+
title: "Page Title",
|
|
44
|
+
description: "Page description",
|
|
45
|
+
};
|
|
46
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Security rules and OWASP guidelines. Always applies.
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Security Rules
|
|
7
|
+
|
|
8
|
+
## OWASP Top 10 Awareness
|
|
9
|
+
1. **Injection** — use Drizzle parameterized queries, never string concatenation
|
|
10
|
+
2. **Broken Auth** — Supabase Auth only, no custom auth
|
|
11
|
+
3. **Sensitive Data** — HTTPS-only cookies, no secrets in code
|
|
12
|
+
4. **XSS** — never use `dangerouslySetInnerHTML`, escape user input
|
|
13
|
+
5. **Broken Access Control** — RLS on all tables, auth check in all protected routes
|
|
14
|
+
6. **Security Misconfiguration** — CSP, HSTS, X-Frame-Options in `next.config.ts`
|
|
15
|
+
7. **Insecure Dependencies** — run `npm audit` regularly
|
|
16
|
+
8. **SSRF** — validate URLs before fetch
|
|
17
|
+
|
|
18
|
+
## Environment Variables
|
|
19
|
+
```
|
|
20
|
+
NEXT_PUBLIC_* → client-safe (Supabase URL, anon key only)
|
|
21
|
+
No prefix → server-only (DB URL, secret keys)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Never use `NEXT_PUBLIC_` for secret keys.
|
|
25
|
+
|
|
26
|
+
## Supabase RLS
|
|
27
|
+
- Enable RLS on EVERY table
|
|
28
|
+
- Write explicit allow/deny policies
|
|
29
|
+
- Test RLS policies — never assume they work
|
|
30
|
+
|
|
31
|
+
## Auth in API Routes
|
|
32
|
+
```typescript
|
|
33
|
+
export async function POST(request: Request) {
|
|
34
|
+
const supabase = await createClient();
|
|
35
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
36
|
+
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
37
|
+
// ...
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Input Validation
|
|
42
|
+
- Validate ALL external input with Zod at the boundary
|
|
43
|
+
- Validate in Server Actions AND API routes
|
|
44
|
+
- Never trust client-side validation alone
|
|
45
|
+
|
|
46
|
+
## Rate Limiting
|
|
47
|
+
- Apply rate limiting to all AI routes
|
|
48
|
+
- Apply rate limiting to auth endpoints
|
|
49
|
+
- Use Vercel's built-in rate limiting or `@upstash/ratelimit`
|
|
50
|
+
|
|
51
|
+
## Cookies
|
|
52
|
+
- `httpOnly: true` for auth tokens (Supabase handles this)
|
|
53
|
+
- `secure: true` in production
|
|
54
|
+
- `sameSite: "lax"` minimum
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Supabase + Drizzle rules. RISK 2 (schema drift).
|
|
3
|
+
globs: ["src/shared/db/**", "src/shared/lib/supabase/**"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Supabase + Drizzle Rules (Risk 2: Zod Schema Drift)
|
|
7
|
+
|
|
8
|
+
## Drizzle: Schema + Migrations ONLY
|
|
9
|
+
```typescript
|
|
10
|
+
// ✅ Use Drizzle for schema definition
|
|
11
|
+
export const users = pgTable("users", {
|
|
12
|
+
id: uuid("id").primaryKey(),
|
|
13
|
+
email: text("email").notNull(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// ✅ Auto-generate Zod from Drizzle — zero manual Zod for DB types
|
|
17
|
+
export const insertUserSchema = createInsertSchema(users);
|
|
18
|
+
export const selectUserSchema = createSelectSchema(users);
|
|
19
|
+
|
|
20
|
+
// ❌ NEVER write Zod schemas manually for DB types
|
|
21
|
+
const userSchema = z.object({ id: z.string(), email: z.string() }); // WRONG
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Runtime Queries: supabase-js (RLS active)
|
|
25
|
+
```typescript
|
|
26
|
+
// ✅ Runtime queries via supabase-js
|
|
27
|
+
const { data, error } = await supabase.from("users").select("*").eq("id", userId);
|
|
28
|
+
|
|
29
|
+
// ❌ Never use Drizzle for runtime queries
|
|
30
|
+
const users = await db.select().from(usersTable); // WRONG — bypasses RLS
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## RLS Mandate
|
|
34
|
+
- Enable RLS on ALL tables in Supabase dashboard
|
|
35
|
+
- Every table must have explicit policies
|
|
36
|
+
- Test RLS policies in migrations
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: TDD, Vitest, and Playwright testing standards.
|
|
3
|
+
globs: ["**/*.test.*", "**/e2e/**"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testing Standards
|
|
7
|
+
|
|
8
|
+
## Iron Rules (Non-Negotiable)
|
|
9
|
+
1. **NEVER modify a test to make it pass** — always fix the implementation code. Tests define the contract. If a test fails, the code is wrong, not the test.
|
|
10
|
+
2. **Tests are immutable during GREEN phase** — only modify tests in RED phase (writing new ones) or REFACTOR phase (improving structure, never weakening assertions)
|
|
11
|
+
3. **100% branch coverage** — every `if`, `else`, `switch`, `??`, `?.`, ternary, and `catch` must be exercised
|
|
12
|
+
4. **Every edge case gets a test** — null/undefined, empty string, empty array, boundary values, error responses, timeout, concurrent calls
|
|
13
|
+
5. **AAA is mandatory** — every test uses Arrange-Act-Assert with labeled comments
|
|
14
|
+
|
|
15
|
+
## TDD Cycle
|
|
16
|
+
1. **RED**: Write failing test describing desired behavior
|
|
17
|
+
2. **GREEN**: Write minimum code to make it pass
|
|
18
|
+
3. **REFACTOR**: Clean up while keeping tests green
|
|
19
|
+
|
|
20
|
+
## AAA Pattern (Arrange-Act-Assert)
|
|
21
|
+
|
|
22
|
+
Every test MUST follow the Arrange-Act-Assert structure. No exceptions.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
it("shows error message on invalid credentials", async () => {
|
|
26
|
+
// Arrange
|
|
27
|
+
const user = userEvent.setup();
|
|
28
|
+
mockSignIn.mockResolvedValue({ error: { message: "Invalid credentials" } });
|
|
29
|
+
render(<LoginForm />);
|
|
30
|
+
|
|
31
|
+
// Act
|
|
32
|
+
await user.type(screen.getByLabelText(/email/i), "test@example.com");
|
|
33
|
+
await user.type(screen.getByLabelText(/password/i), "password123");
|
|
34
|
+
await user.click(screen.getByRole("button", { name: /sign in/i }));
|
|
35
|
+
|
|
36
|
+
// Assert
|
|
37
|
+
expect(await screen.findByRole("alert")).toHaveTextContent("Invalid credentials");
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Rules:
|
|
42
|
+
- **Arrange**: set up state, mocks, render
|
|
43
|
+
- **Act**: perform the single action being tested
|
|
44
|
+
- **Assert**: verify outcome — one logical assertion per test
|
|
45
|
+
- Add blank lines between sections when all three are non-trivial
|
|
46
|
+
- Never skip a section — if Act is empty, you are testing the wrong thing
|
|
47
|
+
|
|
48
|
+
## Vitest (Unit + Integration)
|
|
49
|
+
- Mock external dependencies only (HTTP calls, Supabase, DB)
|
|
50
|
+
- Test behavior, not implementation details
|
|
51
|
+
- Colocate tests with source: `features/auth/__tests__/`
|
|
52
|
+
- 100% coverage is non-negotiable
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// ✅ AAA — tests behavior
|
|
56
|
+
it("shows error message on invalid credentials", async () => {
|
|
57
|
+
// Arrange
|
|
58
|
+
mockSignIn.mockResolvedValue({ error: { message: "Invalid credentials" } });
|
|
59
|
+
render(<LoginForm />);
|
|
60
|
+
|
|
61
|
+
// Act
|
|
62
|
+
await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
|
|
63
|
+
|
|
64
|
+
// Assert
|
|
65
|
+
expect(await screen.findByRole("alert")).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ❌ No AAA — tests implementation details, brittle
|
|
69
|
+
it("calls signInWithPassword with correct args", () => { ... });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Playwright (E2E)
|
|
73
|
+
- Use `data-testid` attributes for stable selectors
|
|
74
|
+
- Page Object Pattern for complex flows
|
|
75
|
+
- Every user flow needs an e2e test
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
test("user can log in", async ({ page }) => {
|
|
79
|
+
// Arrange
|
|
80
|
+
await page.goto("/login");
|
|
81
|
+
|
|
82
|
+
// Act
|
|
83
|
+
await page.getByLabel(/email/i).fill("user@example.com");
|
|
84
|
+
await page.getByLabel(/password/i).fill("password123");
|
|
85
|
+
await page.getByRole("button", { name: /sign in/i }).click();
|
|
86
|
+
|
|
87
|
+
// Assert
|
|
88
|
+
await expect(page).toHaveURL("/");
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Edge Case Checklist
|
|
93
|
+
For every function, ask: what happens with...
|
|
94
|
+
- null / undefined input
|
|
95
|
+
- empty string / empty array / empty object
|
|
96
|
+
- boundary values (0, -1, MAX_SAFE_INTEGER)
|
|
97
|
+
- invalid types (wrong shape, missing fields)
|
|
98
|
+
- API error responses (4xx, 5xx, network failure)
|
|
99
|
+
- Loading / pending states
|
|
100
|
+
- Concurrent calls / race conditions
|
|
101
|
+
- Auth expired / missing session
|
|
102
|
+
|
|
103
|
+
If any of these apply, write a test for it. No exceptions.
|
|
104
|
+
|
|
105
|
+
## Coverage Thresholds
|
|
106
|
+
```typescript
|
|
107
|
+
// vitest.config.ts
|
|
108
|
+
coverage: {
|
|
109
|
+
thresholds: {
|
|
110
|
+
branches: 100,
|
|
111
|
+
functions: 100,
|
|
112
|
+
lines: 100,
|
|
113
|
+
statements: 100,
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-api-route
|
|
3
|
+
description: Create a new Next.js API route with validation, auth, and tests. Use when adding API endpoints to a feature.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Create API Route Skill
|
|
7
|
+
|
|
8
|
+
## Process
|
|
9
|
+
|
|
10
|
+
### 1. Define Schema
|
|
11
|
+
```typescript
|
|
12
|
+
const requestSchema = z.object({ /* fields */ });
|
|
13
|
+
const responseSchema = z.object({ /* fields */ });
|
|
14
|
+
type RequestBody = z.infer<typeof requestSchema>;
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 2. Write Test First (TDD)
|
|
18
|
+
```typescript
|
|
19
|
+
describe("POST /api/my-route", () => {
|
|
20
|
+
it("returns 400 on invalid input", async () => { ... });
|
|
21
|
+
it("returns 401 when unauthenticated", async () => { ... });
|
|
22
|
+
it("returns 200 with valid input", async () => { ... });
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Run test — must FAIL (RED).
|
|
27
|
+
|
|
28
|
+
### 3. Implement Route
|
|
29
|
+
```typescript
|
|
30
|
+
// Determine runtime:
|
|
31
|
+
// - AI routes: export const runtime = "edge"
|
|
32
|
+
// - DB routes: no export (Node.js default)
|
|
33
|
+
|
|
34
|
+
export async function POST(request: Request) {
|
|
35
|
+
const supabase = await createClient();
|
|
36
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
37
|
+
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
38
|
+
|
|
39
|
+
const body = await request.json() as unknown;
|
|
40
|
+
const parsed = requestSchema.safeParse(body);
|
|
41
|
+
if (!parsed.success) return Response.json({ error: "Invalid input" }, { status: 400 });
|
|
42
|
+
|
|
43
|
+
// business logic...
|
|
44
|
+
|
|
45
|
+
return Response.json(result);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 4. Verify
|
|
50
|
+
```bash
|
|
51
|
+
npm run test:unit
|
|
52
|
+
npm run lint
|
|
53
|
+
npm run typecheck
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Checklist
|
|
57
|
+
- [ ] Zod schema for request/response
|
|
58
|
+
- [ ] Auth check (unless public endpoint)
|
|
59
|
+
- [ ] Input validation
|
|
60
|
+
- [ ] Correct runtime (`edge` for AI, `nodejs` default for DB)
|
|
61
|
+
- [ ] Tests written BEFORE implementation
|
|
62
|
+
- [ ] 100% coverage
|