create-supabase-saas 1.0.0
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/README.md +52 -0
- package/bin/index.js +169 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# create-supabase-saas
|
|
2
|
+
|
|
3
|
+
> Scaffold a production-ready Next.js + Supabase SaaS app in seconds.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx create-supabase-saas my-app
|
|
7
|
+
# or
|
|
8
|
+
npm create supabase-saas my-app
|
|
9
|
+
# or
|
|
10
|
+
yarn create supabase-saas my-app
|
|
11
|
+
# or
|
|
12
|
+
pnpm create supabase-saas my-app
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What you get
|
|
16
|
+
|
|
17
|
+
- **Auth** — Email/password + Google OAuth + forgot/reset password (`@supabase/ssr`)
|
|
18
|
+
- **Multi-tenant orgs** — owner / admin / member / viewer roles with full RLS
|
|
19
|
+
- **Stripe** — Checkout, Billing Portal, 4 webhook events
|
|
20
|
+
- **Emails** — Welcome, invite, invoice (React Email + Resend)
|
|
21
|
+
- **Notifications bell** — In-app notifications with unread badge
|
|
22
|
+
- **API key management** — SHA-256 hashed, revocable
|
|
23
|
+
- **Audit log** — Append-only with SECURITY DEFINER
|
|
24
|
+
- **TypeScript strict** — `useUser`, `useSubscription`, `useOrg` hooks
|
|
25
|
+
|
|
26
|
+
**65 files. Zero configuration to start.**
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx create-supabase-saas my-saas
|
|
32
|
+
|
|
33
|
+
cd my-saas
|
|
34
|
+
|
|
35
|
+
# 1. Fill in your keys
|
|
36
|
+
cp .env.example .env.local # already done by the CLI
|
|
37
|
+
# edit .env.local → NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, STRIPE_SECRET_KEY, RESEND_API_KEY
|
|
38
|
+
|
|
39
|
+
# 2. Push DB migrations
|
|
40
|
+
supabase db push
|
|
41
|
+
|
|
42
|
+
# 3. Run
|
|
43
|
+
npm run dev
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Template source
|
|
47
|
+
|
|
48
|
+
[github.com/Edraid/nextjs-supabase-saas-starter](https://github.com/Edraid/nextjs-supabase-saas-starter)
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
MIT
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import prompts from 'prompts';
|
|
5
|
+
import degit from 'degit';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
const TEMPLATE_REPO = 'Edraid/nextjs-supabase-saas-starter';
|
|
11
|
+
const REPO_URL = 'https://github.com/Edraid/nextjs-supabase-saas-starter';
|
|
12
|
+
|
|
13
|
+
// ─── Banner ───────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
console.log(`
|
|
16
|
+
${pc.bold(pc.cyan('create-supabase-saas'))} ${pc.dim('v1.0.0')}
|
|
17
|
+
|
|
18
|
+
${pc.green('▸')} Next.js 15 App Router
|
|
19
|
+
${pc.green('▸')} Supabase Auth + Multi-tenant orgs
|
|
20
|
+
${pc.green('▸')} Stripe Checkout + Billing Portal
|
|
21
|
+
${pc.green('▸')} React Email + Resend
|
|
22
|
+
${pc.green('▸')} TypeScript strict + full RLS
|
|
23
|
+
`);
|
|
24
|
+
|
|
25
|
+
// ─── Get project name from args or prompt ─────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
let projectName = process.argv[2];
|
|
28
|
+
|
|
29
|
+
if (!projectName) {
|
|
30
|
+
const response = await prompts(
|
|
31
|
+
{
|
|
32
|
+
type: 'text',
|
|
33
|
+
name: 'projectName',
|
|
34
|
+
message: 'What is your project named?',
|
|
35
|
+
initial: 'my-saas-app',
|
|
36
|
+
validate: (v) =>
|
|
37
|
+
/^[a-z0-9-_]+$/.test(v) || 'Only lowercase letters, numbers, hyphens and underscores',
|
|
38
|
+
},
|
|
39
|
+
{ onCancel: () => process.exit(0) }
|
|
40
|
+
);
|
|
41
|
+
projectName = response.projectName;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!projectName) process.exit(0);
|
|
45
|
+
|
|
46
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
47
|
+
|
|
48
|
+
// ─── Check target dir ─────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
if (fs.existsSync(targetDir)) {
|
|
51
|
+
const { overwrite } = await prompts(
|
|
52
|
+
{
|
|
53
|
+
type: 'confirm',
|
|
54
|
+
name: 'overwrite',
|
|
55
|
+
message: `Directory ${pc.yellow(projectName)} already exists. Overwrite?`,
|
|
56
|
+
initial: false,
|
|
57
|
+
},
|
|
58
|
+
{ onCancel: () => process.exit(0) }
|
|
59
|
+
);
|
|
60
|
+
if (!overwrite) {
|
|
61
|
+
console.log(pc.red('\nAborted.'));
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Package manager ──────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
const { pkgManager } = await prompts(
|
|
70
|
+
{
|
|
71
|
+
type: 'select',
|
|
72
|
+
name: 'pkgManager',
|
|
73
|
+
message: 'Which package manager do you use?',
|
|
74
|
+
choices: [
|
|
75
|
+
{ title: 'npm', value: 'npm' },
|
|
76
|
+
{ title: 'pnpm', value: 'pnpm' },
|
|
77
|
+
{ title: 'yarn', value: 'yarn' },
|
|
78
|
+
{ title: 'bun', value: 'bun' },
|
|
79
|
+
],
|
|
80
|
+
initial: 0,
|
|
81
|
+
},
|
|
82
|
+
{ onCancel: () => process.exit(0) }
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// ─── Clone template ───────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
console.log(`\n${pc.cyan('◆')} Cloning template into ${pc.bold(projectName)}...`);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const emitter = degit(TEMPLATE_REPO, {
|
|
91
|
+
cache: false,
|
|
92
|
+
force: true,
|
|
93
|
+
verbose: false,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await emitter.clone(targetDir);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(pc.red(`\n✖ Failed to clone template: ${err.message}`));
|
|
99
|
+
console.error(pc.dim(` You can manually clone: git clone ${REPO_URL} ${projectName}`));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Update package.json with project name ────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
106
|
+
if (fs.existsSync(pkgPath)) {
|
|
107
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
108
|
+
pkg.name = projectName;
|
|
109
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Rename .env.example to .env.local ────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
const envExample = path.join(targetDir, '.env.example');
|
|
115
|
+
const envLocal = path.join(targetDir, '.env.local');
|
|
116
|
+
if (fs.existsSync(envExample) && !fs.existsSync(envLocal)) {
|
|
117
|
+
fs.copyFileSync(envExample, envLocal);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Install deps? ────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
const { install } = await prompts(
|
|
123
|
+
{
|
|
124
|
+
type: 'confirm',
|
|
125
|
+
name: 'install',
|
|
126
|
+
message: `Install dependencies with ${pc.bold(pkgManager)}?`,
|
|
127
|
+
initial: true,
|
|
128
|
+
},
|
|
129
|
+
{ onCancel: () => process.exit(0) }
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (install) {
|
|
133
|
+
console.log(`\n${pc.cyan('◆')} Installing dependencies...\n`);
|
|
134
|
+
try {
|
|
135
|
+
const installCmd = pkgManager === 'yarn' ? 'yarn' : `${pkgManager} install`;
|
|
136
|
+
execSync(installCmd, { cwd: targetDir, stdio: 'inherit' });
|
|
137
|
+
} catch {
|
|
138
|
+
console.log(pc.yellow('\n⚠ Install failed — run it manually inside the project folder.\n'));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Success ──────────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
const cdCmd = projectName === '.' ? '' : ` cd ${projectName}\n`;
|
|
145
|
+
const runCmd = pkgManager === 'npm' ? 'npm run dev' :
|
|
146
|
+
pkgManager === 'yarn' ? 'yarn dev' :
|
|
147
|
+
`${pkgManager} dev`;
|
|
148
|
+
|
|
149
|
+
console.log(`
|
|
150
|
+
${pc.green('✔')} ${pc.bold('Project created!')}
|
|
151
|
+
|
|
152
|
+
${pc.bold('Next steps:')}
|
|
153
|
+
|
|
154
|
+
${cdCmd} 1. Fill in ${pc.cyan('.env.local')} with your Supabase + Stripe + Resend keys
|
|
155
|
+
2. Run Supabase migrations:
|
|
156
|
+
${pc.dim('supabase db push')}
|
|
157
|
+
3. Start the dev server:
|
|
158
|
+
${pc.cyan(runCmd)}
|
|
159
|
+
|
|
160
|
+
${pc.bold('Docs & config:')}
|
|
161
|
+
${pc.dim('→')} ${pc.underline(REPO_URL)}
|
|
162
|
+
${pc.dim('→')} CUSTOMIZATION.md (theming, auth providers, Stripe plans)
|
|
163
|
+
${pc.dim('→')} CHANGELOG.md (what's new in v1.0.0)
|
|
164
|
+
|
|
165
|
+
${pc.bold('Need the RLS-only pack?')}
|
|
166
|
+
${pc.dim('→')} ${pc.underline('https://edraid-dev.lemonsqueezy.com')}
|
|
167
|
+
|
|
168
|
+
${pc.dim('Happy building! ⚡')}
|
|
169
|
+
`);
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-supabase-saas",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create a Next.js + Supabase SaaS app with auth, multi-tenant orgs, Stripe, and emails in seconds",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nextjs",
|
|
7
|
+
"supabase",
|
|
8
|
+
"saas",
|
|
9
|
+
"starter",
|
|
10
|
+
"boilerplate",
|
|
11
|
+
"stripe",
|
|
12
|
+
"multi-tenant",
|
|
13
|
+
"create-app"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/Edraid/nextjs-supabase-saas-starter",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/Edraid/create-supabase-saas.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Edraid",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"create-supabase-saas": "bin/index.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"degit": "^2.8.4",
|
|
31
|
+
"picocolors": "^1.1.1",
|
|
32
|
+
"prompts": "^2.4.2"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|