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.
Files changed (87) hide show
  1. package/README.md +112 -151
  2. package/assets/agents/00-AGENT_INDEX.md +1 -1
  3. package/assets/code-patterns/clerk-middleware.ts +138 -0
  4. package/assets/code-patterns/prisma-schema.prisma +224 -0
  5. package/assets/code-patterns/rls-policies.sql +246 -0
  6. package/assets/code-patterns/server-actions.ts +191 -0
  7. package/assets/code-patterns/trpc-router.ts +258 -0
  8. package/assets/cursor-rules/13-ai-integration.mdc +155 -0
  9. package/assets/cursor-rules/14-server-components.mdc +81 -0
  10. package/assets/cursor-rules/15-server-actions.mdc +102 -0
  11. package/assets/cursor-rules/16-edge-middleware.mdc +105 -0
  12. package/assets/cursor-rules/17-streaming-ssr.mdc +138 -0
  13. package/assets/docs/LAUNCH-POSTS.md +1 -1
  14. package/assets/docs/QUICK-REFERENCE.md +9 -4
  15. package/assets/docs/VISION-V2.md +1 -1
  16. package/assets/hooks/pre-commit +98 -0
  17. package/assets/saas-plan/04-Imp-Template.md +1 -1
  18. package/bin/ultra-dex.js +132 -4
  19. package/lib/commands/advanced.js +471 -0
  20. package/lib/commands/agent-builder.js +226 -0
  21. package/lib/commands/agents.js +102 -42
  22. package/lib/commands/auto-implement.js +68 -0
  23. package/lib/commands/banner.js +43 -21
  24. package/lib/commands/build.js +78 -183
  25. package/lib/commands/ci-monitor.js +84 -0
  26. package/lib/commands/config.js +207 -0
  27. package/lib/commands/dashboard.js +770 -0
  28. package/lib/commands/diff.js +233 -0
  29. package/lib/commands/doctor.js +416 -0
  30. package/lib/commands/export.js +408 -0
  31. package/lib/commands/fix.js +96 -0
  32. package/lib/commands/generate.js +105 -78
  33. package/lib/commands/hooks.js +251 -76
  34. package/lib/commands/init.js +102 -54
  35. package/lib/commands/memory.js +80 -0
  36. package/lib/commands/plan.js +82 -0
  37. package/lib/commands/review.js +34 -5
  38. package/lib/commands/run.js +233 -0
  39. package/lib/commands/scaffold.js +151 -0
  40. package/lib/commands/serve.js +179 -146
  41. package/lib/commands/state.js +327 -0
  42. package/lib/commands/swarm.js +306 -0
  43. package/lib/commands/sync.js +82 -23
  44. package/lib/commands/team.js +275 -0
  45. package/lib/commands/upgrade.js +190 -0
  46. package/lib/commands/validate.js +34 -0
  47. package/lib/commands/verify.js +81 -0
  48. package/lib/commands/watch.js +79 -0
  49. package/lib/config/theme.js +47 -0
  50. package/lib/mcp/graph.js +92 -0
  51. package/lib/mcp/memory.js +95 -0
  52. package/lib/mcp/resources.js +152 -0
  53. package/lib/mcp/server.js +34 -0
  54. package/lib/mcp/tools.js +481 -0
  55. package/lib/mcp/websocket.js +117 -0
  56. package/lib/providers/index.js +49 -4
  57. package/lib/providers/ollama.js +136 -0
  58. package/lib/providers/router.js +63 -0
  59. package/lib/quality/scanner.js +128 -0
  60. package/lib/swarm/coordinator.js +97 -0
  61. package/lib/swarm/index.js +598 -0
  62. package/lib/swarm/protocol.js +677 -0
  63. package/lib/swarm/tiers.js +485 -0
  64. package/lib/templates/code/clerk-middleware.ts +138 -0
  65. package/lib/templates/code/prisma-schema.prisma +224 -0
  66. package/lib/templates/code/rls-policies.sql +246 -0
  67. package/lib/templates/code/server-actions.ts +191 -0
  68. package/lib/templates/code/trpc-router.ts +258 -0
  69. package/lib/templates/custom-agent.md +10 -0
  70. package/lib/themes/doomsday.js +229 -0
  71. package/lib/ui/index.js +5 -0
  72. package/lib/ui/interface.js +241 -0
  73. package/lib/ui/spinners.js +116 -0
  74. package/lib/ui/theme.js +183 -0
  75. package/lib/utils/agents.js +32 -0
  76. package/lib/utils/files.js +14 -0
  77. package/lib/utils/graph.js +108 -0
  78. package/lib/utils/help.js +64 -0
  79. package/lib/utils/messages.js +35 -0
  80. package/lib/utils/progress.js +24 -0
  81. package/lib/utils/prompts.js +47 -0
  82. package/lib/utils/spinners.js +46 -0
  83. package/lib/utils/status.js +31 -0
  84. package/lib/utils/tables.js +41 -0
  85. package/lib/utils/theme-state.js +9 -0
  86. package/lib/utils/version-display.js +32 -0
  87. 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