ultra-dex 2.2.1 → 3.2.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/README.md +112 -151
- package/assets/agents/00-AGENT_INDEX.md +1 -1
- package/assets/code-patterns/clerk-middleware.ts +138 -0
- package/assets/code-patterns/prisma-schema.prisma +224 -0
- package/assets/code-patterns/rls-policies.sql +246 -0
- package/assets/code-patterns/server-actions.ts +191 -0
- package/assets/code-patterns/trpc-router.ts +258 -0
- package/assets/cursor-rules/13-ai-integration.mdc +155 -0
- package/assets/cursor-rules/14-server-components.mdc +81 -0
- package/assets/cursor-rules/15-server-actions.mdc +102 -0
- package/assets/cursor-rules/16-edge-middleware.mdc +105 -0
- package/assets/cursor-rules/17-streaming-ssr.mdc +138 -0
- package/assets/docs/LAUNCH-POSTS.md +1 -1
- package/assets/docs/QUICK-REFERENCE.md +9 -4
- package/assets/docs/VISION-V2.md +1 -1
- package/assets/hooks/pre-commit +98 -0
- package/assets/saas-plan/04-Imp-Template.md +1 -1
- package/bin/ultra-dex.js +132 -4
- package/lib/commands/advanced.js +471 -0
- package/lib/commands/agent-builder.js +226 -0
- package/lib/commands/agents.js +102 -42
- package/lib/commands/auto-implement.js +68 -0
- package/lib/commands/banner.js +43 -21
- package/lib/commands/build.js +78 -183
- package/lib/commands/ci-monitor.js +84 -0
- package/lib/commands/config.js +207 -0
- package/lib/commands/dashboard.js +770 -0
- package/lib/commands/diff.js +233 -0
- package/lib/commands/doctor.js +416 -0
- package/lib/commands/export.js +408 -0
- package/lib/commands/fix.js +96 -0
- package/lib/commands/generate.js +105 -78
- package/lib/commands/hooks.js +251 -76
- package/lib/commands/init.js +102 -54
- package/lib/commands/memory.js +80 -0
- package/lib/commands/plan.js +82 -0
- package/lib/commands/review.js +34 -5
- package/lib/commands/run.js +233 -0
- package/lib/commands/scaffold.js +151 -0
- package/lib/commands/serve.js +179 -146
- package/lib/commands/state.js +327 -0
- package/lib/commands/swarm.js +306 -0
- package/lib/commands/sync.js +82 -23
- package/lib/commands/team.js +275 -0
- package/lib/commands/upgrade.js +190 -0
- package/lib/commands/validate.js +34 -0
- package/lib/commands/verify.js +81 -0
- package/lib/commands/watch.js +79 -0
- package/lib/config/theme.js +47 -0
- package/lib/mcp/graph.js +92 -0
- package/lib/mcp/memory.js +95 -0
- package/lib/mcp/resources.js +152 -0
- package/lib/mcp/server.js +34 -0
- package/lib/mcp/tools.js +481 -0
- package/lib/mcp/websocket.js +117 -0
- package/lib/providers/index.js +49 -4
- package/lib/providers/ollama.js +136 -0
- package/lib/providers/router.js +63 -0
- package/lib/quality/scanner.js +128 -0
- package/lib/swarm/coordinator.js +97 -0
- package/lib/swarm/index.js +598 -0
- package/lib/swarm/protocol.js +677 -0
- package/lib/swarm/tiers.js +485 -0
- package/lib/templates/code/clerk-middleware.ts +138 -0
- package/lib/templates/code/prisma-schema.prisma +224 -0
- package/lib/templates/code/rls-policies.sql +246 -0
- package/lib/templates/code/server-actions.ts +191 -0
- package/lib/templates/code/trpc-router.ts +258 -0
- package/lib/templates/custom-agent.md +10 -0
- package/lib/themes/doomsday.js +229 -0
- package/lib/ui/index.js +5 -0
- package/lib/ui/interface.js +241 -0
- package/lib/ui/spinners.js +116 -0
- package/lib/ui/theme.js +183 -0
- package/lib/utils/agents.js +32 -0
- package/lib/utils/files.js +14 -0
- package/lib/utils/graph.js +108 -0
- package/lib/utils/help.js +64 -0
- package/lib/utils/messages.js +35 -0
- package/lib/utils/progress.js +24 -0
- package/lib/utils/prompts.js +47 -0
- package/lib/utils/spinners.js +46 -0
- package/lib/utils/status.js +31 -0
- package/lib/utils/tables.js +41 -0
- package/lib/utils/theme-state.js +9 -0
- package/lib/utils/version-display.js +32 -0
- package/package.json +31 -13
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// Ultra-Dex Production Pattern: tRPC Router
|
|
2
|
+
// Copy to server/routers/ directory
|
|
3
|
+
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { TRPCError } from '@trpc/server';
|
|
6
|
+
import { router, publicProcedure, protectedProcedure, adminProcedure } from '../trpc';
|
|
7
|
+
import { prisma } from '@/lib/prisma';
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// USER ROUTER
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
export const userRouter = router({
|
|
14
|
+
// Get current user profile
|
|
15
|
+
me: protectedProcedure.query(async ({ ctx }) => {
|
|
16
|
+
const user = await prisma.user.findUnique({
|
|
17
|
+
where: { clerkId: ctx.userId },
|
|
18
|
+
include: {
|
|
19
|
+
organizationMemberships: {
|
|
20
|
+
include: { organization: true },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!user) {
|
|
26
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return user;
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
// Update current user profile
|
|
33
|
+
update: protectedProcedure
|
|
34
|
+
.input(
|
|
35
|
+
z.object({
|
|
36
|
+
name: z.string().min(2).optional(),
|
|
37
|
+
imageUrl: z.string().url().optional(),
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
.mutation(async ({ ctx, input }) => {
|
|
41
|
+
return prisma.user.update({
|
|
42
|
+
where: { clerkId: ctx.userId },
|
|
43
|
+
data: input,
|
|
44
|
+
});
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
// List all users (admin only)
|
|
48
|
+
list: adminProcedure
|
|
49
|
+
.input(
|
|
50
|
+
z.object({
|
|
51
|
+
limit: z.number().min(1).max(100).default(50),
|
|
52
|
+
cursor: z.string().optional(),
|
|
53
|
+
})
|
|
54
|
+
)
|
|
55
|
+
.query(async ({ input }) => {
|
|
56
|
+
const users = await prisma.user.findMany({
|
|
57
|
+
take: input.limit + 1,
|
|
58
|
+
cursor: input.cursor ? { id: input.cursor } : undefined,
|
|
59
|
+
orderBy: { createdAt: 'desc' },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
let nextCursor: string | undefined;
|
|
63
|
+
if (users.length > input.limit) {
|
|
64
|
+
const nextItem = users.pop();
|
|
65
|
+
nextCursor = nextItem!.id;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { users, nextCursor };
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// PROJECT ROUTER
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
export const projectRouter = router({
|
|
77
|
+
// List projects for current organization
|
|
78
|
+
list: protectedProcedure
|
|
79
|
+
.input(
|
|
80
|
+
z.object({
|
|
81
|
+
organizationId: z.string(),
|
|
82
|
+
status: z.enum(['ACTIVE', 'ARCHIVED', 'COMPLETED']).optional(),
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
.query(async ({ ctx, input }) => {
|
|
86
|
+
// Verify user has access to organization
|
|
87
|
+
const membership = await prisma.organizationMember.findFirst({
|
|
88
|
+
where: {
|
|
89
|
+
organizationId: input.organizationId,
|
|
90
|
+
user: { clerkId: ctx.userId },
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!membership) {
|
|
95
|
+
throw new TRPCError({ code: 'FORBIDDEN', message: 'Access denied' });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return prisma.project.findMany({
|
|
99
|
+
where: {
|
|
100
|
+
organizationId: input.organizationId,
|
|
101
|
+
...(input.status && { status: input.status }),
|
|
102
|
+
},
|
|
103
|
+
include: {
|
|
104
|
+
owner: { select: { name: true, imageUrl: true } },
|
|
105
|
+
_count: { select: { tasks: true } },
|
|
106
|
+
},
|
|
107
|
+
orderBy: { updatedAt: 'desc' },
|
|
108
|
+
});
|
|
109
|
+
}),
|
|
110
|
+
|
|
111
|
+
// Get single project
|
|
112
|
+
get: protectedProcedure
|
|
113
|
+
.input(z.object({ id: z.string() }))
|
|
114
|
+
.query(async ({ ctx, input }) => {
|
|
115
|
+
const project = await prisma.project.findUnique({
|
|
116
|
+
where: { id: input.id },
|
|
117
|
+
include: {
|
|
118
|
+
owner: true,
|
|
119
|
+
tasks: { orderBy: { createdAt: 'desc' } },
|
|
120
|
+
organization: true,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!project) {
|
|
125
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Verify access
|
|
129
|
+
const membership = await prisma.organizationMember.findFirst({
|
|
130
|
+
where: {
|
|
131
|
+
organizationId: project.organizationId,
|
|
132
|
+
user: { clerkId: ctx.userId },
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!membership) {
|
|
137
|
+
throw new TRPCError({ code: 'FORBIDDEN' });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return project;
|
|
141
|
+
}),
|
|
142
|
+
|
|
143
|
+
// Create project
|
|
144
|
+
create: protectedProcedure
|
|
145
|
+
.input(
|
|
146
|
+
z.object({
|
|
147
|
+
name: z.string().min(1).max(100),
|
|
148
|
+
description: z.string().max(500).optional(),
|
|
149
|
+
organizationId: z.string(),
|
|
150
|
+
})
|
|
151
|
+
)
|
|
152
|
+
.mutation(async ({ ctx, input }) => {
|
|
153
|
+
// Verify user has write access to organization
|
|
154
|
+
const membership = await prisma.organizationMember.findFirst({
|
|
155
|
+
where: {
|
|
156
|
+
organizationId: input.organizationId,
|
|
157
|
+
user: { clerkId: ctx.userId },
|
|
158
|
+
role: { in: ['OWNER', 'ADMIN', 'MEMBER'] },
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!membership) {
|
|
163
|
+
throw new TRPCError({ code: 'FORBIDDEN' });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const user = await prisma.user.findUnique({
|
|
167
|
+
where: { clerkId: ctx.userId },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return prisma.project.create({
|
|
171
|
+
data: {
|
|
172
|
+
name: input.name,
|
|
173
|
+
description: input.description,
|
|
174
|
+
organizationId: input.organizationId,
|
|
175
|
+
ownerId: user!.id,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}),
|
|
179
|
+
|
|
180
|
+
// Update project
|
|
181
|
+
update: protectedProcedure
|
|
182
|
+
.input(
|
|
183
|
+
z.object({
|
|
184
|
+
id: z.string(),
|
|
185
|
+
name: z.string().min(1).max(100).optional(),
|
|
186
|
+
description: z.string().max(500).optional(),
|
|
187
|
+
status: z.enum(['ACTIVE', 'ARCHIVED', 'COMPLETED']).optional(),
|
|
188
|
+
})
|
|
189
|
+
)
|
|
190
|
+
.mutation(async ({ ctx, input }) => {
|
|
191
|
+
const { id, ...data } = input;
|
|
192
|
+
|
|
193
|
+
const project = await prisma.project.findUnique({
|
|
194
|
+
where: { id },
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!project) {
|
|
198
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Verify write access
|
|
202
|
+
const membership = await prisma.organizationMember.findFirst({
|
|
203
|
+
where: {
|
|
204
|
+
organizationId: project.organizationId,
|
|
205
|
+
user: { clerkId: ctx.userId },
|
|
206
|
+
role: { in: ['OWNER', 'ADMIN', 'MEMBER'] },
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (!membership) {
|
|
211
|
+
throw new TRPCError({ code: 'FORBIDDEN' });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return prisma.project.update({
|
|
215
|
+
where: { id },
|
|
216
|
+
data,
|
|
217
|
+
});
|
|
218
|
+
}),
|
|
219
|
+
|
|
220
|
+
// Delete project
|
|
221
|
+
delete: protectedProcedure
|
|
222
|
+
.input(z.object({ id: z.string() }))
|
|
223
|
+
.mutation(async ({ ctx, input }) => {
|
|
224
|
+
const project = await prisma.project.findUnique({
|
|
225
|
+
where: { id: input.id },
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (!project) {
|
|
229
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Only owner/admin can delete
|
|
233
|
+
const membership = await prisma.organizationMember.findFirst({
|
|
234
|
+
where: {
|
|
235
|
+
organizationId: project.organizationId,
|
|
236
|
+
user: { clerkId: ctx.userId },
|
|
237
|
+
role: { in: ['OWNER', 'ADMIN'] },
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (!membership) {
|
|
242
|
+
throw new TRPCError({ code: 'FORBIDDEN' });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return prisma.project.delete({ where: { id: input.id } });
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// =============================================================================
|
|
250
|
+
// ROOT ROUTER
|
|
251
|
+
// =============================================================================
|
|
252
|
+
|
|
253
|
+
export const appRouter = router({
|
|
254
|
+
user: userRouter,
|
|
255
|
+
project: projectRouter,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
export type AppRouter = typeof appRouter;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# AI/LLM Integration (2026 Patterns)
|
|
2
|
+
|
|
3
|
+
> Patterns for integrating AI into production applications.
|
|
4
|
+
|
|
5
|
+
## Vercel AI SDK (Recommended)
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
// app/api/chat/route.ts
|
|
9
|
+
import { streamText } from 'ai';
|
|
10
|
+
import { anthropic } from '@ai-sdk/anthropic';
|
|
11
|
+
|
|
12
|
+
export async function POST(req: Request) {
|
|
13
|
+
const { messages } = await req.json();
|
|
14
|
+
|
|
15
|
+
const result = streamText({
|
|
16
|
+
model: anthropic('claude-sonnet-4-20250514'),
|
|
17
|
+
messages,
|
|
18
|
+
system: 'You are a helpful assistant.',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return result.toDataStreamResponse();
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Client-Side Streaming
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
'use client';
|
|
29
|
+
|
|
30
|
+
import { useChat } from 'ai/react';
|
|
31
|
+
|
|
32
|
+
export function Chat() {
|
|
33
|
+
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div>
|
|
37
|
+
{messages.map(m => (
|
|
38
|
+
<div key={m.id}>
|
|
39
|
+
<strong>{m.role}:</strong> {m.content}
|
|
40
|
+
</div>
|
|
41
|
+
))}
|
|
42
|
+
|
|
43
|
+
<form onSubmit={handleSubmit}>
|
|
44
|
+
<input
|
|
45
|
+
value={input}
|
|
46
|
+
onChange={handleInputChange}
|
|
47
|
+
placeholder="Type a message..."
|
|
48
|
+
disabled={isLoading}
|
|
49
|
+
/>
|
|
50
|
+
<button type="submit" disabled={isLoading}>
|
|
51
|
+
{isLoading ? 'Thinking...' : 'Send'}
|
|
52
|
+
</button>
|
|
53
|
+
</form>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Structured Output with Zod
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { generateObject } from 'ai';
|
|
63
|
+
import { z } from 'zod';
|
|
64
|
+
|
|
65
|
+
const ProductSchema = z.object({
|
|
66
|
+
name: z.string(),
|
|
67
|
+
description: z.string(),
|
|
68
|
+
price: z.number(),
|
|
69
|
+
category: z.enum(['electronics', 'clothing', 'food']),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const { object } = await generateObject({
|
|
73
|
+
model: anthropic('claude-sonnet-4-20250514'),
|
|
74
|
+
schema: ProductSchema,
|
|
75
|
+
prompt: 'Generate a product for an e-commerce store.',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// object is fully typed as { name: string, description: string, ... }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Tool Use / Function Calling
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { generateText, tool } from 'ai';
|
|
85
|
+
import { z } from 'zod';
|
|
86
|
+
|
|
87
|
+
const result = await generateText({
|
|
88
|
+
model: anthropic('claude-sonnet-4-20250514'),
|
|
89
|
+
tools: {
|
|
90
|
+
weather: tool({
|
|
91
|
+
description: 'Get the weather for a location',
|
|
92
|
+
parameters: z.object({
|
|
93
|
+
location: z.string().describe('City name'),
|
|
94
|
+
}),
|
|
95
|
+
execute: async ({ location }) => {
|
|
96
|
+
// Call weather API
|
|
97
|
+
return { temperature: 72, condition: 'sunny' };
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
prompt: 'What is the weather in San Francisco?',
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Rate Limiting for AI Endpoints
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { Ratelimit } from '@upstash/ratelimit';
|
|
109
|
+
import { Redis } from '@upstash/redis';
|
|
110
|
+
|
|
111
|
+
const ratelimit = new Ratelimit({
|
|
112
|
+
redis: Redis.fromEnv(),
|
|
113
|
+
limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 requests per minute
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export async function POST(req: Request) {
|
|
117
|
+
const ip = req.headers.get('x-forwarded-for') ?? '127.0.0.1';
|
|
118
|
+
const { success } = await ratelimit.limit(ip);
|
|
119
|
+
|
|
120
|
+
if (!success) {
|
|
121
|
+
return new Response('Rate limit exceeded', { status: 429 });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Process AI request...
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Caching AI Responses
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { unstable_cache } from 'next/cache';
|
|
132
|
+
|
|
133
|
+
const getCachedSummary = unstable_cache(
|
|
134
|
+
async (documentId: string) => {
|
|
135
|
+
const result = await generateText({
|
|
136
|
+
model: anthropic('claude-sonnet-4-20250514'),
|
|
137
|
+
prompt: `Summarize document ${documentId}`,
|
|
138
|
+
});
|
|
139
|
+
return result.text;
|
|
140
|
+
},
|
|
141
|
+
['document-summary'],
|
|
142
|
+
{ revalidate: 3600 } // Cache for 1 hour
|
|
143
|
+
);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Rules
|
|
147
|
+
|
|
148
|
+
- Always stream long responses (better UX)
|
|
149
|
+
- Use structured output for data extraction
|
|
150
|
+
- Implement rate limiting on AI endpoints
|
|
151
|
+
- Cache deterministic AI responses
|
|
152
|
+
- Handle errors gracefully (retries, fallbacks)
|
|
153
|
+
- Log AI usage for cost monitoring
|
|
154
|
+
- Use environment variables for API keys
|
|
155
|
+
- Validate all AI outputs before using
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Server Components (React 19 / Next.js 15)
|
|
2
|
+
|
|
3
|
+
> Default to Server Components. Use Client Components only when necessary.
|
|
4
|
+
|
|
5
|
+
## When to Use Server Components
|
|
6
|
+
|
|
7
|
+
- Data fetching
|
|
8
|
+
- Accessing backend resources directly
|
|
9
|
+
- Keeping sensitive logic on server
|
|
10
|
+
- Large dependencies that shouldn't be in client bundle
|
|
11
|
+
- SEO-critical content
|
|
12
|
+
|
|
13
|
+
## When to Use Client Components
|
|
14
|
+
|
|
15
|
+
- Interactivity (onClick, onChange, etc.)
|
|
16
|
+
- Browser APIs (localStorage, geolocation)
|
|
17
|
+
- React hooks (useState, useEffect, useContext)
|
|
18
|
+
- Third-party libraries that use browser APIs
|
|
19
|
+
|
|
20
|
+
## Patterns
|
|
21
|
+
|
|
22
|
+
### Data Fetching in Server Components
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
// app/users/page.tsx (Server Component)
|
|
26
|
+
import { prisma } from '@/lib/prisma';
|
|
27
|
+
|
|
28
|
+
export default async function UsersPage() {
|
|
29
|
+
const users = await prisma.user.findMany({
|
|
30
|
+
orderBy: { createdAt: 'desc' },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div>
|
|
35
|
+
{users.map(user => (
|
|
36
|
+
<UserCard key={user.id} user={user} />
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Composition Pattern
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
// Server Component wrapper
|
|
47
|
+
async function UserList() {
|
|
48
|
+
const users = await getUsers();
|
|
49
|
+
return <UserListClient users={users} />;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Client Component for interactivity
|
|
53
|
+
'use client';
|
|
54
|
+
function UserListClient({ users }) {
|
|
55
|
+
const [selected, setSelected] = useState(null);
|
|
56
|
+
return (/* interactive UI */);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Passing Server Data to Client
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
// Server Component
|
|
64
|
+
export default async function Page() {
|
|
65
|
+
const data = await fetchData(); // Server-side fetch
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<ClientComponent
|
|
69
|
+
initialData={data} // Serializable data only
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Rules
|
|
76
|
+
|
|
77
|
+
- Never import Server Components into Client Components
|
|
78
|
+
- Keep 'use client' boundary as low as possible
|
|
79
|
+
- Don't pass functions as props across the boundary
|
|
80
|
+
- Use Server Actions for mutations, not API routes
|
|
81
|
+
- Async components are Server Components by default
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Server Actions (Next.js 15)
|
|
2
|
+
|
|
3
|
+
> Use Server Actions for mutations. They replace API routes for form submissions.
|
|
4
|
+
|
|
5
|
+
## Basic Pattern
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
// app/actions.ts
|
|
9
|
+
'use server';
|
|
10
|
+
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { revalidatePath } from 'next/cache';
|
|
13
|
+
|
|
14
|
+
const schema = z.object({
|
|
15
|
+
name: z.string().min(2),
|
|
16
|
+
email: z.string().email(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export async function createUser(formData: FormData) {
|
|
20
|
+
const data = schema.parse({
|
|
21
|
+
name: formData.get('name'),
|
|
22
|
+
email: formData.get('email'),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await db.user.create({ data });
|
|
26
|
+
revalidatePath('/users');
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## With useActionState (React 19)
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
'use client';
|
|
34
|
+
|
|
35
|
+
import { useActionState } from 'react';
|
|
36
|
+
import { createUser } from './actions';
|
|
37
|
+
|
|
38
|
+
type State = { error?: string; success?: boolean };
|
|
39
|
+
|
|
40
|
+
export function Form() {
|
|
41
|
+
const [state, formAction, isPending] = useActionState<State, FormData>(
|
|
42
|
+
async (prevState, formData) => {
|
|
43
|
+
try {
|
|
44
|
+
await createUser(formData);
|
|
45
|
+
return { success: true };
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return { error: e.message };
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<form action={formAction}>
|
|
55
|
+
<input name="name" required />
|
|
56
|
+
<input name="email" type="email" required />
|
|
57
|
+
{state.error && <p className="error">{state.error}</p>}
|
|
58
|
+
<button disabled={isPending}>
|
|
59
|
+
{isPending ? 'Creating...' : 'Create'}
|
|
60
|
+
</button>
|
|
61
|
+
</form>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Validation Pattern
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
'use server';
|
|
70
|
+
|
|
71
|
+
type ActionResult<T = void> =
|
|
72
|
+
| { success: true; data: T }
|
|
73
|
+
| { success: false; error: string; fieldErrors?: Record<string, string[]> };
|
|
74
|
+
|
|
75
|
+
export async function action(formData: FormData): Promise<ActionResult<{ id: string }>> {
|
|
76
|
+
const validated = schema.safeParse(Object.fromEntries(formData));
|
|
77
|
+
|
|
78
|
+
if (!validated.success) {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
error: 'Validation failed',
|
|
82
|
+
fieldErrors: validated.error.flatten().fieldErrors,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const result = await db.create({ data: validated.data });
|
|
88
|
+
return { success: true, data: { id: result.id } };
|
|
89
|
+
} catch {
|
|
90
|
+
return { success: false, error: 'Database error' };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Rules
|
|
96
|
+
|
|
97
|
+
- Always use 'use server' directive
|
|
98
|
+
- Validate ALL inputs with Zod
|
|
99
|
+
- Return structured results, not throw errors
|
|
100
|
+
- Use revalidatePath/revalidateTag for cache invalidation
|
|
101
|
+
- Keep actions in separate files for organization
|
|
102
|
+
- Actions can only receive serializable arguments
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Edge Middleware (Next.js 15)
|
|
2
|
+
|
|
3
|
+
> Middleware runs before every request. Use for auth, redirects, and headers.
|
|
4
|
+
|
|
5
|
+
## Basic Middleware
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// middleware.ts
|
|
9
|
+
import { NextResponse } from 'next/server';
|
|
10
|
+
import type { NextRequest } from 'next/server';
|
|
11
|
+
|
|
12
|
+
export function middleware(request: NextRequest) {
|
|
13
|
+
// Check auth
|
|
14
|
+
const token = request.cookies.get('session')?.value;
|
|
15
|
+
|
|
16
|
+
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
17
|
+
return NextResponse.redirect(new URL('/login', request.url));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return NextResponse.next();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const config = {
|
|
24
|
+
matcher: ['/dashboard/:path*', '/api/:path*'],
|
|
25
|
+
};
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## With Clerk Auth
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
|
|
32
|
+
|
|
33
|
+
const isPublicRoute = createRouteMatcher(['/', '/sign-in(.*)', '/api/webhooks(.*)']);
|
|
34
|
+
const isAdminRoute = createRouteMatcher(['/admin(.*)']);
|
|
35
|
+
|
|
36
|
+
export default clerkMiddleware(async (auth, req) => {
|
|
37
|
+
if (isPublicRoute(req)) return;
|
|
38
|
+
|
|
39
|
+
const { userId, sessionClaims } = await auth();
|
|
40
|
+
|
|
41
|
+
if (!userId) {
|
|
42
|
+
return Response.redirect(new URL('/sign-in', req.url));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (isAdminRoute(req) && sessionClaims?.role !== 'admin') {
|
|
46
|
+
return Response.redirect(new URL('/unauthorized', req.url));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Geolocation & A/B Testing
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
export function middleware(request: NextRequest) {
|
|
55
|
+
const country = request.geo?.country || 'US';
|
|
56
|
+
const response = NextResponse.next();
|
|
57
|
+
|
|
58
|
+
// Set header for downstream use
|
|
59
|
+
response.headers.set('x-user-country', country);
|
|
60
|
+
|
|
61
|
+
// A/B test assignment
|
|
62
|
+
const bucket = Math.random() < 0.5 ? 'control' : 'variant';
|
|
63
|
+
response.cookies.set('ab-bucket', bucket);
|
|
64
|
+
|
|
65
|
+
return response;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Rate Limiting Pattern
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { Ratelimit } from '@upstash/ratelimit';
|
|
73
|
+
import { Redis } from '@upstash/redis';
|
|
74
|
+
|
|
75
|
+
const ratelimit = new Ratelimit({
|
|
76
|
+
redis: Redis.fromEnv(),
|
|
77
|
+
limiter: Ratelimit.slidingWindow(10, '10 s'),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export async function middleware(request: NextRequest) {
|
|
81
|
+
const ip = request.ip ?? '127.0.0.1';
|
|
82
|
+
const { success, limit, remaining } = await ratelimit.limit(ip);
|
|
83
|
+
|
|
84
|
+
if (!success) {
|
|
85
|
+
return new NextResponse('Too Many Requests', {
|
|
86
|
+
status: 429,
|
|
87
|
+
headers: {
|
|
88
|
+
'X-RateLimit-Limit': limit.toString(),
|
|
89
|
+
'X-RateLimit-Remaining': remaining.toString(),
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return NextResponse.next();
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Rules
|
|
99
|
+
|
|
100
|
+
- Middleware runs on Edge Runtime (limited Node.js APIs)
|
|
101
|
+
- Keep middleware fast (<50ms)
|
|
102
|
+
- Use matcher to limit which routes trigger middleware
|
|
103
|
+
- Don't do heavy computation or database calls
|
|
104
|
+
- Use for: auth, redirects, headers, A/B tests, geolocation
|
|
105
|
+
- Avoid: data fetching, heavy validation, logging
|