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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-solostack",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "The complete SaaS boilerplate for indie hackers - Next.js 15 with auth, payments, database, and email",
5
5
  "type": "module",
6
6
  "bin": {
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
  /**
@@ -2,11 +2,315 @@ import path from 'path';
2
2
  import { writeFile, ensureDir } from '../utils/files.js';
3
3
 
4
4
  /**
5
- * Generates NextAuth.js authentication configuration
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'));
@@ -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 { Rocket, Users, DollarSign, Code, Database, CreditCard, Mail, Lock, CheckCircle2, Trophy } from 'lucide-react';
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 [mounted, setMounted] = useState(false);
319
- const [mrr, setMrr] = useState(0);
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
- setMounted(true);
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
- <main className="min-h-screen bg-black text-white font-sans selection:bg-indigo-500/30 overflow-hidden">
347
- {/* Background Gradient */}
348
- <div className="fixed inset-0 z-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-indigo-900/20 via-black to-black pointer-events-none" />
349
-
350
- <div className="relative z-10 max-w-6xl mx-auto px-4 py-12">
351
- {/* Header */}
352
- <header className="flex items-center justify-between mb-16">
353
- <div className="flex items-center gap-2">
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
- {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"
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
- <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>
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
- {/* Cards Section */}
431
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
432
- <div className="p-6 rounded-2xl bg-zinc-900/30 border border-zinc-800 hover:border-zinc-600 transition-colors group">
433
- <div className="h-10 w-10 bg-zinc-800 rounded-lg flex items-center justify-center mb-4 group-hover:bg-zinc-700 transition-colors">
434
- <Lock className="h-5 w-5 text-indigo-400" />
435
- </div>
436
- <h3 className="text-lg font-semibold mb-2 text-white">Authentication</h3>
437
- <p className="text-zinc-400 text-sm">
438
- NextAuth.js v5 pre-configured with Google & GitHub OAuth.
439
- </p>
440
- </div>
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
- <div className="p-6 rounded-2xl bg-zinc-900/30 border border-zinc-800 hover:border-zinc-600 transition-colors group">
443
- <div className="h-10 w-10 bg-zinc-800 rounded-lg flex items-center justify-center mb-4 group-hover:bg-zinc-700 transition-colors">
444
- <Database className="h-5 w-5 text-emerald-400" />
445
- </div>
446
- <h3 className="text-lg font-semibold mb-2 text-white">Database</h3>
447
- <p className="text-zinc-400 text-sm">
448
- Prisma ORM with PostgreSQL. Complete schema for Users and Subs.
449
- </p>
450
- </div>
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
- <div className="p-6 rounded-2xl bg-zinc-900/30 border border-zinc-800 hover:border-zinc-600 transition-colors group">
453
- <div className="h-10 w-10 bg-zinc-800 rounded-lg flex items-center justify-center mb-4 group-hover:bg-zinc-700 transition-colors">
454
- <CreditCard className="h-5 w-5 text-pink-400" />
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>
463
+ </main>
464
+ </div>
465
+ );
466
+ }
461
467
 
462
- <div className="p-6 rounded-2xl bg-zinc-900/30 border border-zinc-800 hover:border-zinc-600 transition-colors group">
463
- <div className="h-10 w-10 bg-zinc-800 rounded-lg flex items-center justify-center mb-4 group-hover:bg-zinc-700 transition-colors">
464
- <Mail className="h-5 w-5 text-blue-400" />
465
- </div>
466
- <h3 className="text-lg font-semibold mb-2 text-white">Emails</h3>
467
- <p className="text-zinc-400 text-sm">
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
- <div className="text-center text-zinc-500 text-sm">
475
- Get started by editing <code className="bg-zinc-900 border border-zinc-700 px-2 py-1 rounded text-zinc-300 font-mono">src/app/page.tsx</code>
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
- </main>
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 (Optional)
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 = `# ${projectName}
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
- - **Next.js 15** with App Router
526
- - **TypeScript** for type safety
527
- - **Tailwind CSS** for styling
528
- - **Prisma** + PostgreSQL for database
529
- - **NextAuth.js** for authentication
530
- - **Stripe** for payments
531
- - **Resend** for emails
532
- - **shadcn/ui** components
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
- ├── prisma/ # Database schema and migrations
570
- ├── public/ # Static assets
571
- ├── src/
572
- ├── app/ # Next.js app directory
573
- ├── api/ # API routes
574
- ├── (auth)/ # Auth pages
575
- └── dashboard/ # Protected pages
576
- ├── components/ # React components
577
- └── lib/ # Utilities and configurations
578
- └── package.json
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 {string} database - Database type (currently only PostgreSQL supported)
7
+ * @param {object} config - Configuration object
8
8
  */
9
- export async function generateDatabase(projectPath, database) {
9
+ export async function generateDatabase(projectPath, config) {
10
10
  // Create prisma directory
11
11
  await ensureDir(path.join(projectPath, 'prisma'));
12
12
 
13
- // Generate schema.prisma
14
- const schemaPrisma = `generator client {
15
- provider = "prisma-client-js"
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
- datasource db {
19
- provider = "postgresql"
20
- url = env("DATABASE_URL")
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
- const seedScript = `import { PrismaClient } from '@prisma/client';
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.database);
129
+ await generateDatabase(projectPath, config);
130
130
  spinner.succeed('Configured database (Prisma + PostgreSQL)');
131
131
 
132
132
  // Generate authentication