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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-solostack",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
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,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 [mrr, setMrr] = useState(0);
320
- const [users, setUsers] = useState(0);
321
- const [clicks, setClicks] = useState(0);
322
- const [shipped, setShipped] = useState(0);
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 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
- }
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
- <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>
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
- <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"
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
- <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>
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
- {/* 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>
441
-
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>
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
- <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>
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
- <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>
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
- <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>
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
- </div>
478
- </main>
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 (Optional)
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 = `# ${projectName}
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
- - **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
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
- ├── 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
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 {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