create-solostack 1.2.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 +222 -179
- 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,204 @@ export default function RootLayout({
|
|
|
312
316
|
|
|
313
317
|
import { useState, useEffect } from 'react';
|
|
314
318
|
import Link from 'next/link';
|
|
315
|
-
import { Rocket, Users, DollarSign, Code, Database, CreditCard, Mail, Lock, CheckCircle2, Trophy } from 'lucide-react';
|
|
319
|
+
import { Rocket, Users, DollarSign, Code, Database, CreditCard, Mail, Lock, CheckCircle2, Trophy, ArrowRight, Play, TrendingUp, Box } from 'lucide-react';
|
|
316
320
|
|
|
317
321
|
export default function Home() {
|
|
318
322
|
const [mounted, setMounted] = useState(false);
|
|
319
|
-
const [
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
+
const [stats, setStats] = useState({
|
|
324
|
+
mrr: 0,
|
|
325
|
+
users: 0,
|
|
326
|
+
shipped: 0
|
|
327
|
+
});
|
|
323
328
|
|
|
324
329
|
useEffect(() => {
|
|
325
330
|
setMounted(true);
|
|
326
331
|
}, []);
|
|
327
332
|
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (Math.random() > 0.7) {
|
|
335
|
-
setMrr(prev => prev + Math.floor(Math.random() * 20) + 10);
|
|
336
|
-
}
|
|
333
|
+
const incrementStats = () => {
|
|
334
|
+
setStats(prev => ({
|
|
335
|
+
mrr: prev.mrr + 100,
|
|
336
|
+
users: prev.users + 5,
|
|
337
|
+
shipped: prev.shipped + 1
|
|
338
|
+
}));
|
|
337
339
|
};
|
|
338
340
|
|
|
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
|
-
);
|
|
341
|
+
if (!mounted) return null;
|
|
344
342
|
|
|
345
343
|
return (
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
<Code className="h-5 w-5 text-white" />
|
|
356
|
-
</div>
|
|
357
|
-
<span className="font-bold text-xl tracking-tight">${projectName}</span>
|
|
358
|
-
</div>
|
|
359
|
-
|
|
360
|
-
{process.env.NODE_ENV === 'development' && (
|
|
361
|
-
<Link
|
|
362
|
-
href="/setup"
|
|
363
|
-
className="text-sm text-zinc-400 hover:text-white transition-colors flex items-center gap-2 border border-zinc-800 rounded-full px-4 py-1.5 hover:border-zinc-600"
|
|
364
|
-
>
|
|
365
|
-
<CheckCircle2 className="h-4 w-4" />
|
|
366
|
-
Check Diagnostics
|
|
367
|
-
</Link>
|
|
368
|
-
)}
|
|
369
|
-
</header>
|
|
370
|
-
|
|
371
|
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center mb-24">
|
|
372
|
-
{/* Game Section */}
|
|
373
|
-
<div className="space-y-8">
|
|
374
|
-
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-indigo-500/10 text-indigo-400 text-sm border border-indigo-500/20">
|
|
375
|
-
<Rocket className="h-4 w-4" />
|
|
376
|
-
<span>Interactive SaaS Simulator</span>
|
|
377
|
-
</div>
|
|
378
|
-
|
|
379
|
-
<h1 className="text-5xl md:text-6xl font-bold tracking-tight leading-none bg-clip-text text-transparent bg-gradient-to-r from-white to-zinc-500">
|
|
380
|
-
Build your SaaS <br />
|
|
381
|
-
<span className="text-indigo-500">in minutes.</span>
|
|
382
|
-
</h1>
|
|
383
|
-
|
|
384
|
-
<p className="text-xl text-zinc-400 max-w-lg leading-relaxed">
|
|
385
|
-
The ultimate Next.js 15 boilerplate for indie hackers. Authentication, Database, Payments, and Email - pre-configured and ready to ship.
|
|
386
|
-
</p>
|
|
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>
|
|
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" />
|
|
414
353
|
</div>
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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"
|
|
419
375
|
>
|
|
420
|
-
|
|
421
|
-
|
|
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>
|
|
376
|
+
Get Started
|
|
377
|
+
</Link>
|
|
427
378
|
</div>
|
|
428
379
|
</div>
|
|
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
|
|
390
|
+
</div>
|
|
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>
|
|
416
|
+
</div>
|
|
429
417
|
|
|
430
|
-
{/*
|
|
431
|
-
<div className="
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
451
469
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
</div>
|
|
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>
|
|
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>
|
|
461
473
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
<
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
+
/>
|
|
471
498
|
</div>
|
|
472
499
|
</div>
|
|
500
|
+
</div>
|
|
473
501
|
|
|
474
|
-
|
|
475
|
-
|
|
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>
|
|
476
514
|
</div>
|
|
477
|
-
</
|
|
478
|
-
</
|
|
515
|
+
</footer>
|
|
516
|
+
</div>
|
|
479
517
|
);
|
|
480
518
|
}
|
|
481
519
|
`;
|
|
@@ -484,52 +522,57 @@ export default function Home() {
|
|
|
484
522
|
|
|
485
523
|
// Generate .env.example
|
|
486
524
|
const envExample = `# Database
|
|
487
|
-
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
|
|
488
|
-
# 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
|
|
489
527
|
|
|
490
528
|
# NextAuth
|
|
491
|
-
NEXTAUTH_SECRET="" # Generate with: openssl rand -base64 32
|
|
492
|
-
NEXTAUTH_URL="http://localhost:3000"
|
|
529
|
+
NEXTAUTH_SECRET = "" # Generate with: openssl rand - base64 32
|
|
530
|
+
NEXTAUTH_URL = "http://localhost:3000"
|
|
493
531
|
# In production, set to your domain: https://yourdomain.com
|
|
494
532
|
|
|
495
533
|
# Stripe
|
|
496
|
-
STRIPE_SECRET_KEY="sk_test_..."
|
|
497
|
-
STRIPE_WEBHOOK_SECRET="whsec_..."
|
|
498
|
-
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_..."
|
|
499
537
|
# 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
|
|
538
|
+
STRIPE_PRO_PRICE_ID = "price_..." # Monthly Pro plan price ID
|
|
539
|
+
STRIPE_ENTERPRISE_PRICE_ID = "price_..." # Monthly Enterprise plan price ID
|
|
502
540
|
|
|
503
541
|
# Resend
|
|
504
|
-
RESEND_API_KEY="re_..."
|
|
505
|
-
FROM_EMAIL="onboarding@resend.dev" # Use your verified domain
|
|
542
|
+
RESEND_API_KEY = "re_..."
|
|
543
|
+
FROM_EMAIL = "onboarding@resend.dev" # Use your verified domain
|
|
506
544
|
|
|
507
|
-
# OAuth Providers
|
|
545
|
+
# OAuth Providers(Optional)
|
|
508
546
|
# Google: https://console.cloud.google.com/apis/credentials
|
|
509
|
-
GOOGLE_CLIENT_ID=""
|
|
510
|
-
GOOGLE_CLIENT_SECRET=""
|
|
547
|
+
GOOGLE_CLIENT_ID = ""
|
|
548
|
+
GOOGLE_CLIENT_SECRET = ""
|
|
511
549
|
# GitHub: https://github.com/settings/developers
|
|
512
|
-
GITHUB_CLIENT_ID=""
|
|
513
|
-
GITHUB_CLIENT_SECRET=""
|
|
514
|
-
|
|
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
|
+
`;
|
|
515
558
|
|
|
516
559
|
await writeFile(path.join(projectPath, '.env.example'), envExample);
|
|
517
560
|
|
|
518
561
|
// Generate README.md
|
|
519
|
-
const readme =
|
|
562
|
+
const readme = "# " + projectName + `
|
|
520
563
|
|
|
521
564
|
Built with [SoloStack](https://github.com/yourusername/create-solostack) - The complete SaaS boilerplate for indie hackers.
|
|
522
565
|
|
|
523
566
|
## Features
|
|
524
567
|
|
|
525
|
-
-
|
|
526
|
-
-
|
|
527
|
-
-
|
|
528
|
-
-
|
|
529
|
-
-
|
|
530
|
-
-
|
|
531
|
-
-
|
|
532
|
-
-
|
|
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
|
|
533
576
|
|
|
534
577
|
## Getting Started
|
|
535
578
|
|
|
@@ -566,16 +609,16 @@ Open [http://localhost:3000](http://localhost:3000) with your browser.
|
|
|
566
609
|
|
|
567
610
|
\`\`\`
|
|
568
611
|
${projectName}/
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
|
579
622
|
\`\`\`
|
|
580
623
|
|
|
581
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
|