@0x0501/mystack 0.0.0-stage → 0.1.0-bootstrap.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/LICENSE +21 -0
- package/README.md +68 -2
- package/assets/auth.sqlite.schema.ts +479 -0
- package/assets/bun-sqlite.test.ts +46 -0
- package/assets/d1.test.ts +41 -0
- package/assets/drizzle.config.ts +2 -0
- package/assets/generate-auth-schema.mjs +25 -0
- package/assets/platform.sqlite.schema.ts +46 -0
- package/assets/polar-checkout.ts +10 -0
- package/assets/polar-route.ts +19 -0
- package/assets/polar.test.ts +30 -0
- package/assets/polar.ts +12 -0
- package/assets/purchase.bun-sqlite.ts +38 -0
- package/assets/purchase.sqlite.ts +39 -0
- package/assets/sqlite-migrations/0000_thin_phalanx.sql +295 -0
- package/assets/sqlite-migrations/meta/0000_snapshot.json +2058 -0
- package/assets/sqlite-migrations/meta/_journal.json +13 -0
- package/assets/turso.test.ts +40 -0
- package/package.json +44 -5
- package/src/adapters/database.mjs +448 -0
- package/src/adapters/deployment.mjs +101 -0
- package/src/adapters/email.mjs +75 -0
- package/src/adapters/payments.mjs +270 -0
- package/src/catalog.mjs +128 -0
- package/src/cli.mjs +294 -0
- package/src/generate.mjs +183 -0
- package/src/template.mjs +93 -0
- package/template/.env.docker +16 -0
- package/template/.env.example +67 -0
- package/template/DESIGN.md +58 -0
- package/template/LICENSE +21 -0
- package/template/biome.json +46 -0
- package/template/docker-compose.yml +47 -0
- package/template/docs/project-guide.md +61 -0
- package/template/drizzle/0000_create-app-role.sql +54 -0
- package/template/drizzle/0001_add-rls-context.sql +21 -0
- package/template/drizzle/0002_platform_tables.sql +320 -0
- package/template/drizzle/meta/0002_snapshot.json +2125 -0
- package/template/drizzle/meta/_journal.json +27 -0
- package/template/drizzle.config.ts +25 -0
- package/template/e2e/authenticated-ui.spec.ts +40 -0
- package/template/e2e/headless-design.spec.ts +66 -0
- package/template/e2e/navigation-loading.spec.ts +70 -0
- package/template/e2e/theme-responsive.spec.ts +260 -0
- package/template/gitignore +24 -0
- package/template/messages/de.json +212 -0
- package/template/messages/en.json +212 -0
- package/template/package.json +101 -0
- package/template/paraglide.config.ts +49 -0
- package/template/playwright.config.ts +58 -0
- package/template/project.inlang/settings.json +12 -0
- package/template/public/drizzle.svg +1 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/logo192.png +0 -0
- package/template/public/logo512.png +0 -0
- package/template/public/manifest.json +25 -0
- package/template/public/robots.txt +3 -0
- package/template/scripts/check-client-bundle.ts +150 -0
- package/template/scripts/check-server-fns.ts +290 -0
- package/template/scripts/check-sql-raw.ts +50 -0
- package/template/scripts/compile-paraglide.ts +12 -0
- package/template/scripts/postgres/README.md +76 -0
- package/template/scripts/postgres/create-app-role.sh +60 -0
- package/template/scripts/seed-local.ts +331 -0
- package/template/scripts/validate-prod-env.ts +53 -0
- package/template/src/__tests__/README.md +9 -0
- package/template/src/__tests__/account-security.test.ts +664 -0
- package/template/src/__tests__/admin-http-surface.test.ts +311 -0
- package/template/src/__tests__/admin-peer-protection.test.ts +178 -0
- package/template/src/__tests__/auth-harness.ts +104 -0
- package/template/src/__tests__/auth-rate-limit.test.ts +43 -0
- package/template/src/__tests__/auth-redirect.test.ts +120 -0
- package/template/src/__tests__/body-limit.test.ts +47 -0
- package/template/src/__tests__/creem.test.ts +62 -0
- package/template/src/__tests__/demo-checkout.test.ts +141 -0
- package/template/src/__tests__/email-locale.test.ts +110 -0
- package/template/src/__tests__/global-setup.ts +80 -0
- package/template/src/__tests__/input-rules.test.ts +0 -0
- package/template/src/__tests__/message-parity.test.ts +38 -0
- package/template/src/__tests__/navigation-loading.test.tsx +252 -0
- package/template/src/__tests__/nowpayments.test.ts +106 -0
- package/template/src/__tests__/paraglide-url-contract.test.ts +87 -0
- package/template/src/__tests__/purchase-fulfillment.test.ts +204 -0
- package/template/src/__tests__/query-unauthorized-redirect.test.ts +95 -0
- package/template/src/__tests__/rls-policy.test.ts +120 -0
- package/template/src/__tests__/same-page-locale-href.test.ts +47 -0
- package/template/src/__tests__/search-params-oauth.test.ts +57 -0
- package/template/src/__tests__/security-headers.test.ts +12 -0
- package/template/src/__tests__/server-fn-error-contract.test.ts +173 -0
- package/template/src/__tests__/stripe.test.ts +84 -0
- package/template/src/__tests__/test-db.ts +32 -0
- package/template/src/__tests__/test-rls-db.ts +23 -0
- package/template/src/__tests__/user-update-guard.test.ts +109 -0
- package/template/src/components/LocaleSwitcher.tsx +79 -0
- package/template/src/components/ThemeToggle.tsx +47 -0
- package/template/src/components/auth/social.tsx +90 -0
- package/template/src/components/auth/turnstile.tsx +67 -0
- package/template/src/components/root/navigation-progress.tsx +32 -0
- package/template/src/components/root/not-found.tsx +20 -0
- package/template/src/components/root/pending.tsx +9 -0
- package/template/src/components/root/root-document.tsx +37 -0
- package/template/src/components/root/root-error.tsx +34 -0
- package/template/src/components/shells.tsx +201 -0
- package/template/src/components/ui/styles.ts +9 -0
- package/template/src/db/auth.schema.ts +455 -0
- package/template/src/db/helper.ts +38 -0
- package/template/src/db/index.ts +15 -0
- package/template/src/db/platform.schema.ts +122 -0
- package/template/src/db/schema.ts +2 -0
- package/template/src/env.ts +65 -0
- package/template/src/hooks/use-theme.ts +32 -0
- package/template/src/integrations/better-auth/header-user.tsx +36 -0
- package/template/src/integrations/better-auth/session-query.ts +11 -0
- package/template/src/integrations/react-email/PasswordOtpEmail.tsx +62 -0
- package/template/src/integrations/react-email/ResetPasswordEmail.tsx +64 -0
- package/template/src/integrations/react-email/VerifyEmail.tsx +72 -0
- package/template/src/integrations/react-email/theme.ts +75 -0
- package/template/src/integrations/tanstack-query/devtools.tsx +6 -0
- package/template/src/integrations/tanstack-query/root-provider.tsx +60 -0
- package/template/src/lib/auth-client.ts +27 -0
- package/template/src/lib/auth-paths.ts +94 -0
- package/template/src/lib/auth-plugins/existing-email-signup.ts +86 -0
- package/template/src/lib/auth-plugins/field-rules.ts +84 -0
- package/template/src/lib/auth-plugins/social-two-factor.ts +194 -0
- package/template/src/lib/auth-timing.ts +9 -0
- package/template/src/lib/auth.ts +279 -0
- package/template/src/lib/captcha.ts +11 -0
- package/template/src/lib/email.tsx +162 -0
- package/template/src/lib/github.ts +18 -0
- package/template/src/lib/locale-cookie.ts +12 -0
- package/template/src/lib/localized-pages.ts +25 -0
- package/template/src/lib/oauth-metadata.ts +19 -0
- package/template/src/lib/permissions.ts +29 -0
- package/template/src/lib/security-headers.ts +20 -0
- package/template/src/lib/site.ts +59 -0
- package/template/src/lib/theme.ts +42 -0
- package/template/src/lib/user-update-guard.ts +35 -0
- package/template/src/middlewares/admin.ts +46 -0
- package/template/src/middlewares/auth-rate-limit.ts +37 -0
- package/template/src/middlewares/better-auth.ts +15 -0
- package/template/src/middlewares/body-limit.ts +62 -0
- package/template/src/middlewares/database.ts +13 -0
- package/template/src/middlewares/protected.ts +45 -0
- package/template/src/routeTree.gen.ts +536 -0
- package/template/src/router.tsx +53 -0
- package/template/src/routes/[.]well-known/oauth-authorization-server/api/auth.ts +18 -0
- package/template/src/routes/[.]well-known/oauth-protected-resource/index.ts +32 -0
- package/template/src/routes/__root.tsx +175 -0
- package/template/src/routes/admin/audit.tsx +74 -0
- package/template/src/routes/admin/index.tsx +7 -0
- package/template/src/routes/admin/oauth-apps.tsx +164 -0
- package/template/src/routes/admin/route.tsx +27 -0
- package/template/src/routes/admin/users.tsx +215 -0
- package/template/src/routes/api/auth/$.ts +14 -0
- package/template/src/routes/api/auth/[.]well-known/openid-configuration.ts +15 -0
- package/template/src/routes/api/webhooks/creem.ts +69 -0
- package/template/src/routes/api/webhooks/nowpayments.ts +62 -0
- package/template/src/routes/api/webhooks/stripe.ts +74 -0
- package/template/src/routes/dashboard/account.tsx +430 -0
- package/template/src/routes/dashboard/index.tsx +128 -0
- package/template/src/routes/dashboard/route.tsx +17 -0
- package/template/src/routes/forgot-password.tsx +65 -0
- package/template/src/routes/index.tsx +85 -0
- package/template/src/routes/oauth/consent.tsx +77 -0
- package/template/src/routes/reset-password.tsx +78 -0
- package/template/src/routes/sign-in.tsx +174 -0
- package/template/src/routes/sign-up.tsx +135 -0
- package/template/src/routes/two-factor.tsx +74 -0
- package/template/src/server/README.md +3 -0
- package/template/src/server/account-security.ts +163 -0
- package/template/src/server/admin-audit.ts +37 -0
- package/template/src/server/admin-users.ts +112 -0
- package/template/src/server/auth.ts +44 -0
- package/template/src/server/demo-checkout.ts +67 -0
- package/template/src/server/oauth-clients.ts +83 -0
- package/template/src/server/payment-toggles.ts +39 -0
- package/template/src/server.ts +20 -0
- package/template/src/services/README.md +3 -0
- package/template/src/services/account-security.ts +893 -0
- package/template/src/services/admin-security.ts +164 -0
- package/template/src/services/admin-users.ts +181 -0
- package/template/src/services/auth-rate-limit.ts +32 -0
- package/template/src/services/creem.ts +83 -0
- package/template/src/services/demo-checkout.ts +100 -0
- package/template/src/services/nowpayments.ts +154 -0
- package/template/src/services/oauth-client.ts +85 -0
- package/template/src/services/oauth-clients.ts +15 -0
- package/template/src/services/payment-providers.ts +157 -0
- package/template/src/services/payment-toggles.ts +48 -0
- package/template/src/services/purchase.ts +233 -0
- package/template/src/services/stripe.ts +106 -0
- package/template/src/start.ts +35 -0
- package/template/src/styles/layout.css +93 -0
- package/template/src/styles/theme.css +84 -0
- package/template/src/styles/ui.css +233 -0
- package/template/src/styles.css +90 -0
- package/template/src/utils/api-error.ts +129 -0
- package/template/src/utils/auth-errors.ts +157 -0
- package/template/src/utils/auth-redirect.ts +61 -0
- package/template/src/utils/body-limit.ts +84 -0
- package/template/src/utils/cn.ts +6 -0
- package/template/src/utils/input-rules.ts +172 -0
- package/template/src/utils/locale-paths.ts +18 -0
- package/template/src/utils/same-page-locale-href.ts +20 -0
- package/template/src/utils/search-params.ts +71 -0
- package/template/tsconfig.json +30 -0
- package/template/tsr.config.json +6 -0
- package/template/vite.config.ts +24 -0
- package/template/vitest.config.ts +26 -0
- package/template/worker-configuration.d.ts +14660 -0
- package/template/wrangler.jsonc +39 -0
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { parseArgs, stripVTControlCharacters } from "node:util";
|
|
7
|
+
import {
|
|
8
|
+
cancel,
|
|
9
|
+
confirm,
|
|
10
|
+
intro,
|
|
11
|
+
isCancel,
|
|
12
|
+
log,
|
|
13
|
+
multiselect,
|
|
14
|
+
note,
|
|
15
|
+
outro,
|
|
16
|
+
select,
|
|
17
|
+
spinner,
|
|
18
|
+
text,
|
|
19
|
+
} from "@clack/prompts";
|
|
20
|
+
import {
|
|
21
|
+
catalog,
|
|
22
|
+
choices,
|
|
23
|
+
defaults,
|
|
24
|
+
incompatibilities,
|
|
25
|
+
isPostgres,
|
|
26
|
+
validateSelection,
|
|
27
|
+
} from "./catalog.mjs";
|
|
28
|
+
import { createProject, validateName } from "./generate.mjs";
|
|
29
|
+
|
|
30
|
+
const help = `mystack create [name] [options]
|
|
31
|
+
|
|
32
|
+
Create a configured TanStack Start project with an interactive wizard.
|
|
33
|
+
Run mystack without arguments to open the wizard.
|
|
34
|
+
|
|
35
|
+
--database ${catalog.database.map((option) => option.id).join("|")}
|
|
36
|
+
--deployment cloudflare|nitro|bun
|
|
37
|
+
--email cloudflare|resend
|
|
38
|
+
--payments stripe,creem,nowpayments,polar|none
|
|
39
|
+
--yes Use defaults for unspecified options
|
|
40
|
+
--no-install Generate without installing dependencies
|
|
41
|
+
--help Show this help
|
|
42
|
+
|
|
43
|
+
Example: mystack create my-app --database d1 --payments stripe,polar
|
|
44
|
+
Package manager: Bun. Hyperdrive, D1 and Cloudflare Email require Cloudflare Workers.
|
|
45
|
+
Turso Local needs a filesystem. Bun SQLite requires the Bun deployment.
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
class PromptCancelled extends Error {}
|
|
49
|
+
|
|
50
|
+
async function answer(prompt) {
|
|
51
|
+
const value = await prompt;
|
|
52
|
+
if (isCancel(value)) throw new PromptCancelled();
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function step(message, complete, task) {
|
|
57
|
+
const progress = spinner();
|
|
58
|
+
progress.start(message);
|
|
59
|
+
try {
|
|
60
|
+
const result = await task(progress.message);
|
|
61
|
+
progress.stop(complete);
|
|
62
|
+
return result;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
progress.error("Step failed");
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function installDependencies(target) {
|
|
70
|
+
return new Promise((resolveInstall, reject) => {
|
|
71
|
+
const child = spawn("bun", ["install"], {
|
|
72
|
+
cwd: target,
|
|
73
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
74
|
+
shell: false,
|
|
75
|
+
});
|
|
76
|
+
let output = "";
|
|
77
|
+
const capture = (chunk) => {
|
|
78
|
+
output = (output + chunk.toString()).slice(-16_384);
|
|
79
|
+
};
|
|
80
|
+
child.stdout.on("data", capture);
|
|
81
|
+
child.stderr.on("data", capture);
|
|
82
|
+
child.on("error", reject);
|
|
83
|
+
child.on("close", (code, signal) => {
|
|
84
|
+
if (code === 0) resolveInstall();
|
|
85
|
+
else
|
|
86
|
+
reject(
|
|
87
|
+
new Error(
|
|
88
|
+
`bun install ${signal ? `was stopped by ${signal}` : `exited with ${code}`}\n${stripVTControlCharacters(output).trim()}`,
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function main(args = process.argv.slice(2)) {
|
|
96
|
+
try {
|
|
97
|
+
const { values, positionals } = parseArgs({
|
|
98
|
+
args,
|
|
99
|
+
allowPositionals: true,
|
|
100
|
+
options: {
|
|
101
|
+
database: { type: "string" },
|
|
102
|
+
deployment: { type: "string" },
|
|
103
|
+
email: { type: "string" },
|
|
104
|
+
payments: { type: "string" },
|
|
105
|
+
yes: { type: "boolean", short: "y" },
|
|
106
|
+
"no-install": { type: "boolean" },
|
|
107
|
+
help: { type: "boolean", short: "h" },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
if (values.help) {
|
|
111
|
+
console.log(help);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
const [command = "create", projectName] = positionals;
|
|
115
|
+
if (command !== "create" || positionals.length > 2) throw new Error(help);
|
|
116
|
+
const selection = {};
|
|
117
|
+
for (const category of ["database", "deployment", "email"]) {
|
|
118
|
+
if (values[category] !== undefined)
|
|
119
|
+
selection[category] = values[category];
|
|
120
|
+
}
|
|
121
|
+
if (values.payments !== undefined)
|
|
122
|
+
selection.payments =
|
|
123
|
+
values.payments === "none"
|
|
124
|
+
? []
|
|
125
|
+
: values.payments.split(",").map((id) => id.trim());
|
|
126
|
+
const invalid = incompatibilities(selection);
|
|
127
|
+
if (invalid.length) throw new Error(invalid.join("\n"));
|
|
128
|
+
if (!values.yes && !process.stdin.isTTY)
|
|
129
|
+
throw new Error(
|
|
130
|
+
"Interactive input requires a terminal. Use --yes and configuration flags in automation.",
|
|
131
|
+
);
|
|
132
|
+
intro(
|
|
133
|
+
values.yes
|
|
134
|
+
? `Creating a new Mystack app in ${projectName ?? "my-app"}...`
|
|
135
|
+
: "Let's configure your Mystack application",
|
|
136
|
+
);
|
|
137
|
+
const checkName = (value) => {
|
|
138
|
+
try {
|
|
139
|
+
validateName(value);
|
|
140
|
+
if (existsSync(resolve(value)))
|
|
141
|
+
return `Directory '${value}' already exists. Choose another project name.`;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return error.message;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const name =
|
|
147
|
+
projectName ??
|
|
148
|
+
(values.yes
|
|
149
|
+
? "my-app"
|
|
150
|
+
: await answer(
|
|
151
|
+
text({
|
|
152
|
+
message: "Project name",
|
|
153
|
+
placeholder: "my-app",
|
|
154
|
+
defaultValue: "my-app",
|
|
155
|
+
validate: (value) => checkName(value || "my-app"),
|
|
156
|
+
}),
|
|
157
|
+
));
|
|
158
|
+
const nameError = checkName(name);
|
|
159
|
+
if (nameError) throw new Error(nameError);
|
|
160
|
+
const messages = {
|
|
161
|
+
database: "Select database:",
|
|
162
|
+
deployment: "Select deployment adapter:",
|
|
163
|
+
email: "Select email provider:",
|
|
164
|
+
};
|
|
165
|
+
for (const category of ["database", "deployment", "email"]) {
|
|
166
|
+
if (selection[category] !== undefined) continue;
|
|
167
|
+
const options = choices(category, selection);
|
|
168
|
+
const preferred =
|
|
169
|
+
options.find(
|
|
170
|
+
(option) => option.value === defaults[category] && !option.disabled,
|
|
171
|
+
) ?? options.find((option) => !option.disabled);
|
|
172
|
+
if (!preferred) throw new Error(`No compatible ${category} choices.`);
|
|
173
|
+
selection[category] = values.yes
|
|
174
|
+
? preferred.value
|
|
175
|
+
: await answer(
|
|
176
|
+
select({
|
|
177
|
+
message: messages[category],
|
|
178
|
+
options: options.map((option) => ({
|
|
179
|
+
value: option.value,
|
|
180
|
+
label: option.name,
|
|
181
|
+
hint: option.disabled || option.hint,
|
|
182
|
+
disabled: Boolean(option.disabled),
|
|
183
|
+
})),
|
|
184
|
+
initialValue: preferred.value,
|
|
185
|
+
showInstructions: false,
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (selection.payments === undefined) {
|
|
190
|
+
if (values.yes) selection.payments = defaults.payments;
|
|
191
|
+
else {
|
|
192
|
+
note(
|
|
193
|
+
"Use ↑/↓ to navigate • Space to select/deselect • Enter to confirm",
|
|
194
|
+
"Keyboard Shortcuts",
|
|
195
|
+
);
|
|
196
|
+
selection.payments = await answer(
|
|
197
|
+
multiselect({
|
|
198
|
+
message:
|
|
199
|
+
"What payment providers would you like? (Space to toggle, Enter to confirm)",
|
|
200
|
+
options: catalog.payments.map((option) => ({
|
|
201
|
+
label: option.label,
|
|
202
|
+
value: option.id,
|
|
203
|
+
})),
|
|
204
|
+
required: false,
|
|
205
|
+
showInstructions: false,
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
validateSelection(selection);
|
|
211
|
+
const install = values["no-install"]
|
|
212
|
+
? false
|
|
213
|
+
: values.yes
|
|
214
|
+
? true
|
|
215
|
+
: await answer(
|
|
216
|
+
confirm({
|
|
217
|
+
message: "Would you like to install dependencies now?",
|
|
218
|
+
initialValue: true,
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
const label = (category, id) =>
|
|
222
|
+
catalog[category].find((option) => option.id === id).label;
|
|
223
|
+
log.info(
|
|
224
|
+
`About to create:\n\n${[
|
|
225
|
+
["Project", name],
|
|
226
|
+
["Location", resolve(name)],
|
|
227
|
+
["Framework", "TanStack Start (React)"],
|
|
228
|
+
["Package manager", "Bun"],
|
|
229
|
+
["Database", label("database", selection.database)],
|
|
230
|
+
["Deploy", label("deployment", selection.deployment)],
|
|
231
|
+
["Email", label("email", selection.email)],
|
|
232
|
+
[
|
|
233
|
+
"Payments",
|
|
234
|
+
selection.payments.map((id) => label("payments", id)).join(", ") ||
|
|
235
|
+
"None",
|
|
236
|
+
],
|
|
237
|
+
["Install deps", install ? "yes" : "no"],
|
|
238
|
+
]
|
|
239
|
+
.map(([key, value]) => ` ${`${key}:`.padEnd(17)}${value}`)
|
|
240
|
+
.join("\n")}`,
|
|
241
|
+
);
|
|
242
|
+
if (
|
|
243
|
+
!values.yes &&
|
|
244
|
+
!(await answer(
|
|
245
|
+
confirm({
|
|
246
|
+
message: "Continue with these settings?",
|
|
247
|
+
initialValue: true,
|
|
248
|
+
}),
|
|
249
|
+
))
|
|
250
|
+
) {
|
|
251
|
+
cancel("Operation cancelled.");
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
const target = await step(
|
|
255
|
+
"Creating project files...",
|
|
256
|
+
"Project files created",
|
|
257
|
+
(onProgress) => createProject({ name, selection, onProgress }),
|
|
258
|
+
);
|
|
259
|
+
if (install) {
|
|
260
|
+
try {
|
|
261
|
+
await step(
|
|
262
|
+
"Installing dependencies via Bun...",
|
|
263
|
+
"Installed dependencies",
|
|
264
|
+
() => installDependencies(target),
|
|
265
|
+
);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
log.error(error.message);
|
|
268
|
+
note(
|
|
269
|
+
`Your project is saved at ${target}.\n\n% cd ${name}\n% bun install\n\nThen follow README.md to configure and start your app.`,
|
|
270
|
+
"Retry installation",
|
|
271
|
+
);
|
|
272
|
+
cancel("Dependency installation failed.");
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
275
|
+
} else log.step("Dependency installation skipped");
|
|
276
|
+
outro(
|
|
277
|
+
`Your Mystack app is ready in '${name}'.\n\nUse the following commands to start your app:\n% cd ${name}\n${install ? "" : "% bun install\n"}% cp .env.example .env\n\nConfigure .env using README.md, then run:\n${isPostgres(selection.database) ? "% bun run db:up\n" : ""}% bun run db:migrate\n% bun run dev`,
|
|
278
|
+
);
|
|
279
|
+
return 0;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (error instanceof PromptCancelled) {
|
|
282
|
+
cancel("Operation cancelled.");
|
|
283
|
+
return 130;
|
|
284
|
+
}
|
|
285
|
+
log.error(error.message);
|
|
286
|
+
return 1;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (
|
|
291
|
+
process.argv[1] &&
|
|
292
|
+
import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href
|
|
293
|
+
)
|
|
294
|
+
process.exitCode = await main();
|
package/src/generate.mjs
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
mkdir,
|
|
4
|
+
mkdtemp,
|
|
5
|
+
readdir,
|
|
6
|
+
rename,
|
|
7
|
+
rm,
|
|
8
|
+
writeFile,
|
|
9
|
+
} from "node:fs/promises";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
import { Generator, getConfig } from "@tanstack/router-generator";
|
|
15
|
+
import { configureDatabase } from "./adapters/database.mjs";
|
|
16
|
+
import { configureDeployment } from "./adapters/deployment.mjs";
|
|
17
|
+
import { configureEmail } from "./adapters/email.mjs";
|
|
18
|
+
import { configurePayments } from "./adapters/payments.mjs";
|
|
19
|
+
import { catalog, isPostgres, validateSelection } from "./catalog.mjs";
|
|
20
|
+
import { readTemplate, text } from "./template.mjs";
|
|
21
|
+
|
|
22
|
+
export function validateName(name) {
|
|
23
|
+
if (!/^[a-z][a-z0-9-]{0,57}$/.test(name))
|
|
24
|
+
throw new Error(
|
|
25
|
+
"Project name must start with a lowercase letter and contain at most 58 lowercase letters, digits or hyphens.",
|
|
26
|
+
);
|
|
27
|
+
return name;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const bundledTemplateRoot = fileURLToPath(
|
|
31
|
+
new URL("../template", import.meta.url),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export async function renderProject({
|
|
35
|
+
name,
|
|
36
|
+
selection,
|
|
37
|
+
templateRoot = bundledTemplateRoot,
|
|
38
|
+
}) {
|
|
39
|
+
validateName(name);
|
|
40
|
+
validateSelection(selection);
|
|
41
|
+
const files = await readTemplate(templateRoot, {
|
|
42
|
+
packaged: templateRoot === bundledTemplateRoot,
|
|
43
|
+
});
|
|
44
|
+
const pkg = JSON.parse(text(files, "package.json"));
|
|
45
|
+
pkg.name = name;
|
|
46
|
+
delete pkg.pnpm;
|
|
47
|
+
for (const key of ["mystack", "cli:prepare", "test:cli"])
|
|
48
|
+
delete pkg.scripts[key];
|
|
49
|
+
const context = { files, pkg, name, selection };
|
|
50
|
+
await configureDeployment(context);
|
|
51
|
+
await configureDatabase(context);
|
|
52
|
+
await configureEmail(context);
|
|
53
|
+
await configurePayments(context);
|
|
54
|
+
files.set("package.json", `${JSON.stringify(pkg, null, "\t")}\n`);
|
|
55
|
+
files.set(
|
|
56
|
+
"mystack.json",
|
|
57
|
+
`${JSON.stringify({ version: 1, ...selection }, null, "\t")}\n`,
|
|
58
|
+
);
|
|
59
|
+
files.set(
|
|
60
|
+
"tsconfig.json",
|
|
61
|
+
text(files, "tsconfig.json").replace(
|
|
62
|
+
/ {2}"exclude": \["packages\/mystack\/assets".*\n/,
|
|
63
|
+
"",
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
files.set("README.md", projectReadme(context));
|
|
67
|
+
return files;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function createProject({
|
|
71
|
+
name,
|
|
72
|
+
selection,
|
|
73
|
+
cwd = process.cwd(),
|
|
74
|
+
templateRoot = bundledTemplateRoot,
|
|
75
|
+
onProgress = () => {},
|
|
76
|
+
}) {
|
|
77
|
+
validateName(name);
|
|
78
|
+
validateSelection(selection);
|
|
79
|
+
const target = resolve(cwd, name);
|
|
80
|
+
// Reserve a new directory exclusively. Never overwrite even an empty existing
|
|
81
|
+
// directory, and never remove user files if installation later fails.
|
|
82
|
+
await mkdir(target);
|
|
83
|
+
let staging;
|
|
84
|
+
try {
|
|
85
|
+
staging = await mkdtemp(join(cwd, ".mystack-"));
|
|
86
|
+
onProgress("Creating project files...");
|
|
87
|
+
const files = await renderProject({ name, selection, templateRoot });
|
|
88
|
+
for (const [path, contents] of files) {
|
|
89
|
+
await mkdir(dirname(join(staging, path)), { recursive: true });
|
|
90
|
+
await writeFile(join(staging, path), contents);
|
|
91
|
+
}
|
|
92
|
+
onProgress("Generating routes...");
|
|
93
|
+
await new Generator({
|
|
94
|
+
config: getConfig({ disableLogging: true }, staging),
|
|
95
|
+
root: staging,
|
|
96
|
+
}).run();
|
|
97
|
+
onProgress("Formatting project files...");
|
|
98
|
+
const biome = createRequire(import.meta.url).resolve(
|
|
99
|
+
"@biomejs/biome/bin/biome",
|
|
100
|
+
);
|
|
101
|
+
await promisify(execFile)(process.execPath, [biome, "check", "--write"], {
|
|
102
|
+
cwd: staging,
|
|
103
|
+
});
|
|
104
|
+
// Import organization can change formatter decisions; format the fixed AST.
|
|
105
|
+
await promisify(execFile)(process.execPath, [biome, "format", "--write"], {
|
|
106
|
+
cwd: staging,
|
|
107
|
+
});
|
|
108
|
+
if ((await readdir(target)).length)
|
|
109
|
+
throw new Error(
|
|
110
|
+
"Destination changed during generation; refusing to overwrite it.",
|
|
111
|
+
);
|
|
112
|
+
await rename(staging, target);
|
|
113
|
+
return target;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (staging) await rm(staging, { recursive: true, force: true });
|
|
116
|
+
// Only remove our reservation if it is still empty.
|
|
117
|
+
const { rmdir } = await import("node:fs/promises");
|
|
118
|
+
await rmdir(target).catch(() => {});
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function projectReadme({ name, selection }) {
|
|
124
|
+
const label = (category, id) =>
|
|
125
|
+
catalog[category].find((option) => option.id === id).label;
|
|
126
|
+
const databaseNotes = {
|
|
127
|
+
pg: "PostgreSQL connects directly through DATABASE_URL using the application role. DATABASE_MIGRATION_URL is privileged and is only for migrations. Use Docker for local development.",
|
|
128
|
+
"pg-hyperdrive":
|
|
129
|
+
"PostgreSQL connects through the Cloudflare HYPERDRIVE binding. Local development uses hyperdrive.localConnectionString in wrangler.jsonc; production requires your Hyperdrive configuration ID. DATABASE_MIGRATION_URL is only for migrations.",
|
|
130
|
+
d1: "Cloudflare D1 uses the DB binding in wrangler.jsonc. db:migrate applies local migrations; db:migrate:remote applies them to the configured Cloudflare database.",
|
|
131
|
+
"turso-cloud":
|
|
132
|
+
"Turso Cloud uses a remote database. Set TURSO_DATABASE_URL (libsql:// or https://) and TURSO_AUTH_TOKEN from your Turso database before running migrations or starting the app. This option never opens a local SQLite file. Database service tests use an isolated local libSQL fixture; they do not access your cloud database.",
|
|
133
|
+
"turso-local":
|
|
134
|
+
"Turso Local uses @libsql/client with a file: URL (TURSO_DATABASE_URL=file:local.db). No Turso Cloud token is required. Keep the database file on persistent storage when deploying.",
|
|
135
|
+
"bun-sqlite":
|
|
136
|
+
"Bun SQLite uses the native bun:sqlite driver, with SQLITE_DATABASE_PATH=local.db. Run the app with Bun and keep the database file on persistent storage. Native database tests run with bun test after Vitest.",
|
|
137
|
+
};
|
|
138
|
+
return `# ${name}
|
|
139
|
+
|
|
140
|
+
Generated with \`mystack create\`. Configuration is recorded in \`mystack.json\`; editing it does not regenerate or convert the application. This project is maintained independently of the generator repository.
|
|
141
|
+
|
|
142
|
+
Database: ${label("database", selection.database)}; runtime: ${label("deployment", selection.deployment)}; email: ${label("email", selection.email)}; payments: ${selection.payments.map((id) => label("payments", id)).join(", ") || "none"}.
|
|
143
|
+
|
|
144
|
+
## Database
|
|
145
|
+
|
|
146
|
+
${databaseNotes[selection.database]}
|
|
147
|
+
|
|
148
|
+
## Local development
|
|
149
|
+
|
|
150
|
+
Use Bun for application commands.${isPostgres(selection.database) ? " Local PostgreSQL also needs Docker." : ""} Mystack normally installs dependencies; run \`bun install\` if installation was skipped or failed.
|
|
151
|
+
|
|
152
|
+
\`\`\`sh
|
|
153
|
+
cp .env.example .env
|
|
154
|
+
\`\`\`
|
|
155
|
+
|
|
156
|
+
Configure the selected database and provider credentials in \`.env\`, then run:
|
|
157
|
+
|
|
158
|
+
\`\`\`sh
|
|
159
|
+
${isPostgres(selection.database) ? "bun run db:up\nbun run db:migrate" : "bun run db:migrate"}
|
|
160
|
+
bun run dev
|
|
161
|
+
\`\`\`
|
|
162
|
+
|
|
163
|
+
Use **http://localhost:3000**, matching the example app/auth origins. Authentication email is printed in development when credentials are absent. ${isPostgres(selection.database) ? "Use `bun run db:seed` for optional local test users." : "Create your account through normal sign-up and use the development email to verify it."}
|
|
164
|
+
|
|
165
|
+
## Project development
|
|
166
|
+
|
|
167
|
+
See [docs/project-guide.md](./docs/project-guide.md) for source layout, authentication, database authorization, payment fulfillment, localization and validation. See [DESIGN.md](./DESIGN.md) for presentation conventions.
|
|
168
|
+
|
|
169
|
+
\`\`\`sh
|
|
170
|
+
bun run check
|
|
171
|
+
bun run typecheck
|
|
172
|
+
bun run test:unit
|
|
173
|
+
\`\`\`
|
|
174
|
+
|
|
175
|
+
## Production
|
|
176
|
+
|
|
177
|
+
Set a new \`BETTER_AUTH_SECRET\` and real application origins. Configure the selected database and verified email sender. ${selection.payments.length ? `Selected payment webhooks: ${selection.payments.map((provider) => `\`/api/webhooks/${provider}\``).join(", ")}. Set their matching signature secrets and register these URLs with the providers.` : "No payment providers or webhook routes were selected."}
|
|
178
|
+
|
|
179
|
+
${selection.deployment === "cloudflare" ? `Configure the selected resources in \`wrangler.jsonc\`, authenticate with \`bunx wrangler login\`, create \`.env.prod\`, and run \`bun run deploy\`. ${selection.database === "d1" ? "Create the hosted D1 database, replace its placeholder ID, and apply hosted migrations with `bun run db:migrate:remote` before deployment." : "Apply `bun run db:migrate` with production migration credentials before deployment."}${selection.database === "pg-hyperdrive" ? " Set a real Hyperdrive configuration ID and app-role connection credentials." : ""}` : "Inject production environment variables in the hosting environment, apply database migrations, run `bun run deploy` for validation and build checks, then `bun run start`."}
|
|
180
|
+
|
|
181
|
+
${!isPostgres(selection.database) ? `SQLite payments commit a durable \`purchase_delivery\` event atomically with the paid state, using ${selection.database === "bun-sqlite" ? "a synchronous Bun SQLite transaction" : "the driver's atomic batch"}. Implement product fulfillment by consuming these events using \`purchase_id\` as the idempotency key. SQLite authorization uses authenticated/admin middleware and explicit ownership predicates.\n` : ""}
|
|
182
|
+
`;
|
|
183
|
+
}
|
package/src/template.mjs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
// An allowlist prevents credentials, Git history, installed dependencies and
|
|
5
|
+
// local build output from entering the published template.
|
|
6
|
+
export const templateEntries = [
|
|
7
|
+
"src",
|
|
8
|
+
"public",
|
|
9
|
+
"scripts",
|
|
10
|
+
"drizzle",
|
|
11
|
+
"project.inlang/settings.json",
|
|
12
|
+
"messages",
|
|
13
|
+
"e2e",
|
|
14
|
+
"package.json",
|
|
15
|
+
"vite.config.ts",
|
|
16
|
+
"tsconfig.json",
|
|
17
|
+
"tsr.config.json",
|
|
18
|
+
"biome.json",
|
|
19
|
+
"vitest.config.ts",
|
|
20
|
+
"playwright.config.ts",
|
|
21
|
+
"drizzle.config.ts",
|
|
22
|
+
"paraglide.config.ts",
|
|
23
|
+
"wrangler.jsonc",
|
|
24
|
+
"worker-configuration.d.ts",
|
|
25
|
+
"docker-compose.yml",
|
|
26
|
+
".env.example",
|
|
27
|
+
".env.docker",
|
|
28
|
+
".gitignore",
|
|
29
|
+
"DESIGN.md",
|
|
30
|
+
"docs/project-guide.md",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export async function readTemplate(root, { packaged = false } = {}) {
|
|
35
|
+
const files = new Map();
|
|
36
|
+
async function visit(path) {
|
|
37
|
+
if (
|
|
38
|
+
path === "src/paraglide" ||
|
|
39
|
+
path.startsWith("scripts/mystack") ||
|
|
40
|
+
path === "src/__tests__/mystack.test.ts"
|
|
41
|
+
)
|
|
42
|
+
return;
|
|
43
|
+
const absolute = join(
|
|
44
|
+
root,
|
|
45
|
+
packaged && path === ".gitignore" ? "gitignore" : path,
|
|
46
|
+
);
|
|
47
|
+
const { lstat } = await import("node:fs/promises");
|
|
48
|
+
let entry;
|
|
49
|
+
try {
|
|
50
|
+
entry = await lstat(absolute);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error.code === "ENOENT" && templateEntries.includes(path)) return;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
if (entry.isSymbolicLink())
|
|
56
|
+
throw new Error(`Template symlinks are forbidden: ${path}`);
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
for (const name of (await readdir(absolute)).sort())
|
|
59
|
+
await visit(`${path}/${name}`);
|
|
60
|
+
} else if (entry.isFile()) files.set(path, await readFile(absolute));
|
|
61
|
+
}
|
|
62
|
+
for (const path of templateEntries) await visit(path);
|
|
63
|
+
for (const path of [
|
|
64
|
+
"package.json",
|
|
65
|
+
"src/lib/auth.ts",
|
|
66
|
+
"src/db/auth.schema.ts",
|
|
67
|
+
".gitignore",
|
|
68
|
+
".env.example",
|
|
69
|
+
"messages/en.json",
|
|
70
|
+
"messages/de.json",
|
|
71
|
+
"docs/project-guide.md",
|
|
72
|
+
]) {
|
|
73
|
+
if (!files.has(path))
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Template is missing ${path}. Run template:prepare before packaging.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return files;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function text(files, path) {
|
|
82
|
+
if (!files.has(path)) throw new Error(`Template is missing ${path}.`);
|
|
83
|
+
return files.get(path).toString("utf8");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function replace(files, path, before, after) {
|
|
87
|
+
const source = text(files, path);
|
|
88
|
+
if (!source.includes(before))
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Template changed: expected anchor in ${path}: ${before.slice(0, 80)}`,
|
|
91
|
+
);
|
|
92
|
+
files.set(path, source.replace(before, after));
|
|
93
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Local Docker Compose Postgres defaults (committed — dev only).
|
|
2
|
+
# Loaded via: docker compose --env-file .env.docker up -d
|
|
3
|
+
|
|
4
|
+
# Privileged superuser (migrations / drizzle-kit)
|
|
5
|
+
POSTGRES_DB=starter
|
|
6
|
+
POSTGRES_USER=postgres
|
|
7
|
+
POSTGRES_PASSWORD=postgres_dev_password
|
|
8
|
+
# Host port published to localhost. Container always listens on 5432.
|
|
9
|
+
# Use e.g. 5433 if another Postgres already owns 5432 — then update
|
|
10
|
+
# DATABASE_MIGRATION_URL and hyperdrive[].localConnectionString to match.
|
|
11
|
+
POSTGRES_PORT=5432
|
|
12
|
+
|
|
13
|
+
# Runtime app role (Hyperdrive / Worker localConnectionString)
|
|
14
|
+
# NOBYPASSRLS — created by scripts/postgres/create-app-role.sh on first boot
|
|
15
|
+
APP_DB_USER=starter_app
|
|
16
|
+
APP_DB_PASSWORD=starter_app_dev_password
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# —————————————————— App config ————————————————————
|
|
2
|
+
APP_ORIGIN=http://localhost:3000
|
|
3
|
+
# Use http://localhost:3000 in the browser, not 127.0.0.1 (cookie/origin pitfall).
|
|
4
|
+
# Public site identity (document head, OG, JSON-LD). Mirrors APP_ORIGIN for the client.
|
|
5
|
+
VITE_APP_ORIGIN=http://localhost:3000
|
|
6
|
+
VITE_APP_TITLE=TanStack Starter
|
|
7
|
+
|
|
8
|
+
# —————————————————— Better Auth ————————————————————
|
|
9
|
+
BETTER_AUTH_URL=http://localhost:3000
|
|
10
|
+
# Dev-only placeholder for local generated-project development.
|
|
11
|
+
# `bun run validate-prod-env` refuses it, so a deploy cannot inherit it.
|
|
12
|
+
# Real secret: `bunx auth@latest secret` (same CLI as `bun run auth:generate`)
|
|
13
|
+
BETTER_AUTH_SECRET=dev-only-insecure-secret-change-before-deploy
|
|
14
|
+
# Optional comma-separated OAuth resource audiences (defaults to APP_ORIGIN).
|
|
15
|
+
BETTER_AUTH_AUDIENCES=
|
|
16
|
+
|
|
17
|
+
# —————————————————— Database ——————————————————————
|
|
18
|
+
# Local Docker defaults match `.env.docker` (bun run db:up).
|
|
19
|
+
#
|
|
20
|
+
# Privileged superuser — drizzle-kit migrate / push / seed only.
|
|
21
|
+
# Never point the Worker runtime at this URL.
|
|
22
|
+
DATABASE_MIGRATION_URL=postgresql://postgres:postgres_dev_password@localhost:5432/starter
|
|
23
|
+
#
|
|
24
|
+
# Runtime app role (same as wrangler.jsonc hyperdrive.localConnectionString):
|
|
25
|
+
# postgresql://starter_app:starter_app_dev_password@localhost:5432/starter
|
|
26
|
+
#
|
|
27
|
+
# Optional overrides for `bun run db:seed` (defaults are local-only).
|
|
28
|
+
# SEED_ADMIN_EMAIL=admin@localhost.dev
|
|
29
|
+
# SEED_ADMIN_PASSWORD=AdminPass1!
|
|
30
|
+
# SEED_ADMIN_NAME=Local Admin
|
|
31
|
+
# SEED_ADMIN_ROLE=superadmin
|
|
32
|
+
# SEED_USER_EMAIL=user@localhost.dev
|
|
33
|
+
# SEED_USER_PASSWORD=UserPass1!
|
|
34
|
+
# SEED_USER_NAME=Local User
|
|
35
|
+
# SEED_FORCE=1
|
|
36
|
+
|
|
37
|
+
# —————————————————— GitHub social (optional) ——————
|
|
38
|
+
# All three must be set together for GitHub to work end-to-end:
|
|
39
|
+
# - VITE_GITHUB_CLIENT_ID: public client id (UI button + server fallback)
|
|
40
|
+
# - GITHUB_CLIENT_ID: optional alias; if set, must match VITE_GITHUB_CLIENT_ID
|
|
41
|
+
# - GITHUB_CLIENT_SECRET: server-only secret
|
|
42
|
+
# Client IDs are public by OAuth design. Leave all blank to hide GitHub in the UI.
|
|
43
|
+
VITE_GITHUB_CLIENT_ID=
|
|
44
|
+
GITHUB_CLIENT_ID=
|
|
45
|
+
GITHUB_CLIENT_SECRET=
|
|
46
|
+
|
|
47
|
+
# —————————————————— Turnstile —————————————————————
|
|
48
|
+
TURNSTILE_SECRET_KEY=
|
|
49
|
+
# Public site key. Leave blank in dev to disable captcha (CAPTCHA_ENABLED=false).
|
|
50
|
+
VITE_TURNSTILE_SITE_KEY=
|
|
51
|
+
|
|
52
|
+
# —————————————————— Email ————————————————————————
|
|
53
|
+
# Technical From address (domain must be verified in Cloudflare Email).
|
|
54
|
+
EMAIL_FROM=no-reply@example.com
|
|
55
|
+
# Inbox display name (clone-time brand).
|
|
56
|
+
EMAIL_FROM_NAME=Starter
|
|
57
|
+
|
|
58
|
+
# —————————————————— Payments (optional) ——————————
|
|
59
|
+
CREEM_API_KEY=
|
|
60
|
+
CREEM_WEBHOOK_SECRET=
|
|
61
|
+
CREEM_PRODUCT_ID=
|
|
62
|
+
CREEM_TEST_MODE=false
|
|
63
|
+
STRIPE_SECRET_KEY=
|
|
64
|
+
STRIPE_WEBHOOK_SECRET=
|
|
65
|
+
NOW_PAYMENTS_API_KEY=
|
|
66
|
+
NOW_PAYMENTS_IPN_KEY=
|
|
67
|
+
NOW_PAYMENTS_TEST_MODE=false
|