create-solostack 1.2.0 → 1.2.2
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 +239 -183
- 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,8 +1,8 @@
|
|
|
1
|
-
import path from 'path';
|
|
1
|
+
import path from 'path';
|
|
2
2
|
import { writeFile, ensureDir } from '../utils/files.js';
|
|
3
3
|
import { PACKAGE_VERSIONS } from '../constants.js';
|
|
4
4
|
|
|
5
|
-
export async function generateBase(projectPath, projectName) {
|
|
5
|
+
export async function generateBase(projectPath, projectName, config) {
|
|
6
6
|
// Create project directory
|
|
7
7
|
await ensureDir(projectPath);
|
|
8
8
|
|
|
@@ -45,6 +45,10 @@ export async function generateBase(projectPath, projectName) {
|
|
|
45
45
|
'@radix-ui/react-label': '^2.0.2',
|
|
46
46
|
'class-variance-authority': '^0.7.0',
|
|
47
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
|
+
} : {}),
|
|
48
52
|
},
|
|
49
53
|
devDependencies: {
|
|
50
54
|
typescript: PACKAGE_VERSIONS.typescript,
|
|
@@ -312,170 +316,217 @@ export default function RootLayout({
|
|
|
312
316
|
|
|
313
317
|
import { useState, useEffect } from 'react';
|
|
314
318
|
import Link from 'next/link';
|
|
315
|
-
import {
|
|
319
|
+
import {
|
|
320
|
+
CheckCircle2,
|
|
321
|
+
AlertCircle,
|
|
322
|
+
Terminal,
|
|
323
|
+
Database,
|
|
324
|
+
Shield,
|
|
325
|
+
CreditCard,
|
|
326
|
+
Mail,
|
|
327
|
+
Loader2,
|
|
328
|
+
ChevronRight,
|
|
329
|
+
ExternalLink,
|
|
330
|
+
Book
|
|
331
|
+
} from 'lucide-react';
|
|
316
332
|
|
|
317
333
|
export default function Home() {
|
|
318
|
-
const [
|
|
319
|
-
const [
|
|
320
|
-
const [users, setUsers] = useState(0);
|
|
321
|
-
const [clicks, setClicks] = useState(0);
|
|
322
|
-
const [shipped, setShipped] = useState(0);
|
|
334
|
+
const [status, setStatus] = useState<any>(null);
|
|
335
|
+
const [loading, setLoading] = useState(true);
|
|
323
336
|
|
|
324
337
|
useEffect(() => {
|
|
325
|
-
|
|
338
|
+
fetch('/api/setup')
|
|
339
|
+
.then(res => res.json())
|
|
340
|
+
.then(data => {
|
|
341
|
+
setStatus(data);
|
|
342
|
+
setLoading(false);
|
|
343
|
+
})
|
|
344
|
+
.catch(() => setLoading(false));
|
|
326
345
|
}, []);
|
|
327
346
|
|
|
328
|
-
const ship = () => {
|
|
329
|
-
setClicks(prev => prev + 1);
|
|
330
|
-
setShipped(prev => prev + 1);
|
|
331
|
-
setUsers(prev => prev + Math.floor(Math.random() * 3) + 1);
|
|
332
|
-
|
|
333
|
-
// Revenue logic
|
|
334
|
-
if (Math.random() > 0.7) {
|
|
335
|
-
setMrr(prev => prev + Math.floor(Math.random() * 20) + 10);
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
if (!mounted) return (
|
|
340
|
-
<div className="min-h-screen bg-black text-white flex items-center justify-center">
|
|
341
|
-
<div className="animate-pulse">Loading SaaS...</div>
|
|
342
|
-
</div>
|
|
343
|
-
);
|
|
344
|
-
|
|
345
347
|
return (
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
<div className="h-8 w-8 bg-indigo-600 rounded-lg flex items-center justify-center">
|
|
355
|
-
<Code className="h-5 w-5 text-white" />
|
|
356
|
-
</div>
|
|
357
|
-
<span className="font-bold text-xl tracking-tight">${projectName}</span>
|
|
348
|
+
<div className="min-h-screen bg-zinc-950 text-zinc-50 font-sans selection:bg-indigo-500/30">
|
|
349
|
+
|
|
350
|
+
{/* Header */}
|
|
351
|
+
<header className="border-b border-zinc-900 bg-zinc-950/50 backdrop-blur-xl sticky top-0 z-10">
|
|
352
|
+
<div className="max-w-5xl mx-auto px-6 h-16 flex items-center justify-between">
|
|
353
|
+
<div className="font-bold text-lg tracking-tight flex items-center gap-2">
|
|
354
|
+
<div className="w-3 h-3 rounded-full bg-indigo-500 shadow-[0_0_10px_rgba(99,102,241,0.5)]" />
|
|
355
|
+
<span className="text-zinc-100">${projectName}</span>
|
|
358
356
|
</div>
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
357
|
+
<div className="flex items-center gap-4">
|
|
358
|
+
<span className="text-xs font-mono text-zinc-500 bg-zinc-900 px-2 py-1 rounded border border-zinc-800">
|
|
359
|
+
v0.1.0 (Dev Mode)
|
|
360
|
+
</span>
|
|
361
|
+
<a
|
|
362
|
+
href="https://github.com/danish296/create-solostack"
|
|
363
|
+
target="_blank"
|
|
364
|
+
rel="noreferrer"
|
|
365
|
+
className="text-zinc-400 hover:text-white transition-colors"
|
|
364
366
|
>
|
|
365
|
-
<
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
<
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
<
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
{/* Game UI */}
|
|
389
|
-
<div className="bg-zinc-900/50 backdrop-blur-sm border border-zinc-800 rounded-2xl p-6 shadow-2xl relative overflow-hidden group/game">
|
|
390
|
-
<div className="absolute inset-0 bg-indigo-500/5 group-hover/game:bg-indigo-500/10 transition-colors pointer-events-none" />
|
|
391
|
-
|
|
392
|
-
<div className="grid grid-cols-3 gap-4 mb-6 relative z-10">
|
|
393
|
-
<div className="bg-black/50 p-4 rounded-xl border border-zinc-800">
|
|
394
|
-
<div className="text-zinc-500 text-xs uppercase font-semibold mb-1">MRR</div>
|
|
395
|
-
<div className="text-2xl font-mono text-green-400 flex items-center">
|
|
396
|
-
<DollarSign className="h-5 w-5 mr-1" />
|
|
397
|
-
{mrr}
|
|
398
|
-
</div>
|
|
399
|
-
</div>
|
|
400
|
-
<div className="bg-black/50 p-4 rounded-xl border border-zinc-800">
|
|
401
|
-
<div className="text-zinc-500 text-xs uppercase font-semibold mb-1">Users</div>
|
|
402
|
-
<div className="text-2xl font-mono text-blue-400 flex items-center">
|
|
403
|
-
<Users className="h-5 w-5 mr-1" />
|
|
404
|
-
{users}
|
|
405
|
-
</div>
|
|
406
|
-
</div>
|
|
407
|
-
<div className="bg-black/50 p-4 rounded-xl border border-zinc-800">
|
|
408
|
-
<div className="text-zinc-500 text-xs uppercase font-semibold mb-1">Shipped</div>
|
|
409
|
-
<div className="text-2xl font-mono text-purple-400 flex items-center">
|
|
410
|
-
<Trophy className="h-5 w-5 mr-1" />
|
|
411
|
-
{shipped}
|
|
412
|
-
</div>
|
|
413
|
-
</div>
|
|
414
|
-
</div>
|
|
415
|
-
|
|
416
|
-
<button
|
|
417
|
-
onClick={ship}
|
|
418
|
-
className="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-4 rounded-xl transition-all active:scale-95 shadow-[0_0_20px_rgba(79,70,229,0.3)] hover:shadow-[0_0_30px_rgba(79,70,229,0.5)] flex items-center justify-center gap-3 group relative z-10"
|
|
419
|
-
>
|
|
420
|
-
<Rocket className="h-6 w-6 group-hover:rotate-12 transition-transform" />
|
|
421
|
-
SHIP FEATURE
|
|
422
|
-
</button>
|
|
423
|
-
|
|
424
|
-
<p className="text-center text-zinc-500 text-xs mt-4 relative z-10">
|
|
425
|
-
Click to simulate your indie hacker journey! 🚀
|
|
426
|
-
</p>
|
|
367
|
+
<ExternalLink className="w-4 h-4" />
|
|
368
|
+
</a>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
</header>
|
|
372
|
+
|
|
373
|
+
<main className="max-w-5xl mx-auto px-6 py-12">
|
|
374
|
+
{/* Welcome Section */}
|
|
375
|
+
<div className="mb-12 border-b border-zinc-900 pb-12">
|
|
376
|
+
<h1 className="text-4xl font-bold tracking-tight mb-4 text-white">
|
|
377
|
+
Your Stack is Ready.
|
|
378
|
+
</h1>
|
|
379
|
+
<p className="text-zinc-400 text-lg max-w-2xl leading-relaxed">
|
|
380
|
+
Welcome to your new SaaS application. This page is your
|
|
381
|
+
<span className="text-zinc-200 font-medium"> local development hub</span>.
|
|
382
|
+
Check your integrations, review the tech stack, and start building.
|
|
383
|
+
</p>
|
|
384
|
+
|
|
385
|
+
<div className="mt-8 flex items-center gap-4">
|
|
386
|
+
<div className="flex items-center gap-2 text-sm text-zinc-500 bg-zinc-900/50 px-3 py-2 rounded-md border border-zinc-800 font-mono">
|
|
387
|
+
<Terminal className="w-4 h-4 text-indigo-400" />
|
|
388
|
+
<span>src/app/page.tsx</span>
|
|
427
389
|
</div>
|
|
390
|
+
<span className="text-zinc-600 text-sm">← Edit this file to build your landing page</span>
|
|
428
391
|
</div>
|
|
392
|
+
</div>
|
|
429
393
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
394
|
+
{/* System Status Grid */}
|
|
395
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12">
|
|
396
|
+
<StatusCard
|
|
397
|
+
title="Database"
|
|
398
|
+
icon={<Database className="w-5 h-5 text-emerald-400" />}
|
|
399
|
+
status={status?.database?.connected}
|
|
400
|
+
loading={loading}
|
|
401
|
+
description="PostgreSQL + Prisma"
|
|
402
|
+
details={status?.database?.connected ? \`User count: \${status.database.userCount}\` : status?.database?.error || "Not connected"}
|
|
403
|
+
actionLabel="Open Studio"
|
|
404
|
+
actionCommand="npm run db:studio"
|
|
405
|
+
/>
|
|
406
|
+
<StatusCard
|
|
407
|
+
title="Authentication"
|
|
408
|
+
icon={<Shield className="w-5 h-5 text-blue-400" />}
|
|
409
|
+
status={status?.auth?.configured}
|
|
410
|
+
loading={loading}
|
|
411
|
+
description={status?.auth?.providers?.map((p: any) => p.name).join(', ') || "No providers"}
|
|
412
|
+
details={status?.auth?.configured ? "Ready to authenticate users" : "Missing secrets"}
|
|
413
|
+
actionLabel="Read Docs"
|
|
414
|
+
actionUrl="https://next-auth.js.org"
|
|
415
|
+
/>
|
|
416
|
+
<StatusCard
|
|
417
|
+
title="Payments"
|
|
418
|
+
icon={<CreditCard className="w-5 h-5 text-purple-400" />}
|
|
419
|
+
status={status?.stripe?.configured}
|
|
420
|
+
loading={loading}
|
|
421
|
+
description="Stripe"
|
|
422
|
+
details={status?.stripe?.configured ? "API Keys present" : "Missing API Keys"}
|
|
423
|
+
actionLabel="Dashboard"
|
|
424
|
+
actionUrl="https://dashboard.stripe.com/test/apikeys"
|
|
425
|
+
/>
|
|
426
|
+
<StatusCard
|
|
427
|
+
title="Email"
|
|
428
|
+
icon={<Mail className="w-5 h-5 text-pink-400" />}
|
|
429
|
+
status={status?.email?.configured}
|
|
430
|
+
loading={loading}
|
|
431
|
+
description="Resend"
|
|
432
|
+
details={status?.email?.configured ? "Ready to send" : "Missing API Key"}
|
|
433
|
+
actionLabel="Send Test"
|
|
434
|
+
actionUrl="/setup"
|
|
435
|
+
/>
|
|
436
|
+
</div>
|
|
441
437
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
438
|
+
{/* Resources Section */}
|
|
439
|
+
<div>
|
|
440
|
+
<h2 className="text-xl font-semibold mb-6 flex items-center gap-2">
|
|
441
|
+
<Book className="w-5 h-5 text-zinc-400" />
|
|
442
|
+
Documentation & Resources
|
|
443
|
+
</h2>
|
|
444
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
445
|
+
<ResourceLink
|
|
446
|
+
href="https://nextjs.org/docs"
|
|
447
|
+
title="Next.js 15"
|
|
448
|
+
desc="App Router, Server Actions"
|
|
449
|
+
/>
|
|
450
|
+
<ResourceLink
|
|
451
|
+
href="https://ui.shadcn.com"
|
|
452
|
+
title="shadcn/ui"
|
|
453
|
+
desc="Component Library"
|
|
454
|
+
/>
|
|
455
|
+
<ResourceLink
|
|
456
|
+
href="https://www.prisma.io/docs"
|
|
457
|
+
title="Prisma"
|
|
458
|
+
desc="Database ORM"
|
|
459
|
+
/>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
451
462
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
<h3 className="text-lg font-semibold mb-2 text-white">Payments</h3>
|
|
457
|
-
<p className="text-zinc-400 text-sm">
|
|
458
|
-
Stripe integration with Webhook idempotency and Customer Portal.
|
|
459
|
-
</p>
|
|
460
|
-
</div>
|
|
463
|
+
</main>
|
|
464
|
+
</div>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
461
467
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
Resend + React Email based transactional emails.
|
|
469
|
-
</p>
|
|
470
|
-
</div>
|
|
471
|
-
</div>
|
|
468
|
+
function StatusCard({ title, icon, status, loading, description, details, actionLabel, actionCommand, actionUrl }: any) {
|
|
469
|
+
return (
|
|
470
|
+
<div className="border border-zinc-800 bg-zinc-900/30 rounded-xl p-6 hover:bg-zinc-900/50 transition-colors">
|
|
471
|
+
<div className="flex justify-between items-start mb-4">
|
|
472
|
+
<div className="p-2 bg-zinc-950 rounded-lg border border-zinc-800">
|
|
473
|
+
{icon}
|
|
472
474
|
</div>
|
|
475
|
+
{loading ? (
|
|
476
|
+
<Loader2 className="w-5 h-5 animate-spin text-zinc-600" />
|
|
477
|
+
) : status ? (
|
|
478
|
+
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
|
|
479
|
+
) : (
|
|
480
|
+
<AlertCircle className="w-5 h-5 text-amber-500" />
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
|
|
484
|
+
<h3 className="text-lg font-medium text-white mb-1">{title}</h3>
|
|
485
|
+
<p className="text-sm font-medium text-zinc-300 mb-2">{description}</p>
|
|
486
|
+
|
|
487
|
+
<div className="min-h-[20px] mb-6">
|
|
488
|
+
<p className={\`text-xs \${status ? 'text-zinc-500' : 'text-amber-500/80'}\`}>
|
|
489
|
+
{details}
|
|
490
|
+
</p>
|
|
491
|
+
</div>
|
|
473
492
|
|
|
474
|
-
|
|
475
|
-
|
|
493
|
+
{actionCommand ? (
|
|
494
|
+
<div className="flex items-center justify-between pt-4 border-t border-zinc-800/50">
|
|
495
|
+
<code className="text-xs bg-black px-2 py-1 rounded text-zinc-400 font-mono">
|
|
496
|
+
{actionCommand}
|
|
497
|
+
</code>
|
|
476
498
|
</div>
|
|
499
|
+
) : actionUrl ? (
|
|
500
|
+
<div className="pt-4 border-t border-zinc-800/50">
|
|
501
|
+
{actionUrl.startsWith('http') ? (
|
|
502
|
+
<a href={actionUrl} target="_blank" rel="noreferrer" className="text-xs font-medium text-indigo-400 hover:text-indigo-300 flex items-center gap-1">
|
|
503
|
+
{actionLabel} <ExternalLink className="w-3 h-3" />
|
|
504
|
+
</a>
|
|
505
|
+
) : (
|
|
506
|
+
<Link href={actionUrl} className="text-xs font-medium text-indigo-400 hover:text-indigo-300 flex items-center gap-1">
|
|
507
|
+
{actionLabel} <ChevronRight className="w-3 h-3" />
|
|
508
|
+
</Link>
|
|
509
|
+
)}
|
|
510
|
+
</div>
|
|
511
|
+
) : null}
|
|
512
|
+
</div>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function ResourceLink({ href, title, desc }: any) {
|
|
517
|
+
return (
|
|
518
|
+
<a
|
|
519
|
+
href={href}
|
|
520
|
+
target="_blank"
|
|
521
|
+
rel="noreferrer"
|
|
522
|
+
className="group block p-4 rounded-lg border border-zinc-800 bg-zinc-900/20 hover:bg-zinc-800/50 transition-all"
|
|
523
|
+
>
|
|
524
|
+
<div className="flex items-center justify-between mb-1">
|
|
525
|
+
<span className="font-medium text-zinc-200 group-hover:text-white transition-colors">{title}</span>
|
|
526
|
+
<ExternalLink className="w-3 h-3 text-zinc-600 group-hover:text-zinc-400" />
|
|
477
527
|
</div>
|
|
478
|
-
|
|
528
|
+
<p className="text-xs text-zinc-500">{desc}</p>
|
|
529
|
+
</a>
|
|
479
530
|
);
|
|
480
531
|
}
|
|
481
532
|
`;
|
|
@@ -484,52 +535,57 @@ export default function Home() {
|
|
|
484
535
|
|
|
485
536
|
// Generate .env.example
|
|
486
537
|
const envExample = `# Database
|
|
487
|
-
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
|
|
488
|
-
# For cloud databases, add ?sslmode=require to the connection string
|
|
538
|
+
DATABASE_URL = "postgresql://user:password@localhost:5432/dbname"
|
|
539
|
+
# For cloud databases, add ? sslmode = require to the connection string
|
|
489
540
|
|
|
490
541
|
# NextAuth
|
|
491
|
-
NEXTAUTH_SECRET="" # Generate with: openssl rand -base64 32
|
|
492
|
-
NEXTAUTH_URL="http://localhost:3000"
|
|
542
|
+
NEXTAUTH_SECRET = "" # Generate with: openssl rand - base64 32
|
|
543
|
+
NEXTAUTH_URL = "http://localhost:3000"
|
|
493
544
|
# In production, set to your domain: https://yourdomain.com
|
|
494
545
|
|
|
495
546
|
# Stripe
|
|
496
|
-
STRIPE_SECRET_KEY="sk_test_..."
|
|
497
|
-
STRIPE_WEBHOOK_SECRET="whsec_..."
|
|
498
|
-
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."
|
|
547
|
+
STRIPE_SECRET_KEY = "sk_test_..."
|
|
548
|
+
STRIPE_WEBHOOK_SECRET = "whsec_..."
|
|
549
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY = "pk_test_..."
|
|
499
550
|
# Create price IDs in Stripe Dashboard -> Products
|
|
500
|
-
STRIPE_PRO_PRICE_ID="price_..." # Monthly Pro plan price ID
|
|
501
|
-
STRIPE_ENTERPRISE_PRICE_ID="price_..." # Monthly Enterprise plan price ID
|
|
551
|
+
STRIPE_PRO_PRICE_ID = "price_..." # Monthly Pro plan price ID
|
|
552
|
+
STRIPE_ENTERPRISE_PRICE_ID = "price_..." # Monthly Enterprise plan price ID
|
|
502
553
|
|
|
503
554
|
# Resend
|
|
504
|
-
RESEND_API_KEY="re_..."
|
|
505
|
-
FROM_EMAIL="onboarding@resend.dev" # Use your verified domain
|
|
555
|
+
RESEND_API_KEY = "re_..."
|
|
556
|
+
FROM_EMAIL = "onboarding@resend.dev" # Use your verified domain
|
|
506
557
|
|
|
507
|
-
# OAuth Providers
|
|
558
|
+
# OAuth Providers(Optional)
|
|
508
559
|
# Google: https://console.cloud.google.com/apis/credentials
|
|
509
|
-
GOOGLE_CLIENT_ID=""
|
|
510
|
-
GOOGLE_CLIENT_SECRET=""
|
|
560
|
+
GOOGLE_CLIENT_ID = ""
|
|
561
|
+
GOOGLE_CLIENT_SECRET = ""
|
|
511
562
|
# GitHub: https://github.com/settings/developers
|
|
512
|
-
GITHUB_CLIENT_ID=""
|
|
513
|
-
GITHUB_CLIENT_SECRET=""
|
|
514
|
-
|
|
563
|
+
GITHUB_CLIENT_ID = ""
|
|
564
|
+
GITHUB_CLIENT_SECRET = ""${config?.auth === 'Supabase Auth' || config?.database === 'Supabase' ? `
|
|
565
|
+
|
|
566
|
+
# Supabase
|
|
567
|
+
NEXT_PUBLIC_SUPABASE_URL="https://your-project.supabase.co"
|
|
568
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY="your-anon-key"` : ''
|
|
569
|
+
}
|
|
570
|
+
`;
|
|
515
571
|
|
|
516
572
|
await writeFile(path.join(projectPath, '.env.example'), envExample);
|
|
517
573
|
|
|
518
574
|
// Generate README.md
|
|
519
|
-
const readme =
|
|
575
|
+
const readme = "# " + projectName + `
|
|
520
576
|
|
|
521
577
|
Built with [SoloStack](https://github.com/yourusername/create-solostack) - The complete SaaS boilerplate for indie hackers.
|
|
522
578
|
|
|
523
579
|
## Features
|
|
524
580
|
|
|
525
|
-
-
|
|
526
|
-
-
|
|
527
|
-
-
|
|
528
|
-
-
|
|
529
|
-
-
|
|
530
|
-
-
|
|
531
|
-
-
|
|
532
|
-
-
|
|
581
|
+
- ✅ ** Next.js 15 ** with App Router
|
|
582
|
+
- ✅ ** TypeScript ** for type safety
|
|
583
|
+
- ✅ ** Tailwind CSS ** for styling
|
|
584
|
+
- ✅ ** Prisma ** + PostgreSQL for database
|
|
585
|
+
- ✅ ** NextAuth.js ** for authentication
|
|
586
|
+
- ✅ ** Stripe ** for payments
|
|
587
|
+
- ✅ ** Resend ** for emails
|
|
588
|
+
- ✅ ** shadcn / ui ** components
|
|
533
589
|
|
|
534
590
|
## Getting Started
|
|
535
591
|
|
|
@@ -566,16 +622,16 @@ Open [http://localhost:3000](http://localhost:3000) with your browser.
|
|
|
566
622
|
|
|
567
623
|
\`\`\`
|
|
568
624
|
${projectName}/
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
625
|
+
├── prisma/ # Database schema and migrations
|
|
626
|
+
├── public/ # Static assets
|
|
627
|
+
├── src/
|
|
628
|
+
│ ├── app/ # Next.js app directory
|
|
629
|
+
│ │ ├── api/ # API routes
|
|
630
|
+
│ │ ├── (auth)/ # Auth pages
|
|
631
|
+
│ │ └── dashboard/ # Protected pages
|
|
632
|
+
│ ├── components/ # React components
|
|
633
|
+
│ └── lib/ # Utilities and configurations
|
|
634
|
+
└── package.json
|
|
579
635
|
\`\`\`
|
|
580
636
|
|
|
581
637
|
## 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
|