create-solostack 1.1.0 → 1.2.1
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/package.json +1 -1
- package/src/constants.js +4 -0
- package/src/generators/auth.js +305 -1
- package/src/generators/base.js +268 -102
- package/src/generators/database.js +130 -54
- package/src/index.js +8 -8
package/package.json
CHANGED
package/src/constants.js
CHANGED
|
@@ -26,6 +26,8 @@ export const PACKAGE_VERSIONS = {
|
|
|
26
26
|
'next-auth': '^5.0.0-beta.25',
|
|
27
27
|
bcryptjs: '^2.4.3',
|
|
28
28
|
'@types/bcryptjs': '^2.4.6',
|
|
29
|
+
'@supabase/ssr': '^0.0.10',
|
|
30
|
+
'@supabase/supabase-js': '^2.39.0',
|
|
29
31
|
|
|
30
32
|
// Payments
|
|
31
33
|
stripe: '^17.5.0',
|
|
@@ -70,6 +72,7 @@ export const DEFAULT_CONFIG = {
|
|
|
70
72
|
*/
|
|
71
73
|
export const AUTH_PROVIDERS = [
|
|
72
74
|
'NextAuth.js (Email + OAuth)',
|
|
75
|
+
'Supabase Auth',
|
|
73
76
|
];
|
|
74
77
|
|
|
75
78
|
/**
|
|
@@ -77,6 +80,7 @@ export const AUTH_PROVIDERS = [
|
|
|
77
80
|
*/
|
|
78
81
|
export const DATABASES = [
|
|
79
82
|
'PostgreSQL + Prisma',
|
|
83
|
+
'Supabase',
|
|
80
84
|
];
|
|
81
85
|
|
|
82
86
|
/**
|
package/src/generators/auth.js
CHANGED
|
@@ -2,11 +2,315 @@ import path from 'path';
|
|
|
2
2
|
import { writeFile, ensureDir } from '../utils/files.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Generates NextAuth
|
|
5
|
+
* Generates authentication configuration (NextAuth or Supabase)
|
|
6
6
|
* @param {string} projectPath - Path where the project is located
|
|
7
7
|
* @param {string} authProvider - Auth provider type
|
|
8
8
|
*/
|
|
9
9
|
export async function generateAuth(projectPath, authProvider) {
|
|
10
|
+
if (authProvider === 'Supabase Auth') {
|
|
11
|
+
await generateSupabaseAuth(projectPath);
|
|
12
|
+
} else {
|
|
13
|
+
await generateNextAuth(projectPath);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function generateSupabaseAuth(projectPath) {
|
|
18
|
+
// Create utils directory
|
|
19
|
+
await ensureDir(path.join(projectPath, 'src/utils/supabase'));
|
|
20
|
+
|
|
21
|
+
// Create auth directories
|
|
22
|
+
await ensureDir(path.join(projectPath, 'src/app/auth/callback'));
|
|
23
|
+
await ensureDir(path.join(projectPath, 'src/app/login'));
|
|
24
|
+
await ensureDir(path.join(projectPath, 'src/app/private'));
|
|
25
|
+
|
|
26
|
+
// 1. Client Utility
|
|
27
|
+
const clientUtil = `import { createBrowserClient } from '@supabase/ssr'
|
|
28
|
+
|
|
29
|
+
export function createClient() {
|
|
30
|
+
return createBrowserClient(
|
|
31
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
32
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
await writeFile(path.join(projectPath, 'src/utils/supabase/client.ts'), clientUtil);
|
|
37
|
+
|
|
38
|
+
// 2. Server Utility
|
|
39
|
+
const serverUtil = `import { createServerClient, type CookieOptions } from '@supabase/ssr'
|
|
40
|
+
import { cookies } from 'next/headers'
|
|
41
|
+
|
|
42
|
+
export function createClient(cookieStore: ReturnType<typeof cookies>) {
|
|
43
|
+
return createServerClient(
|
|
44
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
45
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
46
|
+
{
|
|
47
|
+
cookies: {
|
|
48
|
+
get(name: string) {
|
|
49
|
+
return cookieStore.get(name)?.value
|
|
50
|
+
},
|
|
51
|
+
set(name: string, value: string, options: CookieOptions) {
|
|
52
|
+
try {
|
|
53
|
+
cookieStore.set({ name, value, ...options })
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// The \`set\` method was called from a Server Component.
|
|
56
|
+
// This can be ignored if you have middleware refreshing
|
|
57
|
+
// user sessions.
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
remove(name: string, options: CookieOptions) {
|
|
61
|
+
try {
|
|
62
|
+
cookieStore.set({ name, value: '', ...options })
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// The \`delete\` method was called from a Server Component.
|
|
65
|
+
// This can be ignored if you have middleware refreshing
|
|
66
|
+
// user sessions.
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
await writeFile(path.join(projectPath, 'src/utils/supabase/server.ts'), serverUtil);
|
|
75
|
+
|
|
76
|
+
// 3. MacOS/Middleware Utility
|
|
77
|
+
const middlewareUtil = `import { createServerClient, type CookieOptions } from '@supabase/ssr'
|
|
78
|
+
import { NextResponse, type NextRequest } from 'next/server'
|
|
79
|
+
|
|
80
|
+
export async function updateSession(request: NextRequest) {
|
|
81
|
+
let response = NextResponse.next({
|
|
82
|
+
request: {
|
|
83
|
+
headers: request.headers,
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const supabase = createServerClient(
|
|
88
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
89
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
90
|
+
{
|
|
91
|
+
cookies: {
|
|
92
|
+
get(name: string) {
|
|
93
|
+
return request.cookies.get(name)?.value
|
|
94
|
+
},
|
|
95
|
+
set(name: string, value: string, options: CookieOptions) {
|
|
96
|
+
request.cookies.set({
|
|
97
|
+
name,
|
|
98
|
+
value,
|
|
99
|
+
...options,
|
|
100
|
+
})
|
|
101
|
+
response = NextResponse.next({
|
|
102
|
+
request: {
|
|
103
|
+
headers: request.headers,
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
response.cookies.set({
|
|
107
|
+
name,
|
|
108
|
+
value,
|
|
109
|
+
...options,
|
|
110
|
+
})
|
|
111
|
+
},
|
|
112
|
+
remove(name: string, options: CookieOptions) {
|
|
113
|
+
request.cookies.set({
|
|
114
|
+
name,
|
|
115
|
+
value: '',
|
|
116
|
+
...options,
|
|
117
|
+
})
|
|
118
|
+
response = NextResponse.next({
|
|
119
|
+
request: {
|
|
120
|
+
headers: request.headers,
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
response.cookies.set({
|
|
124
|
+
name,
|
|
125
|
+
value: '',
|
|
126
|
+
...options,
|
|
127
|
+
})
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
await supabase.auth.getUser()
|
|
134
|
+
|
|
135
|
+
return response
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
await writeFile(path.join(projectPath, 'src/utils/supabase/middleware.ts'), middlewareUtil);
|
|
139
|
+
|
|
140
|
+
// 4. Root Middleware
|
|
141
|
+
const middleware = `import { type NextRequest } from 'next/server'
|
|
142
|
+
import { updateSession } from '@/utils/supabase/middleware'
|
|
143
|
+
|
|
144
|
+
export async function middleware(request: NextRequest) {
|
|
145
|
+
return await updateSession(request)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const config = {
|
|
149
|
+
matcher: [
|
|
150
|
+
/*
|
|
151
|
+
* Match all request paths except for the ones starting with:
|
|
152
|
+
* - _next/static (static files)
|
|
153
|
+
* - _next/image (image optimization files)
|
|
154
|
+
* - favicon.ico (favicon file)
|
|
155
|
+
* Feel free to modify this pattern to include more paths.
|
|
156
|
+
*/
|
|
157
|
+
'/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
|
158
|
+
],
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
161
|
+
await writeFile(path.join(projectPath, 'middleware.ts'), middleware);
|
|
162
|
+
|
|
163
|
+
// 5. Auth Callback Route
|
|
164
|
+
const routeCallback = `import { NextResponse } from 'next/server'
|
|
165
|
+
import { cookies } from 'next/headers'
|
|
166
|
+
import { createClient } from '@/utils/supabase/server'
|
|
167
|
+
|
|
168
|
+
export async function GET(request: Request) {
|
|
169
|
+
const { searchParams } = new URL(request.url)
|
|
170
|
+
const code = searchParams.get('code')
|
|
171
|
+
const next = searchParams.get('next') ?? '/'
|
|
172
|
+
|
|
173
|
+
if (code) {
|
|
174
|
+
const cookieStore = cookies()
|
|
175
|
+
const supabase = createClient(cookieStore)
|
|
176
|
+
const { error } = await supabase.auth.exchangeCodeForSession(code)
|
|
177
|
+
if (!error) {
|
|
178
|
+
return NextResponse.redirect(new URL(next, request.url))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// return the user to an error page with instructions
|
|
183
|
+
return NextResponse.redirect(new URL('/auth/auth-code-error', request.url))
|
|
184
|
+
}
|
|
185
|
+
`;
|
|
186
|
+
await writeFile(path.join(projectPath, 'src/app/auth/callback/route.ts'), routeCallback);
|
|
187
|
+
|
|
188
|
+
// 6. Login Page (Supabase UI)
|
|
189
|
+
const loginPage = `'use client';
|
|
190
|
+
import { createClient } from '@/utils/supabase/client';
|
|
191
|
+
import { useRouter } from 'next/navigation';
|
|
192
|
+
import { useState } from 'react';
|
|
193
|
+
import { Rocket } from 'lucide-react';
|
|
194
|
+
|
|
195
|
+
export default function LoginPage() {
|
|
196
|
+
const router = useRouter();
|
|
197
|
+
const [email, setEmail] = useState('');
|
|
198
|
+
const [password, setPassword] = useState('');
|
|
199
|
+
const [loading, setLoading] = useState(false);
|
|
200
|
+
const [message, setMessage] = useState('');
|
|
201
|
+
|
|
202
|
+
const supabase = createClient();
|
|
203
|
+
|
|
204
|
+
const handleEmailLogin = async (e: React.FormEvent) => {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
setLoading(true);
|
|
207
|
+
const { error } = await supabase.auth.signInWithPassword({
|
|
208
|
+
email,
|
|
209
|
+
password,
|
|
210
|
+
});
|
|
211
|
+
if (error) setMessage(error.message);
|
|
212
|
+
else {
|
|
213
|
+
router.refresh();
|
|
214
|
+
router.push('/');
|
|
215
|
+
}
|
|
216
|
+
setLoading(false);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const handleGitHubLogin = async () => {
|
|
220
|
+
await supabase.auth.signInWithOAuth({
|
|
221
|
+
provider: 'github',
|
|
222
|
+
options: {
|
|
223
|
+
redirectTo: \`\${location.origin}/auth/callback\`,
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const handleGoogleLogin = async () => {
|
|
229
|
+
await supabase.auth.signInWithOAuth({
|
|
230
|
+
provider: 'google',
|
|
231
|
+
options: {
|
|
232
|
+
redirectTo: \`\${location.origin}/auth/callback\`,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className="flex min-h-screen flex-col items-center justify-center bg-zinc-950 p-4 text-white">
|
|
239
|
+
<div className="w-full max-w-sm space-y-8 rounded-xl border border-zinc-800 bg-zinc-900/50 p-8 shadow-xl backdrop-blur-xl">
|
|
240
|
+
<div className="text-center">
|
|
241
|
+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-600">
|
|
242
|
+
<Rocket className="h-6 w-6 text-white" />
|
|
243
|
+
</div>
|
|
244
|
+
<h2 className="text-2xl font-bold tracking-tight">Welcome back</h2>
|
|
245
|
+
<p className="mt-2 text-sm text-zinc-400">Sign in to your account</p>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<div className="space-y-4">
|
|
249
|
+
<button
|
|
250
|
+
onClick={handleGitHubLogin}
|
|
251
|
+
className="w-full rounded-md border border-zinc-700 bg-zinc-800 py-2 text-sm font-medium hover:bg-zinc-700 transition-colors"
|
|
252
|
+
>
|
|
253
|
+
Continue with GitHub
|
|
254
|
+
</button>
|
|
255
|
+
<button
|
|
256
|
+
onClick={handleGoogleLogin}
|
|
257
|
+
className="w-full rounded-md border border-zinc-700 bg-zinc-800 py-2 text-sm font-medium hover:bg-zinc-700 transition-colors"
|
|
258
|
+
>
|
|
259
|
+
Continue with Google
|
|
260
|
+
</button>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div className="relative">
|
|
264
|
+
<div className="absolute inset-0 flex items-center">
|
|
265
|
+
<div className="w-full border-t border-zinc-800" />
|
|
266
|
+
</div>
|
|
267
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
268
|
+
<span className="bg-zinc-900 px-2 text-zinc-500">Or continue with</span>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<form onSubmit={handleEmailLogin} className="space-y-4">
|
|
273
|
+
<div>
|
|
274
|
+
<label className="mb-2 block text-sm font-medium text-zinc-400">Email</label>
|
|
275
|
+
<input
|
|
276
|
+
type="email"
|
|
277
|
+
value={email}
|
|
278
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
279
|
+
className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm placeholder:text-zinc-600 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
|
280
|
+
required
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
<div>
|
|
284
|
+
<label className="mb-2 block text-sm font-medium text-zinc-400">Password</label>
|
|
285
|
+
<input
|
|
286
|
+
type="password"
|
|
287
|
+
value={password}
|
|
288
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
289
|
+
className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm placeholder:text-zinc-600 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
|
290
|
+
required
|
|
291
|
+
/>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
{message && <p className="text-sm text-red-400">{message}</p>}
|
|
295
|
+
|
|
296
|
+
<button
|
|
297
|
+
type="submit"
|
|
298
|
+
disabled={loading}
|
|
299
|
+
className="w-full rounded-md bg-indigo-600 py-2 text-sm font-medium text-white hover:bg-indigo-500 disabled:opacity-50"
|
|
300
|
+
>
|
|
301
|
+
{loading ? 'Signing in...' : 'Sign In'}
|
|
302
|
+
</button>
|
|
303
|
+
</form>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
`;
|
|
309
|
+
|
|
310
|
+
await writeFile(path.join(projectPath, 'src/app/login/page.tsx'), loginPage);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function generateNextAuth(projectPath) {
|
|
10
314
|
// Create auth directory
|
|
11
315
|
await ensureDir(path.join(projectPath, 'src/app/api/auth/[...nextauth]'));
|
|
12
316
|
await ensureDir(path.join(projectPath, 'src/app/(auth)/login'));
|
package/src/generators/base.js
CHANGED
|
@@ -1,59 +1,54 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import { writeFile,
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { writeFile, ensureDir } from '../utils/files.js';
|
|
3
3
|
import { PACKAGE_VERSIONS } from '../constants.js';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Generates the base Next.js 15 project structure
|
|
7
|
-
* @param {string} projectPath - Path where the project should be generated
|
|
8
|
-
* @param {string} projectName - Name of the project
|
|
9
|
-
* @param {object} config - Project configuration
|
|
10
|
-
*/
|
|
11
5
|
export async function generateBase(projectPath, projectName, config) {
|
|
12
|
-
// Create directory
|
|
6
|
+
// Create project directory
|
|
7
|
+
await ensureDir(projectPath);
|
|
8
|
+
|
|
9
|
+
// Create src/app directory
|
|
13
10
|
await ensureDir(path.join(projectPath, 'src/app'));
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
|
|
12
|
+
// Create public directory
|
|
16
13
|
await ensureDir(path.join(projectPath, 'public'));
|
|
17
14
|
|
|
18
|
-
//Generate package.json
|
|
15
|
+
// Generate package.json
|
|
19
16
|
const packageJson = {
|
|
20
17
|
name: projectName,
|
|
21
18
|
version: '0.1.0',
|
|
22
19
|
private: true,
|
|
23
|
-
type:
|
|
20
|
+
type: "module",
|
|
24
21
|
scripts: {
|
|
25
22
|
dev: 'next dev',
|
|
26
23
|
build: 'next build',
|
|
27
24
|
start: 'next start',
|
|
28
25
|
lint: 'next lint',
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
"db:push": "prisma db push",
|
|
27
|
+
"db:seed": "tsx prisma/seed.ts",
|
|
28
|
+
"db:studio": "prisma studio",
|
|
29
|
+
postinstall: "prisma generate"
|
|
32
30
|
},
|
|
33
31
|
dependencies: {
|
|
34
32
|
next: PACKAGE_VERSIONS.next,
|
|
35
33
|
react: PACKAGE_VERSIONS.react,
|
|
36
|
-
'react-dom': PACKAGE_VERSIONS
|
|
37
|
-
'@prisma/client': PACKAGE_VERSIONS['@prisma/client'],
|
|
34
|
+
'react-dom': PACKAGE_VERSIONS['react-dom'],
|
|
38
35
|
'next-auth': PACKAGE_VERSIONS['next-auth'],
|
|
39
|
-
|
|
36
|
+
'@prisma/client': PACKAGE_VERSIONS['@prisma/client'],
|
|
40
37
|
stripe: PACKAGE_VERSIONS.stripe,
|
|
41
38
|
resend: PACKAGE_VERSIONS.resend,
|
|
42
|
-
'@react-email/components': PACKAGE_VERSIONS['@react-email/components'],
|
|
43
39
|
zod: PACKAGE_VERSIONS.zod,
|
|
44
|
-
'react-hook-form': PACKAGE_VERSIONS['react-hook-form'],
|
|
45
|
-
'@hookform/resolvers': PACKAGE_VERSIONS['@hookform/resolvers'],
|
|
46
|
-
'class-variance-authority': PACKAGE_VERSIONS['class-variance-authority'],
|
|
47
|
-
clsx: PACKAGE_VERSIONS.clsx,
|
|
48
|
-
'tailwind-merge': PACKAGE_VERSIONS['tailwind-merge'],
|
|
49
|
-
'tailwindcss-animate': PACKAGE_VERSIONS['tailwindcss-animate'],
|
|
50
40
|
'lucide-react': PACKAGE_VERSIONS['lucide-react'],
|
|
51
|
-
'
|
|
41
|
+
'date-fns': PACKAGE_VERSIONS['date-fns'],
|
|
42
|
+
'clsx': PACKAGE_VERSIONS.clsx,
|
|
43
|
+
'tailwind-merge': PACKAGE_VERSIONS['tailwind-merge'],
|
|
52
44
|
'@radix-ui/react-slot': PACKAGE_VERSIONS['@radix-ui/react-slot'],
|
|
53
|
-
'@radix-ui/react-
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
|
|
45
|
+
'@radix-ui/react-label': '^2.0.2',
|
|
46
|
+
'class-variance-authority': '^0.7.0',
|
|
47
|
+
'tailwindcss-animate': '^1.0.7',
|
|
48
|
+
...(config?.auth === 'Supabase Auth' || config?.database === 'Supabase' ? {
|
|
49
|
+
'@supabase/ssr': PACKAGE_VERSIONS['@supabase/ssr'],
|
|
50
|
+
'@supabase/supabase-js': PACKAGE_VERSIONS['@supabase/supabase-js'],
|
|
51
|
+
} : {}),
|
|
57
52
|
},
|
|
58
53
|
devDependencies: {
|
|
59
54
|
typescript: PACKAGE_VERSIONS.typescript,
|
|
@@ -112,7 +107,14 @@ export async function generateBase(projectPath, projectName, config) {
|
|
|
112
107
|
|
|
113
108
|
// Generate next.config.js
|
|
114
109
|
const nextConfig = `/** @type {import('next').NextConfig} */
|
|
110
|
+
import path from 'path';
|
|
111
|
+
import { fileURLToPath } from 'url';
|
|
112
|
+
|
|
113
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
114
|
+
const __dirname = path.dirname(__filename);
|
|
115
|
+
|
|
115
116
|
const nextConfig = {
|
|
117
|
+
outputFileTracingRoot: __dirname,
|
|
116
118
|
experimental: {
|
|
117
119
|
serverActions: {
|
|
118
120
|
bodySizeLimit: '2mb',
|
|
@@ -310,49 +312,208 @@ export default function RootLayout({
|
|
|
310
312
|
await writeFile(path.join(projectPath, 'src/app/layout.tsx'), layoutTsx);
|
|
311
313
|
|
|
312
314
|
// Generate app/page.tsx
|
|
313
|
-
const pageTsx = `
|
|
315
|
+
const pageTsx = `'use client';
|
|
316
|
+
|
|
317
|
+
import { useState, useEffect } from 'react';
|
|
318
|
+
import Link from 'next/link';
|
|
319
|
+
import { Rocket, Users, DollarSign, Code, Database, CreditCard, Mail, Lock, CheckCircle2, Trophy, ArrowRight, Play, TrendingUp, Box } from 'lucide-react';
|
|
320
|
+
|
|
321
|
+
export default function Home() {
|
|
322
|
+
const [mounted, setMounted] = useState(false);
|
|
323
|
+
const [stats, setStats] = useState({
|
|
324
|
+
mrr: 0,
|
|
325
|
+
users: 0,
|
|
326
|
+
shipped: 0
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
setMounted(true);
|
|
331
|
+
}, []);
|
|
332
|
+
|
|
333
|
+
const incrementStats = () => {
|
|
334
|
+
setStats(prev => ({
|
|
335
|
+
mrr: prev.mrr + 100,
|
|
336
|
+
users: prev.users + 5,
|
|
337
|
+
shipped: prev.shipped + 1
|
|
338
|
+
}));
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
if (!mounted) return null;
|
|
342
|
+
|
|
314
343
|
return (
|
|
315
|
-
<
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
344
|
+
<div className="min-h-screen bg-zinc-950 text-zinc-50 font-sans selection:bg-indigo-500/30">
|
|
345
|
+
|
|
346
|
+
{/* Navigation */}
|
|
347
|
+
<nav className="fixed w-full z-50 border-b border-zinc-800 bg-zinc-950/80 backdrop-blur-md">
|
|
348
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
349
|
+
<div className="flex justify-between h-16 items-center">
|
|
350
|
+
<div className="flex items-center gap-2">
|
|
351
|
+
<div className="h-8 w-8 bg-indigo-600 rounded-lg flex items-center justify-center">
|
|
352
|
+
<Rocket className="h-5 w-5 text-white" />
|
|
353
|
+
</div>
|
|
354
|
+
<span className="text-xl font-bold tracking-tight">${projectName}</span>
|
|
355
|
+
</div>
|
|
356
|
+
<div className="flex items-center gap-4">
|
|
357
|
+
{process.env.NODE_ENV === 'development' && (
|
|
358
|
+
<Link
|
|
359
|
+
href="/setup"
|
|
360
|
+
className="text-sm font-medium text-zinc-400 hover:text-white transition-colors flex items-center gap-2"
|
|
361
|
+
>
|
|
362
|
+
<CheckCircle2 className="h-4 w-4" />
|
|
363
|
+
Diagnostics
|
|
364
|
+
</Link>
|
|
365
|
+
)}
|
|
366
|
+
<Link
|
|
367
|
+
href="/login"
|
|
368
|
+
className="text-sm font-medium text-zinc-400 hover:text-white transition-colors"
|
|
369
|
+
>
|
|
370
|
+
Sign In
|
|
371
|
+
</Link>
|
|
372
|
+
<Link
|
|
373
|
+
href="/signup"
|
|
374
|
+
className="inline-flex items-center justify-center rounded-md bg-white px-4 py-2 text-sm font-medium text-zinc-950 shadow-sm hover:bg-zinc-200 transition-colors"
|
|
375
|
+
>
|
|
376
|
+
Get Started
|
|
377
|
+
</Link>
|
|
378
|
+
</div>
|
|
329
379
|
</div>
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
380
|
+
</div>
|
|
381
|
+
</nav>
|
|
382
|
+
|
|
383
|
+
{/* Hero Section */}
|
|
384
|
+
<div className="relative pt-32 pb-20 sm:pt-40 sm:pb-24 overflow-hidden">
|
|
385
|
+
|
|
386
|
+
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center z-10">
|
|
387
|
+
<div className="inline-flex items-center rounded-full border border-zinc-800 bg-zinc-900/50 px-3 py-1 text-sm text-zinc-400 mb-8 backdrop-blur-xl">
|
|
388
|
+
<span className="flex h-2 w-2 rounded-full bg-emerald-500 mr-2 animate-pulse"></span>
|
|
389
|
+
v1.2.1 is now live
|
|
335
390
|
</div>
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
<
|
|
339
|
-
|
|
340
|
-
|
|
391
|
+
|
|
392
|
+
<h1 className="text-5xl sm:text-7xl font-bold tracking-tight mb-8 bg-gradient-to-b from-white to-zinc-500 bg-clip-text text-transparent pb-2">
|
|
393
|
+
Build your SaaS <br className="hidden sm:block" />
|
|
394
|
+
<span className="text-white">in record time.</span>
|
|
395
|
+
</h1>
|
|
396
|
+
|
|
397
|
+
<p className="mt-4 text-xl text-zinc-400 max-w-2xl mx-auto mb-10 leading-relaxed">
|
|
398
|
+
The modern stack for ambitious developers. Authentication, payments, database, and emails — configured and ready to scale.
|
|
399
|
+
</p>
|
|
400
|
+
|
|
401
|
+
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
|
402
|
+
<Link
|
|
403
|
+
href="/signup"
|
|
404
|
+
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 rounded-lg bg-indigo-600 px-8 py-3.5 text-sm font-semibold text-white shadow-lg hover:bg-indigo-500 transition-all hover:scale-105 active:scale-95"
|
|
405
|
+
>
|
|
406
|
+
Start Building <ArrowRight className="h-4 w-4" />
|
|
407
|
+
</Link>
|
|
408
|
+
<a
|
|
409
|
+
href="https://github.com/yourusername/create-solostack"
|
|
410
|
+
target="_blank"
|
|
411
|
+
rel="noopener noreferrer"
|
|
412
|
+
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 rounded-lg border border-zinc-800 bg-zinc-900/50 px-8 py-3.5 text-sm font-medium text-zinc-300 hover:bg-zinc-800 hover:text-white transition-all backdrop-blur-sm"
|
|
413
|
+
>
|
|
414
|
+
Star on GitHub
|
|
415
|
+
</a>
|
|
341
416
|
</div>
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
417
|
+
|
|
418
|
+
{/* Interactive Game: SaaS Simulator */}
|
|
419
|
+
<div className="mt-20 max-w-4xl mx-auto">
|
|
420
|
+
<div className="relative rounded-xl border border-zinc-800 bg-zinc-900/50 p-2 backdrop-blur-xl shadow-2xl">
|
|
421
|
+
<div className="absolute -inset-1 rounded-xl bg-gradient-to-r from-indigo-500/20 via-purple-500/20 to-pink-500/20 blur opacity-75"></div>
|
|
422
|
+
<div className="relative rounded-lg bg-zinc-950 p-8">
|
|
423
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
|
|
424
|
+
<div className="p-4 rounded-lg bg-zinc-900/50 border border-zinc-800">
|
|
425
|
+
<div className="flex items-center gap-2 text-zinc-400 mb-2">
|
|
426
|
+
<TrendingUp className="h-4 w-4 text-emerald-500" /> MRR
|
|
427
|
+
</div>
|
|
428
|
+
<div className="text-3xl font-mono font-bold text-white">
|
|
429
|
+
\${stats.mrr.toLocaleString()}
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
<div className="p-4 rounded-lg bg-zinc-900/50 border border-zinc-800">
|
|
433
|
+
<div className="flex items-center gap-2 text-zinc-400 mb-2">
|
|
434
|
+
<Users className="h-4 w-4 text-blue-500" /> Users
|
|
435
|
+
</div>
|
|
436
|
+
<div className="text-3xl font-mono font-bold text-white">
|
|
437
|
+
{stats.users.toLocaleString()}
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
<div className="p-4 rounded-lg bg-zinc-900/50 border border-zinc-800">
|
|
441
|
+
<div className="flex items-center gap-2 text-zinc-400 mb-2">
|
|
442
|
+
<Box className="h-4 w-4 text-purple-500" /> Shipped
|
|
443
|
+
</div>
|
|
444
|
+
<div className="text-3xl font-mono font-bold text-white">
|
|
445
|
+
{stats.shipped}
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
<div className="text-center">
|
|
451
|
+
<p className="text-zinc-500 mb-6 font-mono text-sm">
|
|
452
|
+
> CLICK_BELOW_TO_SIMULATE_GROWTH.exe
|
|
453
|
+
</p>
|
|
454
|
+
<button
|
|
455
|
+
onClick={incrementStats}
|
|
456
|
+
className="group relative inline-flex items-center justify-center rounded-full bg-zinc-100 px-8 py-4 font-bold text-zinc-950 transition-all hover:scale-105 active:scale-95 shadow-lg hover:shadow-indigo-500/20"
|
|
457
|
+
>
|
|
458
|
+
<Play className="mr-2 h-5 w-5 fill-zinc-950 transition-transform group-hover:translate-x-1" />
|
|
459
|
+
SHIP FEATURE
|
|
460
|
+
</button>
|
|
461
|
+
<p className="mt-4 text-xs text-zinc-600">
|
|
462
|
+
* Results may vary. Consistency is key.
|
|
463
|
+
</p>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
347
467
|
</div>
|
|
348
468
|
</div>
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
469
|
+
|
|
470
|
+
{/* Background Gradients */}
|
|
471
|
+
<div className="absolute top-0 left-0 right-0 h-[500px] bg-[radial-gradient(circle_at_50%_0%,rgba(99,102,241,0.15),transparent_70%)] pointer-events-none"></div>
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
{/* Features Grid */}
|
|
475
|
+
<div className="py-24 border-t border-zinc-900 bg-zinc-950">
|
|
476
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
477
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
|
478
|
+
<FeatureCard
|
|
479
|
+
icon={<Lock className="h-6 w-6 text-indigo-400" />}
|
|
480
|
+
title="Authentication"
|
|
481
|
+
description="Secure user management with NextAuth or Supabase. Login, signup, and protected routes."
|
|
482
|
+
/>
|
|
483
|
+
<FeatureCard
|
|
484
|
+
icon={<Database className="h-6 w-6 text-emerald-400" />}
|
|
485
|
+
title="Database"
|
|
486
|
+
description="Type-safe data access with Prisma and PostgreSQL. Seeding scripts included."
|
|
487
|
+
/>
|
|
488
|
+
<FeatureCard
|
|
489
|
+
icon={<CreditCard className="h-6 w-6 text-purple-400" />}
|
|
490
|
+
title="Payments"
|
|
491
|
+
description="Stripe integration with checkout sessions, webhooks, and subscription management."
|
|
492
|
+
/>
|
|
493
|
+
<FeatureCard
|
|
494
|
+
icon={<Mail className="h-6 w-6 text-pink-400" />}
|
|
495
|
+
title="Emails"
|
|
496
|
+
description="Transactional emails with Resend and React Email. Beautiful templates out of the box."
|
|
497
|
+
/>
|
|
498
|
+
</div>
|
|
353
499
|
</div>
|
|
354
500
|
</div>
|
|
355
|
-
|
|
501
|
+
|
|
502
|
+
{/* Footer */}
|
|
503
|
+
<footer className="border-t border-zinc-900 bg-zinc-950 py-12">
|
|
504
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row justify-between items-center gap-6">
|
|
505
|
+
<div className="flex items-center gap-2">
|
|
506
|
+
<div className="h-6 w-6 bg-zinc-800 rounded flex items-center justify-center">
|
|
507
|
+
<Rocket className="h-3 w-3 text-zinc-400" />
|
|
508
|
+
</div>
|
|
509
|
+
<span className="text-sm font-semibold text-zinc-300">${projectName}</span>
|
|
510
|
+
</div>
|
|
511
|
+
<p className="text-sm text-zinc-500">
|
|
512
|
+
Built with create-solostack.
|
|
513
|
+
</p>
|
|
514
|
+
</div>
|
|
515
|
+
</footer>
|
|
516
|
+
</div>
|
|
356
517
|
);
|
|
357
518
|
}
|
|
358
519
|
`;
|
|
@@ -361,52 +522,57 @@ export default function RootLayout({
|
|
|
361
522
|
|
|
362
523
|
// Generate .env.example
|
|
363
524
|
const envExample = `# Database
|
|
364
|
-
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
|
|
365
|
-
# For cloud databases, add ?sslmode=require to the connection string
|
|
525
|
+
DATABASE_URL = "postgresql://user:password@localhost:5432/dbname"
|
|
526
|
+
# For cloud databases, add ? sslmode = require to the connection string
|
|
366
527
|
|
|
367
528
|
# NextAuth
|
|
368
|
-
NEXTAUTH_SECRET="" # Generate with: openssl rand -base64 32
|
|
369
|
-
NEXTAUTH_URL="http://localhost:3000"
|
|
529
|
+
NEXTAUTH_SECRET = "" # Generate with: openssl rand - base64 32
|
|
530
|
+
NEXTAUTH_URL = "http://localhost:3000"
|
|
370
531
|
# In production, set to your domain: https://yourdomain.com
|
|
371
532
|
|
|
372
533
|
# Stripe
|
|
373
|
-
STRIPE_SECRET_KEY="sk_test_..."
|
|
374
|
-
STRIPE_WEBHOOK_SECRET="whsec_..."
|
|
375
|
-
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."
|
|
534
|
+
STRIPE_SECRET_KEY = "sk_test_..."
|
|
535
|
+
STRIPE_WEBHOOK_SECRET = "whsec_..."
|
|
536
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY = "pk_test_..."
|
|
376
537
|
# Create price IDs in Stripe Dashboard -> Products
|
|
377
|
-
STRIPE_PRO_PRICE_ID="price_..." # Monthly Pro plan price ID
|
|
378
|
-
STRIPE_ENTERPRISE_PRICE_ID="price_..." # Monthly Enterprise plan price ID
|
|
538
|
+
STRIPE_PRO_PRICE_ID = "price_..." # Monthly Pro plan price ID
|
|
539
|
+
STRIPE_ENTERPRISE_PRICE_ID = "price_..." # Monthly Enterprise plan price ID
|
|
379
540
|
|
|
380
541
|
# Resend
|
|
381
|
-
RESEND_API_KEY="re_..."
|
|
382
|
-
FROM_EMAIL="onboarding@resend.dev" # Use your verified domain
|
|
542
|
+
RESEND_API_KEY = "re_..."
|
|
543
|
+
FROM_EMAIL = "onboarding@resend.dev" # Use your verified domain
|
|
383
544
|
|
|
384
|
-
# OAuth Providers
|
|
545
|
+
# OAuth Providers(Optional)
|
|
385
546
|
# Google: https://console.cloud.google.com/apis/credentials
|
|
386
|
-
GOOGLE_CLIENT_ID=""
|
|
387
|
-
GOOGLE_CLIENT_SECRET=""
|
|
547
|
+
GOOGLE_CLIENT_ID = ""
|
|
548
|
+
GOOGLE_CLIENT_SECRET = ""
|
|
388
549
|
# GitHub: https://github.com/settings/developers
|
|
389
|
-
GITHUB_CLIENT_ID=""
|
|
390
|
-
GITHUB_CLIENT_SECRET=""
|
|
391
|
-
|
|
550
|
+
GITHUB_CLIENT_ID = ""
|
|
551
|
+
GITHUB_CLIENT_SECRET = ""${config?.auth === 'Supabase Auth' || config?.database === 'Supabase' ? `
|
|
552
|
+
|
|
553
|
+
# Supabase
|
|
554
|
+
NEXT_PUBLIC_SUPABASE_URL="https://your-project.supabase.co"
|
|
555
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY="your-anon-key"` : ''
|
|
556
|
+
}
|
|
557
|
+
`;
|
|
392
558
|
|
|
393
559
|
await writeFile(path.join(projectPath, '.env.example'), envExample);
|
|
394
560
|
|
|
395
561
|
// Generate README.md
|
|
396
|
-
const readme =
|
|
562
|
+
const readme = "# " + projectName + `
|
|
397
563
|
|
|
398
564
|
Built with [SoloStack](https://github.com/yourusername/create-solostack) - The complete SaaS boilerplate for indie hackers.
|
|
399
565
|
|
|
400
566
|
## Features
|
|
401
567
|
|
|
402
|
-
-
|
|
403
|
-
-
|
|
404
|
-
-
|
|
405
|
-
-
|
|
406
|
-
-
|
|
407
|
-
-
|
|
408
|
-
-
|
|
409
|
-
-
|
|
568
|
+
- ✅ ** Next.js 15 ** with App Router
|
|
569
|
+
- ✅ ** TypeScript ** for type safety
|
|
570
|
+
- ✅ ** Tailwind CSS ** for styling
|
|
571
|
+
- ✅ ** Prisma ** + PostgreSQL for database
|
|
572
|
+
- ✅ ** NextAuth.js ** for authentication
|
|
573
|
+
- ✅ ** Stripe ** for payments
|
|
574
|
+
- ✅ ** Resend ** for emails
|
|
575
|
+
- ✅ ** shadcn / ui ** components
|
|
410
576
|
|
|
411
577
|
## Getting Started
|
|
412
578
|
|
|
@@ -443,16 +609,16 @@ Open [http://localhost:3000](http://localhost:3000) with your browser.
|
|
|
443
609
|
|
|
444
610
|
\`\`\`
|
|
445
611
|
${projectName}/
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
612
|
+
├── prisma/ # Database schema and migrations
|
|
613
|
+
├── public/ # Static assets
|
|
614
|
+
├── src/
|
|
615
|
+
│ ├── app/ # Next.js app directory
|
|
616
|
+
│ │ ├── api/ # API routes
|
|
617
|
+
│ │ ├── (auth)/ # Auth pages
|
|
618
|
+
│ │ └── dashboard/ # Protected pages
|
|
619
|
+
│ ├── components/ # React components
|
|
620
|
+
│ └── lib/ # Utilities and configurations
|
|
621
|
+
└── package.json
|
|
456
622
|
\`\`\`
|
|
457
623
|
|
|
458
624
|
## Available Scripts
|
|
@@ -4,22 +4,89 @@ import { writeFile, ensureDir } from '../utils/files.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Generates Prisma database configuration
|
|
6
6
|
* @param {string} projectPath - Path where the project is located
|
|
7
|
-
* @param {
|
|
7
|
+
* @param {object} config - Configuration object
|
|
8
8
|
*/
|
|
9
|
-
export async function generateDatabase(projectPath,
|
|
9
|
+
export async function generateDatabase(projectPath, config) {
|
|
10
10
|
// Create prisma directory
|
|
11
11
|
await ensureDir(path.join(projectPath, 'prisma'));
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
const
|
|
15
|
-
|
|
13
|
+
// Define schema based on Auth provider
|
|
14
|
+
const isSupabaseAuth = config.auth === 'Supabase Auth';
|
|
15
|
+
|
|
16
|
+
let schemaModels = '';
|
|
17
|
+
|
|
18
|
+
if (isSupabaseAuth) {
|
|
19
|
+
// Schema for Supabase Auth (simplified User, no adapter tables)
|
|
20
|
+
schemaModels = `
|
|
21
|
+
model User {
|
|
22
|
+
id String @id @default(uuid())
|
|
23
|
+
email String @unique
|
|
24
|
+
name String?
|
|
25
|
+
image String?
|
|
26
|
+
stripeCustomerId String? @unique
|
|
27
|
+
subscription Subscription?
|
|
28
|
+
createdAt DateTime @default(now())
|
|
29
|
+
updatedAt DateTime @updatedAt
|
|
30
|
+
|
|
31
|
+
payments Payment[]
|
|
16
32
|
}
|
|
17
33
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
34
|
+
// Note: Account, Session, VerificationToken are not needed for Supabase Auth
|
|
35
|
+
|
|
36
|
+
model Subscription {
|
|
37
|
+
id String @id @default(cuid())
|
|
38
|
+
userId String @unique
|
|
39
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
40
|
+
stripeSubscriptionId String @unique
|
|
41
|
+
stripePriceId String
|
|
42
|
+
status SubscriptionStatus
|
|
43
|
+
currentPeriodStart DateTime
|
|
44
|
+
currentPeriodEnd DateTime
|
|
45
|
+
cancelAtPeriodEnd Boolean @default(false)
|
|
46
|
+
createdAt DateTime @default(now())
|
|
47
|
+
updatedAt DateTime @updatedAt
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
model Payment {
|
|
51
|
+
id String @id @default(cuid())
|
|
52
|
+
userId String
|
|
53
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
54
|
+
stripePaymentId String @unique
|
|
55
|
+
amount Int // In cents
|
|
56
|
+
currency String @default("usd")
|
|
57
|
+
status String
|
|
58
|
+
createdAt DateTime @default(now())
|
|
59
|
+
|
|
60
|
+
@@index([userId])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
model StripeEvent {
|
|
64
|
+
id String @id @default(cuid())
|
|
65
|
+
eventId String @unique // Stripe event ID
|
|
66
|
+
type String // Event type (e.g., "checkout.session.completed")
|
|
67
|
+
processed Boolean @default(false)
|
|
68
|
+
createdAt DateTime @default(now())
|
|
69
|
+
|
|
70
|
+
@@index([eventId])
|
|
71
|
+
@@index([processed])
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
enum Role {
|
|
75
|
+
USER
|
|
76
|
+
ADMIN
|
|
21
77
|
}
|
|
22
78
|
|
|
79
|
+
enum SubscriptionStatus {
|
|
80
|
+
ACTIVE
|
|
81
|
+
CANCELED
|
|
82
|
+
PAST_DUE
|
|
83
|
+
TRIALING
|
|
84
|
+
INCOMPLETE
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
} else {
|
|
88
|
+
// Standard Schema for NextAuth (with Adapter tables)
|
|
89
|
+
schemaModels = `
|
|
23
90
|
model User {
|
|
24
91
|
id String @id @default(cuid())
|
|
25
92
|
email String @unique
|
|
@@ -124,6 +191,19 @@ enum SubscriptionStatus {
|
|
|
124
191
|
TRIALING
|
|
125
192
|
INCOMPLETE
|
|
126
193
|
}
|
|
194
|
+
`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Generate schema.prisma
|
|
198
|
+
const schemaPrisma = `generator client {
|
|
199
|
+
provider = "prisma-client-js"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
datasource db {
|
|
203
|
+
provider = "postgresql"
|
|
204
|
+
url = env("DATABASE_URL")
|
|
205
|
+
}
|
|
206
|
+
${schemaModels}
|
|
127
207
|
`;
|
|
128
208
|
|
|
129
209
|
await writeFile(path.join(projectPath, 'prisma/schema.prisma'), schemaPrisma);
|
|
@@ -143,7 +223,36 @@ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
|
|
|
143
223
|
await writeFile(path.join(projectPath, 'src/lib/db.ts'), dbClient);
|
|
144
224
|
|
|
145
225
|
// Generate seed script
|
|
146
|
-
|
|
226
|
+
let seedScript = '';
|
|
227
|
+
|
|
228
|
+
if (isSupabaseAuth) {
|
|
229
|
+
seedScript = `import { PrismaClient } from '@prisma/client';
|
|
230
|
+
|
|
231
|
+
const prisma = new PrismaClient();
|
|
232
|
+
|
|
233
|
+
async function main() {
|
|
234
|
+
console.log('🌱 Seeding database...');
|
|
235
|
+
|
|
236
|
+
console.log('⚠️ Using Supabase Auth: Users should be created via the Application UI or Supabase Dashboard.');
|
|
237
|
+
console.log('⚠️ Prisma seed will skip User creation to avoid conflicts with triggers/auth.users.');
|
|
238
|
+
|
|
239
|
+
// Example of seeding other data if needed
|
|
240
|
+
// ...
|
|
241
|
+
|
|
242
|
+
console.log('🎉 Seeding complete!');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
main()
|
|
246
|
+
.catch((e) => {
|
|
247
|
+
console.error('❌ Seeding failed:', e);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
})
|
|
250
|
+
.finally(async () => {
|
|
251
|
+
await prisma.$disconnect();
|
|
252
|
+
});
|
|
253
|
+
`;
|
|
254
|
+
} else {
|
|
255
|
+
seedScript = `import { PrismaClient } from '@prisma/client';
|
|
147
256
|
import bcrypt from 'bcryptjs';
|
|
148
257
|
|
|
149
258
|
const prisma = new PrismaClient();
|
|
@@ -194,12 +303,20 @@ main()
|
|
|
194
303
|
await prisma.$disconnect();
|
|
195
304
|
});
|
|
196
305
|
`;
|
|
306
|
+
}
|
|
197
307
|
|
|
198
308
|
await writeFile(path.join(projectPath, 'prisma/seed.ts'), seedScript);
|
|
199
309
|
|
|
200
310
|
// Generate database migration guide
|
|
201
311
|
const dbGuide = `# Database Setup Guide
|
|
202
|
-
|
|
312
|
+
${isSupabaseAuth ? `
|
|
313
|
+
## Supabase Setup (Important)
|
|
314
|
+
Since you are using **Supabase Auth**, you should use Supabase as your database provider.
|
|
315
|
+
|
|
316
|
+
1. Create a project at [supabase.com](https://supabase.com).
|
|
317
|
+
2. Go to Project Settings -> Database -> Connection String.
|
|
318
|
+
3. Copy the URI (Mode: Transaction or Session).
|
|
319
|
+
` : ''}
|
|
203
320
|
## Initial Setup
|
|
204
321
|
|
|
205
322
|
### 1. Set up your PostgreSQL database
|
|
@@ -229,8 +346,8 @@ GRANT ALL PRIVILEGES ON DATABASE your_database_name TO your_user;
|
|
|
229
346
|
\`\`\`
|
|
230
347
|
|
|
231
348
|
**Option B: Cloud Database (Recommended for Production)**
|
|
349
|
+
- [Supabase](https://supabase.com) - Free tier (Required if using Supabase Auth)
|
|
232
350
|
- [Neon](https://neon.tech) - Free tier with PostgreSQL
|
|
233
|
-
- [Supabase](https://supabase.com) - Free tier with PostgreSQL
|
|
234
351
|
- [Railway](https://railway.app) - Easy PostgreSQL deployment
|
|
235
352
|
- [Vercel Postgres](https://vercel.com/storage/postgres) - Serverless PostgreSQL
|
|
236
353
|
|
|
@@ -259,10 +376,11 @@ This command:
|
|
|
259
376
|
npm run db:seed
|
|
260
377
|
\`\`\`
|
|
261
378
|
|
|
379
|
+
${!isSupabaseAuth ? `
|
|
262
380
|
This creates:
|
|
263
381
|
- Test user: test@example.com (password: password123)
|
|
264
382
|
- Admin user: admin@example.com (password: password123)
|
|
265
|
-
|
|
383
|
+
` : ''}
|
|
266
384
|
## Making Schema Changes
|
|
267
385
|
|
|
268
386
|
### 1. Edit prisma/schema.prisma
|
|
@@ -317,48 +435,6 @@ npx prisma migrate reset
|
|
|
317
435
|
# View database structure
|
|
318
436
|
npx prisma db pull
|
|
319
437
|
\`\`\`
|
|
320
|
-
|
|
321
|
-
## Backup & Restore
|
|
322
|
-
|
|
323
|
-
### Backup
|
|
324
|
-
\`\`\`bash
|
|
325
|
-
pg_dump -U your_user -d your_database > backup.sql
|
|
326
|
-
\`\`\`
|
|
327
|
-
|
|
328
|
-
### Restore
|
|
329
|
-
\`\`\`bash
|
|
330
|
-
psql -U your_user -d your_database < backup.sql
|
|
331
|
-
\`\`\`
|
|
332
|
-
|
|
333
|
-
## Troubleshooting
|
|
334
|
-
|
|
335
|
-
### Connection Issues
|
|
336
|
-
- Verify DATABASE_URL is correct
|
|
337
|
-
- Check if PostgreSQL is running
|
|
338
|
-
- Ensure database exists
|
|
339
|
-
- Check firewall settings for remote connections
|
|
340
|
-
|
|
341
|
-
### Migration Conflicts
|
|
342
|
-
\`\`\`bash
|
|
343
|
-
# Reset migrations (development only)
|
|
344
|
-
npx prisma migrate reset
|
|
345
|
-
|
|
346
|
-
# Mark migration as applied without running
|
|
347
|
-
npx prisma migrate resolve --applied migration_name
|
|
348
|
-
\`\`\`
|
|
349
|
-
|
|
350
|
-
### Performance
|
|
351
|
-
- Add indexes for frequently queried fields
|
|
352
|
-
- Use \`@@index\` in your schema
|
|
353
|
-
- Monitor with \`EXPLAIN ANALYZE\` in PostgreSQL
|
|
354
|
-
|
|
355
|
-
## Best Practices
|
|
356
|
-
|
|
357
|
-
1. **Always backup before major migrations**
|
|
358
|
-
2. **Test migrations on staging first**
|
|
359
|
-
3. **Use migrations (not db push) in production**
|
|
360
|
-
4. **Keep DATABASE_URL in .env, never commit it**
|
|
361
|
-
5. **Use connection pooling for serverless (e.g., Prisma Data Proxy)**
|
|
362
438
|
`;
|
|
363
439
|
|
|
364
440
|
await writeFile(path.join(projectPath, 'prisma/DATABASE_GUIDE.md'), dbGuide);
|
package/src/index.js
CHANGED
|
@@ -68,13 +68,6 @@ export async function main() {
|
|
|
68
68
|
|
|
69
69
|
// Ask configuration questions
|
|
70
70
|
const config = await inquirer.prompt([
|
|
71
|
-
{
|
|
72
|
-
type: 'list',
|
|
73
|
-
name: 'auth',
|
|
74
|
-
message: 'Choose authentication:',
|
|
75
|
-
choices: AUTH_PROVIDERS,
|
|
76
|
-
default: AUTH_PROVIDERS[0],
|
|
77
|
-
},
|
|
78
71
|
{
|
|
79
72
|
type: 'list',
|
|
80
73
|
name: 'database',
|
|
@@ -82,6 +75,13 @@ export async function main() {
|
|
|
82
75
|
choices: DATABASES,
|
|
83
76
|
default: DATABASES[0],
|
|
84
77
|
},
|
|
78
|
+
{
|
|
79
|
+
type: 'list',
|
|
80
|
+
name: 'auth',
|
|
81
|
+
message: 'Choose authentication:',
|
|
82
|
+
choices: AUTH_PROVIDERS,
|
|
83
|
+
default: (answers) => answers.database === 'Supabase' ? 'Supabase Auth' : AUTH_PROVIDERS[0],
|
|
84
|
+
},
|
|
85
85
|
{
|
|
86
86
|
type: 'list',
|
|
87
87
|
name: 'payments',
|
|
@@ -126,7 +126,7 @@ export async function main() {
|
|
|
126
126
|
|
|
127
127
|
// Generate database integration
|
|
128
128
|
spinner = ora('Configuring database').start();
|
|
129
|
-
await generateDatabase(projectPath, config
|
|
129
|
+
await generateDatabase(projectPath, config);
|
|
130
130
|
spinner.succeed('Configured database (Prisma + PostgreSQL)');
|
|
131
131
|
|
|
132
132
|
// Generate authentication
|