ultra-dex 1.7.2 → 1.8.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 +40 -2
- package/assets/agents/0-orchestration/orchestrator.md +225 -0
- package/assets/agents/00-AGENT_INDEX.md +138 -0
- package/assets/agents/1-leadership/cto.md +186 -0
- package/assets/agents/1-leadership/planner.md +205 -0
- package/assets/agents/1-leadership/research.md +285 -0
- package/assets/agents/2-development/backend.md +472 -0
- package/assets/agents/2-development/database.md +516 -0
- package/assets/agents/2-development/frontend.md +144 -0
- package/assets/agents/3-security/auth.md +168 -0
- package/assets/agents/3-security/security.md +335 -0
- package/assets/agents/4-devops/devops.md +587 -0
- package/assets/agents/5-quality/debugger.md +188 -0
- package/assets/agents/5-quality/documentation.md +167 -0
- package/assets/agents/5-quality/reviewer.md +213 -0
- package/assets/agents/5-quality/testing.md +280 -0
- package/assets/agents/6-specialist/performance.md +323 -0
- package/assets/agents/6-specialist/refactoring.md +343 -0
- package/assets/agents/AGENT-INSTRUCTIONS.md +315 -0
- package/assets/agents/README.md +232 -0
- package/assets/cursor-rules/00-ultra-dex-core.mdc +48 -0
- package/assets/cursor-rules/01-database.mdc +50 -0
- package/assets/cursor-rules/02-api.mdc +81 -0
- package/assets/cursor-rules/03-auth.mdc +70 -0
- package/assets/cursor-rules/04-frontend.mdc +92 -0
- package/assets/cursor-rules/05-payments.mdc +88 -0
- package/assets/cursor-rules/06-testing.mdc +104 -0
- package/assets/cursor-rules/07-security.mdc +94 -0
- package/assets/cursor-rules/08-deployment.mdc +92 -0
- package/assets/cursor-rules/09-error-handling.mdc +137 -0
- package/assets/cursor-rules/10-performance.mdc +123 -0
- package/assets/cursor-rules/11-nextjs-v15.mdc +307 -0
- package/assets/cursor-rules/12-multi-tenancy.mdc +282 -0
- package/assets/cursor-rules/README.md +78 -0
- package/assets/cursor-rules/load.ps1 +108 -0
- package/assets/cursor-rules/load.sh +102 -0
- package/assets/docs/BUILD-AUTH-30M.md +113 -0
- package/assets/docs/CHECKLIST-21-STEP.md +86 -0
- package/assets/docs/CODEMAP.md +229 -0
- package/assets/docs/CUSTOMIZATION.md +127 -0
- package/assets/docs/LAUNCH-POSTS.md +238 -0
- package/assets/docs/QUICK-REFERENCE.md +338 -0
- package/assets/docs/README.md +21 -0
- package/assets/docs/ROADMAP.md +480 -0
- package/assets/docs/TROUBLESHOOTING.md +148 -0
- package/assets/docs/TUTORIAL.md +182 -0
- package/assets/docs/VERIFICATION.md +108 -0
- package/assets/docs/VISION-V2.md +187 -0
- package/assets/docs/WORKFLOW-DIAGRAMS.md +463 -0
- package/assets/docs/index.html +550 -0
- package/assets/live-templates/next15-prisma-clerk/.env.example +3 -0
- package/assets/live-templates/next15-prisma-clerk/README.md +10 -0
- package/assets/live-templates/next15-prisma-clerk/app/layout.tsx +7 -0
- package/assets/live-templates/next15-prisma-clerk/app/page.tsx +8 -0
- package/assets/live-templates/next15-prisma-clerk/next.config.js +6 -0
- package/assets/live-templates/next15-prisma-clerk/package.json +22 -0
- package/assets/live-templates/next15-prisma-clerk/prisma/schema.prisma +34 -0
- package/assets/live-templates/remix-supabase/.env.example +2 -0
- package/assets/live-templates/remix-supabase/README.md +9 -0
- package/assets/live-templates/remix-supabase/app/root.tsx +19 -0
- package/assets/live-templates/remix-supabase/app/routes/_index.tsx +8 -0
- package/assets/live-templates/remix-supabase/app/utils/supabase.server.ts +6 -0
- package/assets/live-templates/remix-supabase/package.json +20 -0
- package/assets/live-templates/remix-supabase/remix.config.js +6 -0
- package/assets/live-templates/sveltekit-drizzle/.env.example +1 -0
- package/assets/live-templates/sveltekit-drizzle/README.md +9 -0
- package/assets/live-templates/sveltekit-drizzle/drizzle/schema.ts +7 -0
- package/assets/live-templates/sveltekit-drizzle/drizzle.config.ts +5 -0
- package/assets/live-templates/sveltekit-drizzle/package.json +21 -0
- package/assets/live-templates/sveltekit-drizzle/src/lib/db.ts +5 -0
- package/assets/live-templates/sveltekit-drizzle/src/routes/+page.svelte +2 -0
- package/assets/live-templates/sveltekit-drizzle/svelte.config.js +5 -0
- package/assets/live-templates/sveltekit-drizzle/vite.config.js +5 -0
- package/assets/saas-plan/04-Imp-Template.md +5546 -0
- package/assets/templates/CASE-STUDY-TEMPLATE.md +139 -0
- package/assets/templates/MASTER-PLAN-TEMPLATE.md +647 -0
- package/assets/templates/ORDER-TRACKER-TEMPLATE.md +731 -0
- package/assets/templates/PHASE-TRACKER-TEMPLATE.md +577 -0
- package/assets/templates/README.md +419 -0
- package/bin/ultra-dex.js +643 -29
- package/package.json +3 -3
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# Next.js 15 Production Patterns
|
|
2
|
+
|
|
3
|
+
> Modern App Router patterns for production-grade Next.js 15 applications
|
|
4
|
+
|
|
5
|
+
## Architecture Rules
|
|
6
|
+
|
|
7
|
+
1. **ALWAYS use App Router** - `src/app/` directory structure
|
|
8
|
+
2. **Route Groups** for organization - `(marketing)/`, `(dashboard)/`, `(auth)/`
|
|
9
|
+
3. **Parallel Routes** for complex layouts - `@modal`, `@sidebar`
|
|
10
|
+
4. **Intercepting Routes** for modals - `(.)photo/[id]`
|
|
11
|
+
|
|
12
|
+
## File Structure
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
src/app/
|
|
16
|
+
├── (marketing)/
|
|
17
|
+
│ ├── page.tsx # Landing page
|
|
18
|
+
│ ├── pricing/page.tsx
|
|
19
|
+
│ └── layout.tsx # Marketing layout
|
|
20
|
+
├── (dashboard)/
|
|
21
|
+
│ ├── dashboard/
|
|
22
|
+
│ │ ├── page.tsx # Main dashboard
|
|
23
|
+
│ │ ├── loading.tsx # Suspense fallback
|
|
24
|
+
│ │ └── error.tsx # Error boundary
|
|
25
|
+
│ ├── settings/page.tsx
|
|
26
|
+
│ └── layout.tsx # Dashboard layout with sidebar
|
|
27
|
+
├── (auth)/
|
|
28
|
+
│ ├── login/page.tsx
|
|
29
|
+
│ ├── signup/page.tsx
|
|
30
|
+
│ └── layout.tsx # Auth layout (no navbar)
|
|
31
|
+
├── api/
|
|
32
|
+
│ └── [...]/route.ts # API routes
|
|
33
|
+
├── layout.tsx # Root layout
|
|
34
|
+
└── globals.css
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Server Components (Default)
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// This is a Server Component by default
|
|
41
|
+
export default async function DashboardPage() {
|
|
42
|
+
const data = await prisma.user.findMany()
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div>
|
|
46
|
+
{data.map(user => <UserCard key={user.id} user={user} />)}
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Client Components (When Needed)
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
'use client'
|
|
56
|
+
|
|
57
|
+
import { useState } from 'react'
|
|
58
|
+
|
|
59
|
+
export function Counter() {
|
|
60
|
+
const [count, setCount] = useState(0)
|
|
61
|
+
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Server Actions
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
// app/actions.ts
|
|
69
|
+
'use server'
|
|
70
|
+
|
|
71
|
+
import { revalidatePath } from 'next/cache'
|
|
72
|
+
import { redirect } from 'next/navigation'
|
|
73
|
+
|
|
74
|
+
export async function createPost(formData: FormData) {
|
|
75
|
+
const title = formData.get('title') as string
|
|
76
|
+
|
|
77
|
+
await prisma.post.create({ data: { title } })
|
|
78
|
+
|
|
79
|
+
revalidatePath('/posts')
|
|
80
|
+
redirect('/posts')
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Loading States
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// app/dashboard/loading.tsx
|
|
88
|
+
export default function Loading() {
|
|
89
|
+
return (
|
|
90
|
+
<div className="animate-pulse">
|
|
91
|
+
<div className="h-8 bg-gray-200 rounded w-1/4 mb-4" />
|
|
92
|
+
<div className="h-4 bg-gray-200 rounded w-full mb-2" />
|
|
93
|
+
<div className="h-4 bg-gray-200 rounded w-3/4" />
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Error Boundaries
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
// app/dashboard/error.tsx
|
|
103
|
+
'use client'
|
|
104
|
+
|
|
105
|
+
export default function Error({
|
|
106
|
+
error,
|
|
107
|
+
reset,
|
|
108
|
+
}: {
|
|
109
|
+
error: Error & { digest?: string }
|
|
110
|
+
reset: () => void
|
|
111
|
+
}) {
|
|
112
|
+
return (
|
|
113
|
+
<div className="p-4 bg-red-50 rounded-lg">
|
|
114
|
+
<h2 className="text-red-800 font-semibold">Something went wrong!</h2>
|
|
115
|
+
<button
|
|
116
|
+
onClick={() => reset()}
|
|
117
|
+
className="mt-2 px-4 py-2 bg-red-600 text-white rounded"
|
|
118
|
+
>
|
|
119
|
+
Try again
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Middleware for Auth/Tenant
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
// middleware.ts
|
|
130
|
+
import { NextResponse } from 'next/server'
|
|
131
|
+
import type { NextRequest } from 'next/server'
|
|
132
|
+
|
|
133
|
+
export function middleware(request: NextRequest) {
|
|
134
|
+
// Get tenant from subdomain or header
|
|
135
|
+
const hostname = request.headers.get('host') || ''
|
|
136
|
+
const subdomain = hostname.split('.')[0]
|
|
137
|
+
|
|
138
|
+
// Add tenant to headers for downstream use
|
|
139
|
+
const response = NextResponse.next()
|
|
140
|
+
response.headers.set('x-tenant-id', subdomain)
|
|
141
|
+
|
|
142
|
+
// Auth check
|
|
143
|
+
const session = request.cookies.get('session')
|
|
144
|
+
if (!session && request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
145
|
+
return NextResponse.redirect(new URL('/login', request.url))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return response
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const config = {
|
|
152
|
+
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Streaming with Suspense
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
import { Suspense } from 'react'
|
|
160
|
+
|
|
161
|
+
export default function Dashboard() {
|
|
162
|
+
return (
|
|
163
|
+
<div>
|
|
164
|
+
<h1>Dashboard</h1>
|
|
165
|
+
|
|
166
|
+
{/* This loads immediately */}
|
|
167
|
+
<StaticContent />
|
|
168
|
+
|
|
169
|
+
{/* This streams in when ready */}
|
|
170
|
+
<Suspense fallback={<LoadingStats />}>
|
|
171
|
+
<AsyncStats />
|
|
172
|
+
</Suspense>
|
|
173
|
+
|
|
174
|
+
{/* This streams separately */}
|
|
175
|
+
<Suspense fallback={<LoadingChart />}>
|
|
176
|
+
<AsyncChart />
|
|
177
|
+
</Suspense>
|
|
178
|
+
</div>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Metadata API
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
// app/dashboard/page.tsx
|
|
187
|
+
import { Metadata } from 'next'
|
|
188
|
+
|
|
189
|
+
export const metadata: Metadata = {
|
|
190
|
+
title: 'Dashboard | MyApp',
|
|
191
|
+
description: 'Your personal dashboard',
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Dynamic metadata
|
|
195
|
+
export async function generateMetadata({ params }): Promise<Metadata> {
|
|
196
|
+
const user = await getUser(params.id)
|
|
197
|
+
return {
|
|
198
|
+
title: `${user.name}'s Profile`,
|
|
199
|
+
openGraph: {
|
|
200
|
+
images: [user.avatar],
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Data Fetching Patterns
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
// Parallel data fetching
|
|
210
|
+
async function Dashboard() {
|
|
211
|
+
// These run in parallel
|
|
212
|
+
const [user, posts, stats] = await Promise.all([
|
|
213
|
+
getUser(),
|
|
214
|
+
getPosts(),
|
|
215
|
+
getStats(),
|
|
216
|
+
])
|
|
217
|
+
|
|
218
|
+
return <DashboardView user={user} posts={posts} stats={stats} />
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Sequential (when dependent)
|
|
222
|
+
async function Profile({ params }) {
|
|
223
|
+
const user = await getUser(params.id)
|
|
224
|
+
const posts = await getPostsByUser(user.id) // Needs user.id
|
|
225
|
+
|
|
226
|
+
return <ProfileView user={user} posts={posts} />
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Route Handlers (API)
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
// app/api/users/route.ts
|
|
234
|
+
import { NextResponse } from 'next/server'
|
|
235
|
+
|
|
236
|
+
export async function GET(request: Request) {
|
|
237
|
+
const users = await prisma.user.findMany()
|
|
238
|
+
return NextResponse.json(users)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function POST(request: Request) {
|
|
242
|
+
const body = await request.json()
|
|
243
|
+
const user = await prisma.user.create({ data: body })
|
|
244
|
+
return NextResponse.json(user, { status: 201 })
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Multi-Tenancy Pattern
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
// lib/tenant.ts
|
|
252
|
+
import { headers } from 'next/headers'
|
|
253
|
+
|
|
254
|
+
export async function getTenant() {
|
|
255
|
+
const headersList = headers()
|
|
256
|
+
const tenantId = headersList.get('x-tenant-id')
|
|
257
|
+
|
|
258
|
+
if (!tenantId) throw new Error('No tenant')
|
|
259
|
+
|
|
260
|
+
return prisma.organization.findUniqueOrThrow({
|
|
261
|
+
where: { subdomain: tenantId },
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Usage in any server component
|
|
266
|
+
export default async function Dashboard() {
|
|
267
|
+
const tenant = await getTenant()
|
|
268
|
+
const data = await prisma.post.findMany({
|
|
269
|
+
where: { organizationId: tenant.id },
|
|
270
|
+
})
|
|
271
|
+
// ...
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Vercel AI SDK Integration
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
// app/api/chat/route.ts
|
|
279
|
+
import { openai } from '@ai-sdk/openai'
|
|
280
|
+
import { streamText } from 'ai'
|
|
281
|
+
|
|
282
|
+
export async function POST(req: Request) {
|
|
283
|
+
const { messages } = await req.json()
|
|
284
|
+
|
|
285
|
+
const result = await streamText({
|
|
286
|
+
model: openai('gpt-4-turbo'),
|
|
287
|
+
messages,
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
return result.toDataStreamResponse()
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Production Checklist
|
|
295
|
+
|
|
296
|
+
- [ ] All routes have `loading.tsx`
|
|
297
|
+
- [ ] All routes have `error.tsx`
|
|
298
|
+
- [ ] Metadata on all pages
|
|
299
|
+
- [ ] Server Actions for mutations
|
|
300
|
+
- [ ] Proper auth middleware
|
|
301
|
+
- [ ] Multi-tenant isolation (if applicable)
|
|
302
|
+
- [ ] Streaming for slow data
|
|
303
|
+
- [ ] Edge runtime where beneficial
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
*Ultra-Dex v1.7.0 - Next.js 15 Production Patterns*
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# SaaS Multi-Tenancy Patterns
|
|
2
|
+
|
|
3
|
+
> Patterns for building secure multi-tenant SaaS applications
|
|
4
|
+
|
|
5
|
+
## Core Principles
|
|
6
|
+
|
|
7
|
+
1. **Tenant Isolation** - Data from one tenant NEVER visible to another
|
|
8
|
+
2. **Consistent Filtering** - ALL queries must include tenant context
|
|
9
|
+
3. **Defense in Depth** - Multiple layers of tenant validation
|
|
10
|
+
|
|
11
|
+
## Tenant Identification
|
|
12
|
+
|
|
13
|
+
### Subdomain-Based (Recommended for SaaS)
|
|
14
|
+
```typescript
|
|
15
|
+
// middleware.ts
|
|
16
|
+
import { NextResponse } from 'next/server'
|
|
17
|
+
import type { NextRequest } from 'next/server'
|
|
18
|
+
|
|
19
|
+
export function middleware(request: NextRequest) {
|
|
20
|
+
const hostname = request.headers.get('host') || ''
|
|
21
|
+
const subdomain = hostname.split('.')[0]
|
|
22
|
+
|
|
23
|
+
// Skip for main domain
|
|
24
|
+
if (subdomain === 'www' || subdomain === 'app') {
|
|
25
|
+
return NextResponse.next()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const response = NextResponse.next()
|
|
29
|
+
response.headers.set('x-tenant-id', subdomain)
|
|
30
|
+
return response
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Header-Based (API Clients)
|
|
35
|
+
```typescript
|
|
36
|
+
// For API-first architectures
|
|
37
|
+
const tenantId = request.headers.get('x-tenant-id')
|
|
38
|
+
if (!tenantId) {
|
|
39
|
+
return Response.json({ error: 'Tenant ID required' }, { status: 400 })
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### User Association (After Auth)
|
|
44
|
+
```typescript
|
|
45
|
+
// Get tenant from authenticated user
|
|
46
|
+
const session = await getSession()
|
|
47
|
+
const tenantId = session?.user?.organizationId
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Database Patterns
|
|
51
|
+
|
|
52
|
+
### Row-Level Security (Recommended)
|
|
53
|
+
|
|
54
|
+
**Prisma Schema:**
|
|
55
|
+
```prisma
|
|
56
|
+
model Organization {
|
|
57
|
+
id String @id @default(cuid())
|
|
58
|
+
subdomain String @unique
|
|
59
|
+
name String
|
|
60
|
+
users User[]
|
|
61
|
+
posts Post[]
|
|
62
|
+
// All tenant-owned data has organizationId
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
model User {
|
|
66
|
+
id String @id @default(cuid())
|
|
67
|
+
email String @unique
|
|
68
|
+
organization Organization @relation(fields: [organizationId], references: [id])
|
|
69
|
+
organizationId String
|
|
70
|
+
posts Post[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
model Post {
|
|
74
|
+
id String @id @default(cuid())
|
|
75
|
+
title String
|
|
76
|
+
content String
|
|
77
|
+
organization Organization @relation(fields: [organizationId], references: [id])
|
|
78
|
+
organizationId String
|
|
79
|
+
author User @relation(fields: [authorId], references: [id])
|
|
80
|
+
authorId String
|
|
81
|
+
|
|
82
|
+
@@index([organizationId]) // ALWAYS index tenant column
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Query Pattern (ALWAYS include organizationId):**
|
|
87
|
+
```typescript
|
|
88
|
+
// lib/db.ts - Tenant-scoped client
|
|
89
|
+
export function getTenantDb(organizationId: string) {
|
|
90
|
+
return {
|
|
91
|
+
post: {
|
|
92
|
+
findMany: (args?: Prisma.PostFindManyArgs) =>
|
|
93
|
+
prisma.post.findMany({
|
|
94
|
+
...args,
|
|
95
|
+
where: { ...args?.where, organizationId }
|
|
96
|
+
}),
|
|
97
|
+
create: (data: Prisma.PostCreateInput) =>
|
|
98
|
+
prisma.post.create({
|
|
99
|
+
data: { ...data, organizationId }
|
|
100
|
+
}),
|
|
101
|
+
// ... other methods
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Usage
|
|
107
|
+
const db = getTenantDb(session.user.organizationId)
|
|
108
|
+
const posts = await db.post.findMany() // Auto-filtered
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### PostgreSQL Row-Level Security (RLS)
|
|
112
|
+
|
|
113
|
+
```sql
|
|
114
|
+
-- Enable RLS
|
|
115
|
+
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
|
|
116
|
+
|
|
117
|
+
-- Policy: Users can only see their org's posts
|
|
118
|
+
CREATE POLICY posts_tenant_isolation ON posts
|
|
119
|
+
USING (organization_id = current_setting('app.current_tenant')::text);
|
|
120
|
+
|
|
121
|
+
-- Set tenant context in your app
|
|
122
|
+
SET app.current_tenant = 'org_123';
|
|
123
|
+
SELECT * FROM posts; -- Only org_123 posts returned
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Schema-Per-Tenant (Enterprise)
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// For highest isolation (enterprise customers)
|
|
130
|
+
const schema = `tenant_${tenantId}`
|
|
131
|
+
|
|
132
|
+
// Prisma with schema
|
|
133
|
+
const prisma = new PrismaClient({
|
|
134
|
+
datasources: {
|
|
135
|
+
db: {
|
|
136
|
+
url: `${DATABASE_URL}?schema=${schema}`
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Middleware Pattern
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// lib/tenant.ts
|
|
146
|
+
import { headers } from 'next/headers'
|
|
147
|
+
import { cache } from 'react'
|
|
148
|
+
|
|
149
|
+
export const getTenant = cache(async () => {
|
|
150
|
+
const headersList = headers()
|
|
151
|
+
const tenantId = headersList.get('x-tenant-id')
|
|
152
|
+
|
|
153
|
+
if (!tenantId) {
|
|
154
|
+
throw new Error('Tenant context required')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const tenant = await prisma.organization.findUnique({
|
|
158
|
+
where: { subdomain: tenantId }
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
if (!tenant) {
|
|
162
|
+
throw new Error('Tenant not found')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return tenant
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Usage in any Server Component or API route
|
|
169
|
+
export default async function Dashboard() {
|
|
170
|
+
const tenant = await getTenant()
|
|
171
|
+
const posts = await prisma.post.findMany({
|
|
172
|
+
where: { organizationId: tenant.id }
|
|
173
|
+
})
|
|
174
|
+
// ...
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## API Route Pattern
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// app/api/posts/route.ts
|
|
182
|
+
import { getTenant } from '@/lib/tenant'
|
|
183
|
+
import { getSession } from '@/lib/auth'
|
|
184
|
+
|
|
185
|
+
export async function GET() {
|
|
186
|
+
const session = await getSession()
|
|
187
|
+
if (!session) {
|
|
188
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const tenant = await getTenant()
|
|
192
|
+
|
|
193
|
+
// Verify user belongs to this tenant
|
|
194
|
+
if (session.user.organizationId !== tenant.id) {
|
|
195
|
+
return Response.json({ error: 'Forbidden' }, { status: 403 })
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const posts = await prisma.post.findMany({
|
|
199
|
+
where: { organizationId: tenant.id }
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
return Response.json(posts)
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Testing Multi-Tenancy
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// tests/multi-tenancy.test.ts
|
|
210
|
+
describe('Tenant Isolation', () => {
|
|
211
|
+
it('should not leak data between tenants', async () => {
|
|
212
|
+
// Create data for tenant A
|
|
213
|
+
const tenantA = await createTenant('tenant-a')
|
|
214
|
+
const postA = await createPost({
|
|
215
|
+
title: 'Secret',
|
|
216
|
+
organizationId: tenantA.id
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// Create tenant B
|
|
220
|
+
const tenantB = await createTenant('tenant-b')
|
|
221
|
+
|
|
222
|
+
// Query as tenant B - should NOT see tenant A's post
|
|
223
|
+
const postsForB = await prisma.post.findMany({
|
|
224
|
+
where: { organizationId: tenantB.id }
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
expect(postsForB).not.toContainEqual(
|
|
228
|
+
expect.objectContaining({ id: postA.id })
|
|
229
|
+
)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should prevent cross-tenant access via API', async () => {
|
|
233
|
+
const response = await fetch('/api/posts', {
|
|
234
|
+
headers: {
|
|
235
|
+
'x-tenant-id': 'wrong-tenant',
|
|
236
|
+
'Authorization': `Bearer ${validToken}`
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
expect(response.status).toBe(403)
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Checklist
|
|
246
|
+
|
|
247
|
+
- [ ] All database tables have `organizationId` column
|
|
248
|
+
- [ ] All queries filter by `organizationId`
|
|
249
|
+
- [ ] Middleware extracts tenant from subdomain/header
|
|
250
|
+
- [ ] API routes verify user belongs to tenant
|
|
251
|
+
- [ ] Indexes on `organizationId` columns
|
|
252
|
+
- [ ] Cross-tenant access tests written
|
|
253
|
+
- [ ] No raw SQL without tenant filter
|
|
254
|
+
- [ ] Admin routes have additional protection
|
|
255
|
+
|
|
256
|
+
## Common Mistakes
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// ❌ BAD - No tenant filter
|
|
260
|
+
const posts = await prisma.post.findMany()
|
|
261
|
+
|
|
262
|
+
// ✅ GOOD - Always filter
|
|
263
|
+
const posts = await prisma.post.findMany({
|
|
264
|
+
where: { organizationId: tenant.id }
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// ❌ BAD - Trusting client-provided tenant ID for data access
|
|
268
|
+
const { tenantId } = req.body
|
|
269
|
+
const posts = await prisma.post.findMany({
|
|
270
|
+
where: { organizationId: tenantId }
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// ✅ GOOD - Use server-verified tenant context
|
|
274
|
+
const tenant = await getTenant() // From middleware/session
|
|
275
|
+
const posts = await prisma.post.findMany({
|
|
276
|
+
where: { organizationId: tenant.id }
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
*Ultra-Dex v1.7.1 - Multi-Tenancy Patterns for Production SaaS*
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Ultra-Dex Cursor Rules
|
|
2
|
+
|
|
3
|
+
> Modular AI rules for Cursor, Copilot, and other AI coding assistants.
|
|
4
|
+
|
|
5
|
+
## What is This?
|
|
6
|
+
|
|
7
|
+
The 34-section Ultra-Dex template, atomized into small, focused rule files. Each file is under 200 lines and optimized for AI context windows.
|
|
8
|
+
|
|
9
|
+
## How to Use
|
|
10
|
+
|
|
11
|
+
### Option 1: Copy to `.cursor/rules/`
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# In your project root
|
|
15
|
+
mkdir -p .cursor/rules
|
|
16
|
+
cp path/to/ultra-dex/cursor-rules/*.mdc .cursor/rules/
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Option 2: Reference in System Prompt
|
|
20
|
+
|
|
21
|
+
Paste the relevant rule into your AI assistant's system prompt when working on that domain.
|
|
22
|
+
|
|
23
|
+
### Option 3: Selective Loading
|
|
24
|
+
|
|
25
|
+
Only load rules relevant to your current task:
|
|
26
|
+
|
|
27
|
+
| Working On | Load These |
|
|
28
|
+
|------------|------------|
|
|
29
|
+
| Database schema | `00-ultra-dex-core.mdc` + `01-database.mdc` |
|
|
30
|
+
| API endpoints | `00-ultra-dex-core.mdc` + `02-api.mdc` |
|
|
31
|
+
| Authentication | `00-ultra-dex-core.mdc` + `03-auth.mdc` |
|
|
32
|
+
| Frontend components | `00-ultra-dex-core.mdc` + `04-frontend.mdc` |
|
|
33
|
+
| Payments | `00-ultra-dex-core.mdc` + `05-payments.mdc` |
|
|
34
|
+
| Testing | `00-ultra-dex-core.mdc` + `06-testing.mdc` |
|
|
35
|
+
| Security review | `00-ultra-dex-core.mdc` + `07-security.mdc` |
|
|
36
|
+
| Deployment | `00-ultra-dex-core.mdc` + `08-deployment.mdc` |
|
|
37
|
+
| Error handling | `00-ultra-dex-core.mdc` + `09-error-handling.mdc` |
|
|
38
|
+
| Performance | `00-ultra-dex-core.mdc` + `10-performance.mdc` |
|
|
39
|
+
| Next.js 15 app | `00-ultra-dex-core.mdc` + `11-nextjs-v15.mdc` |
|
|
40
|
+
| Multi-tenant SaaS | `00-ultra-dex-core.mdc` + `12-multi-tenancy.mdc` |
|
|
41
|
+
|
|
42
|
+
## Files
|
|
43
|
+
|
|
44
|
+
| File | Lines | Purpose |
|
|
45
|
+
|------|-------|---------|
|
|
46
|
+
| `00-ultra-dex-core.mdc` | ~60 | Base rules (always load) |
|
|
47
|
+
| `01-database.mdc` | ~70 | Prisma, schema, queries |
|
|
48
|
+
| `02-api.mdc` | ~100 | API routes, validation, responses |
|
|
49
|
+
| `03-auth.mdc` | ~70 | NextAuth configuration |
|
|
50
|
+
| `04-frontend.mdc` | ~100 | React, components, state |
|
|
51
|
+
| `05-payments.mdc` | ~90 | Stripe integration |
|
|
52
|
+
| `06-testing.mdc` | ~100 | Vitest, Playwright |
|
|
53
|
+
| `07-security.mdc` | ~100 | Input validation, auth, headers |
|
|
54
|
+
| `08-deployment.mdc` | ~90 | Vercel, CI/CD, migrations |
|
|
55
|
+
| `09-error-handling.mdc` | ~100 | Error patterns, logging |
|
|
56
|
+
| `10-performance.mdc` | ~100 | Optimization, caching |
|
|
57
|
+
| `11-nextjs-v15.mdc` | ~200 | Next.js 15 App Router patterns |
|
|
58
|
+
| `12-multi-tenancy.mdc` | ~200 | SaaS multi-tenant patterns |
|
|
59
|
+
|
|
60
|
+
## Why Modular?
|
|
61
|
+
|
|
62
|
+
1. **AI Context Limits**: LLMs perform better with focused context (<500 lines)
|
|
63
|
+
2. **"Lost in the Middle"**: Long contexts degrade AI attention on middle content
|
|
64
|
+
3. **Relevance**: Load only what you need for the current task
|
|
65
|
+
4. **Maintainability**: Update one domain without touching others
|
|
66
|
+
|
|
67
|
+
## Customization
|
|
68
|
+
|
|
69
|
+
These are starting points. Customize for your stack:
|
|
70
|
+
|
|
71
|
+
- Using Supabase instead of Prisma? Modify `01-database.mdc`
|
|
72
|
+
- Using Clerk instead of NextAuth? Replace `03-auth.mdc`
|
|
73
|
+
- Using Paddle instead of Stripe? Replace `05-payments.mdc`
|
|
74
|
+
|
|
75
|
+
## Full Template
|
|
76
|
+
|
|
77
|
+
For the complete 34-section template with all details:
|
|
78
|
+
- [04-Imp-Template.md](../@%20Ultra%20DeX/Saas%20plan/04-Imp-Template.md)
|