create-reactor 0.1.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 +123 -0
- package/create-app.mjs +712 -0
- package/lib/build.mjs +434 -0
- package/lib/pm.mjs +85 -0
- package/lib/presets.mjs +122 -0
- package/lib/templates/ai-docs.mjs +80 -0
- package/lib/templates/app.mjs +961 -0
- package/lib/templates/backend.mjs +715 -0
- package/lib/templates/base.mjs +671 -0
- package/lib/templates/biome.mjs +107 -0
- package/lib/templates/extras.mjs +360 -0
- package/lib/templates/features.mjs +463 -0
- package/lib/templates/quality.mjs +159 -0
- package/lib/templates/readme.mjs +351 -0
- package/lib/templates/security.mjs +70 -0
- package/lib/templates/server.mjs +141 -0
- package/lib/templates/state.mjs +192 -0
- package/package.json +52 -0
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
// Backend files: Convex functions/schema/auth, Supabase client, Drizzle, Prisma, AI SDK.
|
|
2
|
+
|
|
3
|
+
export const AI_MODELS = {
|
|
4
|
+
anthropic: {
|
|
5
|
+
pkg: "@ai-sdk/anthropic",
|
|
6
|
+
importName: "anthropic",
|
|
7
|
+
model: "claude-sonnet-4-6",
|
|
8
|
+
envKey: "ANTHROPIC_API_KEY",
|
|
9
|
+
keysUrl: "https://console.anthropic.com/settings/keys",
|
|
10
|
+
},
|
|
11
|
+
openai: {
|
|
12
|
+
pkg: "@ai-sdk/openai",
|
|
13
|
+
importName: "openai",
|
|
14
|
+
model: "gpt-5.1",
|
|
15
|
+
envKey: "OPENAI_API_KEY",
|
|
16
|
+
keysUrl: "https://platform.openai.com/api-keys",
|
|
17
|
+
},
|
|
18
|
+
google: {
|
|
19
|
+
pkg: "@ai-sdk/google",
|
|
20
|
+
importName: "google",
|
|
21
|
+
model: "gemini-2.5-flash",
|
|
22
|
+
envKey: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
23
|
+
keysUrl: "https://aistudio.google.com/apikey",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Convex
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
export function convexTsconfig() {
|
|
32
|
+
return JSON.stringify(
|
|
33
|
+
{
|
|
34
|
+
compilerOptions: {
|
|
35
|
+
allowJs: true,
|
|
36
|
+
strict: true,
|
|
37
|
+
moduleResolution: "Bundler",
|
|
38
|
+
jsx: "react-jsx",
|
|
39
|
+
skipLibCheck: true,
|
|
40
|
+
allowSyntheticDefaultImports: true,
|
|
41
|
+
target: "ESNext",
|
|
42
|
+
lib: ["ES2021", "dom"],
|
|
43
|
+
forceConsistentCasingInFileNames: true,
|
|
44
|
+
module: "ESNext",
|
|
45
|
+
isolatedModules: true,
|
|
46
|
+
noEmit: true,
|
|
47
|
+
},
|
|
48
|
+
include: ["./**/*"],
|
|
49
|
+
exclude: ["./_generated"],
|
|
50
|
+
},
|
|
51
|
+
null,
|
|
52
|
+
2,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function convexSchema(c) {
|
|
57
|
+
const authImport =
|
|
58
|
+
c.auth === "convex-auth" ? `import { authTables } from "@convex-dev/auth/server";\n` : "";
|
|
59
|
+
const authSpread = c.auth === "convex-auth" ? " ...authTables,\n" : "";
|
|
60
|
+
return `import { defineSchema, defineTable } from "convex/server";
|
|
61
|
+
import { v } from "convex/values";
|
|
62
|
+
${authImport}
|
|
63
|
+
export default defineSchema({
|
|
64
|
+
${authSpread} tasks: defineTable({
|
|
65
|
+
text: v.string(),
|
|
66
|
+
isCompleted: v.boolean(),
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function convexTasks() {
|
|
73
|
+
return `import { v } from "convex/values";
|
|
74
|
+
import { mutation, query } from "./_generated/server";
|
|
75
|
+
|
|
76
|
+
export const get = query({
|
|
77
|
+
args: {},
|
|
78
|
+
handler: async (ctx) => {
|
|
79
|
+
return await ctx.db.query("tasks").collect();
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const add = mutation({
|
|
84
|
+
args: { text: v.string() },
|
|
85
|
+
handler: async (ctx, args) => {
|
|
86
|
+
return await ctx.db.insert("tasks", {
|
|
87
|
+
text: args.text,
|
|
88
|
+
isCompleted: false,
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const toggle = mutation({
|
|
94
|
+
args: { id: v.id("tasks") },
|
|
95
|
+
handler: async (ctx, args) => {
|
|
96
|
+
const task = await ctx.db.get(args.id);
|
|
97
|
+
if (!task) throw new Error("Task not found");
|
|
98
|
+
await ctx.db.patch(args.id, { isCompleted: !task.isCompleted });
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function convexSampleData() {
|
|
105
|
+
return `{"text": "Scaffold this project", "isCompleted": true}
|
|
106
|
+
{"text": "Run the dev server", "isCompleted": true}
|
|
107
|
+
{"text": "Build something great", "isCompleted": false}
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** convex/auth.config.ts — only when auth is wired through Convex. */
|
|
112
|
+
export function convexAuthConfig(c) {
|
|
113
|
+
if (c.auth === "clerk") {
|
|
114
|
+
return `export default {
|
|
115
|
+
providers: [
|
|
116
|
+
{
|
|
117
|
+
// Set CLERK_JWT_ISSUER_DOMAIN on your Convex deployment:
|
|
118
|
+
// npx convex env set CLERK_JWT_ISSUER_DOMAIN https://<your-app>.clerk.accounts.dev
|
|
119
|
+
// (find it in Clerk Dashboard -> JWT templates -> "convex" -> Issuer)
|
|
120
|
+
domain: process.env.CLERK_JWT_ISSUER_DOMAIN,
|
|
121
|
+
applicationID: "convex",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
if (c.auth === "convex-auth") {
|
|
128
|
+
return `export default {
|
|
129
|
+
providers: [
|
|
130
|
+
{
|
|
131
|
+
domain: process.env.CONVEX_SITE_URL,
|
|
132
|
+
applicationID: "convex",
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
if (c.auth === "better-auth") {
|
|
139
|
+
return `import { getAuthConfigProvider } from "@convex-dev/better-auth/auth-config";
|
|
140
|
+
|
|
141
|
+
export default {
|
|
142
|
+
providers: [getAuthConfigProvider()],
|
|
143
|
+
};
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** convex/auth.ts (Convex Auth). */
|
|
150
|
+
export function convexAuthTs() {
|
|
151
|
+
return `import { Password } from "@convex-dev/auth/providers/Password";
|
|
152
|
+
import { convexAuth } from "@convex-dev/auth/server";
|
|
153
|
+
|
|
154
|
+
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
|
155
|
+
providers: [Password],
|
|
156
|
+
});
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** convex/http.ts — composed from auth routes (Convex Auth / Better Auth) and/or the AI chat route. */
|
|
161
|
+
export function convexHttp(c) {
|
|
162
|
+
const needsAuth = c.auth === "convex-auth" || c.auth === "better-auth";
|
|
163
|
+
const needsChat = c.ai !== "none";
|
|
164
|
+
if (!needsAuth && !needsChat) return "";
|
|
165
|
+
|
|
166
|
+
const imports = [`import { httpRouter } from "convex/server";`];
|
|
167
|
+
if (c.auth === "convex-auth") imports.push(`import { auth } from "./auth";`);
|
|
168
|
+
if (c.auth === "better-auth") imports.push(`import { authComponent, createAuth } from "./auth";`);
|
|
169
|
+
if (needsChat) imports.push(`import { chat, chatOptions } from "./chat";`);
|
|
170
|
+
|
|
171
|
+
const routes = [];
|
|
172
|
+
if (c.auth === "convex-auth") routes.push(`auth.addHttpRoutes(http);`);
|
|
173
|
+
if (c.auth === "better-auth") routes.push(`authComponent.registerRoutes(http, createAuth, { cors: true });`);
|
|
174
|
+
if (needsChat) {
|
|
175
|
+
routes.push(`http.route({
|
|
176
|
+
path: "/api/chat",
|
|
177
|
+
method: "POST",
|
|
178
|
+
handler: chat,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
http.route({
|
|
182
|
+
path: "/api/chat",
|
|
183
|
+
method: "OPTIONS",
|
|
184
|
+
handler: chatOptions,
|
|
185
|
+
});`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return `${imports.join("\n")}
|
|
189
|
+
|
|
190
|
+
const http = httpRouter();
|
|
191
|
+
|
|
192
|
+
${routes.join("\n\n")}
|
|
193
|
+
|
|
194
|
+
export default http;
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** convex/chat.ts — AI SDK streaming chat as a Convex HTTP action. */
|
|
199
|
+
export function convexChat(c) {
|
|
200
|
+
const ai = AI_MODELS[c.ai];
|
|
201
|
+
return `import { ${ai.importName} } from "${ai.pkg}";
|
|
202
|
+
import { convertToModelMessages, streamText, type UIMessage } from "ai";
|
|
203
|
+
import { httpAction } from "./_generated/server";
|
|
204
|
+
|
|
205
|
+
// CORS headers let the Vite dev server (a different origin) call this endpoint.
|
|
206
|
+
// Tighten Access-Control-Allow-Origin before going to production.
|
|
207
|
+
const corsHeaders = {
|
|
208
|
+
"Access-Control-Allow-Origin": "*",
|
|
209
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
210
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const chat = httpAction(async (_ctx, request) => {
|
|
214
|
+
// Requires ${ai.envKey} on your Convex deployment:
|
|
215
|
+
// npx convex env set ${ai.envKey} <your-key>
|
|
216
|
+
const { messages }: { messages: UIMessage[] } = await request.json();
|
|
217
|
+
|
|
218
|
+
const result = streamText({
|
|
219
|
+
model: ${ai.importName}("${ai.model}"),
|
|
220
|
+
messages: await convertToModelMessages(messages),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return result.toUIMessageStreamResponse({ headers: corsHeaders });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
export const chatOptions = httpAction(async () => {
|
|
227
|
+
return new Response(null, { status: 204, headers: corsHeaders });
|
|
228
|
+
});
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Convex _generated stubs
|
|
234
|
+
//
|
|
235
|
+
// Written at scaffold time so the project typechecks and builds immediately.
|
|
236
|
+
// `convex dev` regenerates these with the same shape on first run.
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
/** List of convex function modules for the api stub. */
|
|
240
|
+
function convexModules(c) {
|
|
241
|
+
const modules = ["tasks"];
|
|
242
|
+
if (c.auth === "convex-auth") modules.push("auth");
|
|
243
|
+
if (c.ai !== "none") modules.push("chat", "http");
|
|
244
|
+
else if (c.auth === "convex-auth") modules.push("http");
|
|
245
|
+
return [...new Set(modules)].sort();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function convexGeneratedApiDts(c) {
|
|
249
|
+
const modules = convexModules(c);
|
|
250
|
+
return `/* eslint-disable */
|
|
251
|
+
/**
|
|
252
|
+
* Generated \`api\` utility.
|
|
253
|
+
*
|
|
254
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
255
|
+
*
|
|
256
|
+
* To regenerate, run \`npx convex dev\`.
|
|
257
|
+
*/
|
|
258
|
+
import type {
|
|
259
|
+
ApiFromModules,
|
|
260
|
+
FilterApi,
|
|
261
|
+
FunctionReference,
|
|
262
|
+
} from "convex/server";
|
|
263
|
+
${modules.map((m) => `import type * as ${m} from "../${m}.js";`).join("\n")}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* A utility for referencing Convex functions in your app's API.
|
|
267
|
+
*
|
|
268
|
+
* Usage:
|
|
269
|
+
* \`\`\`js
|
|
270
|
+
* const myFunctionReference = api.myModule.myFunction;
|
|
271
|
+
* \`\`\`
|
|
272
|
+
*/
|
|
273
|
+
declare const fullApi: ApiFromModules<{
|
|
274
|
+
${modules.map((m) => ` ${m}: typeof ${m};`).join("\n")}
|
|
275
|
+
}>;
|
|
276
|
+
export declare const api: FilterApi<
|
|
277
|
+
typeof fullApi,
|
|
278
|
+
FunctionReference<any, "public">
|
|
279
|
+
>;
|
|
280
|
+
export declare const internal: FilterApi<
|
|
281
|
+
typeof fullApi,
|
|
282
|
+
FunctionReference<any, "internal">
|
|
283
|
+
>;
|
|
284
|
+
`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function convexGeneratedApiJs() {
|
|
288
|
+
return `/* eslint-disable */
|
|
289
|
+
/**
|
|
290
|
+
* Generated \`api\` utility.
|
|
291
|
+
*
|
|
292
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
293
|
+
*
|
|
294
|
+
* To regenerate, run \`npx convex dev\`.
|
|
295
|
+
*/
|
|
296
|
+
import { anyApi } from "convex/server";
|
|
297
|
+
|
|
298
|
+
export const api = anyApi;
|
|
299
|
+
export const internal = anyApi;
|
|
300
|
+
`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function convexGeneratedDataModelDts() {
|
|
304
|
+
return `/* eslint-disable */
|
|
305
|
+
/**
|
|
306
|
+
* Generated data model types.
|
|
307
|
+
*
|
|
308
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
309
|
+
*
|
|
310
|
+
* To regenerate, run \`npx convex dev\`.
|
|
311
|
+
*/
|
|
312
|
+
import type {
|
|
313
|
+
DataModelFromSchemaDefinition,
|
|
314
|
+
DocumentByName,
|
|
315
|
+
TableNamesInDataModel,
|
|
316
|
+
SystemTableNames,
|
|
317
|
+
} from "convex/server";
|
|
318
|
+
import type { GenericId } from "convex/values";
|
|
319
|
+
import schema from "../schema.js";
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The names of all of your Convex tables.
|
|
323
|
+
*/
|
|
324
|
+
export type TableNames = TableNamesInDataModel<DataModel>;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The type of a document stored in Convex.
|
|
328
|
+
*/
|
|
329
|
+
export type Doc<TableName extends TableNames> = DocumentByName<
|
|
330
|
+
DataModel,
|
|
331
|
+
TableName
|
|
332
|
+
>;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* An identifier for a document in Convex.
|
|
336
|
+
*/
|
|
337
|
+
export type Id<TableName extends TableNames | SystemTableNames> =
|
|
338
|
+
GenericId<TableName>;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* A type describing your Convex data model.
|
|
342
|
+
*/
|
|
343
|
+
export type DataModel = DataModelFromSchemaDefinition<typeof schema>;
|
|
344
|
+
`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function convexGeneratedServerDts() {
|
|
348
|
+
return `/* eslint-disable */
|
|
349
|
+
/**
|
|
350
|
+
* Generated utilities for implementing server-side Convex query and mutation functions.
|
|
351
|
+
*
|
|
352
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
353
|
+
*
|
|
354
|
+
* To regenerate, run \`npx convex dev\`.
|
|
355
|
+
*/
|
|
356
|
+
import {
|
|
357
|
+
ActionBuilder,
|
|
358
|
+
HttpActionBuilder,
|
|
359
|
+
MutationBuilder,
|
|
360
|
+
QueryBuilder,
|
|
361
|
+
GenericActionCtx,
|
|
362
|
+
GenericMutationCtx,
|
|
363
|
+
GenericQueryCtx,
|
|
364
|
+
GenericDatabaseReader,
|
|
365
|
+
GenericDatabaseWriter,
|
|
366
|
+
} from "convex/server";
|
|
367
|
+
import type { DataModel } from "./dataModel.js";
|
|
368
|
+
|
|
369
|
+
export declare const query: QueryBuilder<DataModel, "public">;
|
|
370
|
+
export declare const internalQuery: QueryBuilder<DataModel, "internal">;
|
|
371
|
+
export declare const mutation: MutationBuilder<DataModel, "public">;
|
|
372
|
+
export declare const internalMutation: MutationBuilder<DataModel, "internal">;
|
|
373
|
+
export declare const action: ActionBuilder<DataModel, "public">;
|
|
374
|
+
export declare const internalAction: ActionBuilder<DataModel, "internal">;
|
|
375
|
+
export declare const httpAction: HttpActionBuilder;
|
|
376
|
+
|
|
377
|
+
export type QueryCtx = GenericQueryCtx<DataModel>;
|
|
378
|
+
export type MutationCtx = GenericMutationCtx<DataModel>;
|
|
379
|
+
export type ActionCtx = GenericActionCtx<DataModel>;
|
|
380
|
+
export type DatabaseReader = GenericDatabaseReader<DataModel>;
|
|
381
|
+
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
|
|
382
|
+
`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function convexGeneratedServerJs() {
|
|
386
|
+
return `/* eslint-disable */
|
|
387
|
+
/**
|
|
388
|
+
* Generated utilities for implementing server-side Convex query and mutation functions.
|
|
389
|
+
*
|
|
390
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
391
|
+
*
|
|
392
|
+
* To regenerate, run \`npx convex dev\`.
|
|
393
|
+
*/
|
|
394
|
+
import {
|
|
395
|
+
actionGeneric,
|
|
396
|
+
httpActionGeneric,
|
|
397
|
+
queryGeneric,
|
|
398
|
+
mutationGeneric,
|
|
399
|
+
internalActionGeneric,
|
|
400
|
+
internalMutationGeneric,
|
|
401
|
+
internalQueryGeneric,
|
|
402
|
+
} from "convex/server";
|
|
403
|
+
|
|
404
|
+
export const query = queryGeneric;
|
|
405
|
+
export const internalQuery = internalQueryGeneric;
|
|
406
|
+
export const mutation = mutationGeneric;
|
|
407
|
+
export const internalMutation = internalMutationGeneric;
|
|
408
|
+
export const action = actionGeneric;
|
|
409
|
+
export const internalAction = internalActionGeneric;
|
|
410
|
+
export const httpAction = httpActionGeneric;
|
|
411
|
+
`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// Supabase
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
export function supabaseClient() {
|
|
419
|
+
return `import { createClient } from "@supabase/supabase-js";
|
|
420
|
+
|
|
421
|
+
export const supabase = createClient(
|
|
422
|
+
import.meta.env.VITE_SUPABASE_URL,
|
|
423
|
+
import.meta.env.VITE_SUPABASE_ANON_KEY,
|
|
424
|
+
);
|
|
425
|
+
`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
// Drizzle (provider-aware: neon | turso | docker | supabase | other)
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
|
|
432
|
+
export function drizzleConfig(c) {
|
|
433
|
+
if (c.dbProvider === "turso") {
|
|
434
|
+
return `import "dotenv/config";
|
|
435
|
+
import { defineConfig } from "drizzle-kit";
|
|
436
|
+
|
|
437
|
+
export default defineConfig({
|
|
438
|
+
out: "./drizzle",
|
|
439
|
+
schema: "./db/schema.ts",
|
|
440
|
+
dialect: "turso",
|
|
441
|
+
dbCredentials: {
|
|
442
|
+
url: process.env.TURSO_DATABASE_URL!,
|
|
443
|
+
authToken: process.env.TURSO_AUTH_TOKEN,
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
`;
|
|
447
|
+
}
|
|
448
|
+
return `import "dotenv/config";
|
|
449
|
+
import { defineConfig } from "drizzle-kit";
|
|
450
|
+
|
|
451
|
+
export default defineConfig({
|
|
452
|
+
out: "./drizzle",
|
|
453
|
+
schema: "./db/schema.ts",
|
|
454
|
+
dialect: "postgresql",
|
|
455
|
+
dbCredentials: {
|
|
456
|
+
url: process.env.DATABASE_URL!,
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function drizzleSchema(c) {
|
|
463
|
+
if (c.dbProvider === "turso") {
|
|
464
|
+
return `import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
465
|
+
|
|
466
|
+
export const tasks = sqliteTable("tasks", {
|
|
467
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
468
|
+
text: text("text").notNull(),
|
|
469
|
+
isCompleted: integer("is_completed", { mode: "boolean" }).notNull().default(false),
|
|
470
|
+
createdAt: integer("created_at", { mode: "timestamp" })
|
|
471
|
+
.notNull()
|
|
472
|
+
.$defaultFn(() => new Date()),
|
|
473
|
+
});
|
|
474
|
+
`;
|
|
475
|
+
}
|
|
476
|
+
return `import { boolean, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
|
|
477
|
+
|
|
478
|
+
export const tasks = pgTable("tasks", {
|
|
479
|
+
id: serial("id").primaryKey(),
|
|
480
|
+
text: text("text").notNull(),
|
|
481
|
+
isCompleted: boolean("is_completed").notNull().default(false),
|
|
482
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
483
|
+
});
|
|
484
|
+
`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function drizzleIndex(c) {
|
|
488
|
+
const header = `// Server-side only — never import this from browser code.
|
|
489
|
+
// Use it from scripts, API routes, or server functions.`;
|
|
490
|
+
if (c.dbProvider === "neon") {
|
|
491
|
+
return `${header}
|
|
492
|
+
import { neon } from "@neondatabase/serverless";
|
|
493
|
+
import { drizzle } from "drizzle-orm/neon-http";
|
|
494
|
+
import * as schema from "./schema";
|
|
495
|
+
|
|
496
|
+
const sql = neon(process.env.DATABASE_URL!);
|
|
497
|
+
|
|
498
|
+
export const db = drizzle({ client: sql, schema });
|
|
499
|
+
`;
|
|
500
|
+
}
|
|
501
|
+
if (c.dbProvider === "turso") {
|
|
502
|
+
return `${header}
|
|
503
|
+
import { drizzle } from "drizzle-orm/libsql";
|
|
504
|
+
import * as schema from "./schema";
|
|
505
|
+
|
|
506
|
+
export const db = drizzle({
|
|
507
|
+
connection: {
|
|
508
|
+
url: process.env.TURSO_DATABASE_URL!,
|
|
509
|
+
authToken: process.env.TURSO_AUTH_TOKEN,
|
|
510
|
+
},
|
|
511
|
+
schema,
|
|
512
|
+
});
|
|
513
|
+
`;
|
|
514
|
+
}
|
|
515
|
+
// docker / supabase / other -> standard postgres.js driver
|
|
516
|
+
return `${header}
|
|
517
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
518
|
+
import postgres from "postgres";
|
|
519
|
+
import * as schema from "./schema";
|
|
520
|
+
|
|
521
|
+
const client = postgres(process.env.DATABASE_URL!);
|
|
522
|
+
|
|
523
|
+
export const db = drizzle(client, { schema });
|
|
524
|
+
`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Runtime deps for the chosen drizzle provider (dotenv is a devDep — only configs use it). */
|
|
528
|
+
export function drizzleDeps(c) {
|
|
529
|
+
const deps = ["drizzle-orm"];
|
|
530
|
+
if (c.dbProvider === "neon") deps.push("@neondatabase/serverless");
|
|
531
|
+
else if (c.dbProvider === "turso") deps.push("@libsql/client");
|
|
532
|
+
else deps.push("postgres");
|
|
533
|
+
return deps;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ---------------------------------------------------------------------------
|
|
537
|
+
// Local Postgres via Docker
|
|
538
|
+
// ---------------------------------------------------------------------------
|
|
539
|
+
|
|
540
|
+
export function dockerCompose(c) {
|
|
541
|
+
return `services:
|
|
542
|
+
postgres:
|
|
543
|
+
image: postgres:17
|
|
544
|
+
container_name: ${c.name}-postgres
|
|
545
|
+
restart: unless-stopped
|
|
546
|
+
environment:
|
|
547
|
+
POSTGRES_USER: postgres
|
|
548
|
+
POSTGRES_PASSWORD: postgres
|
|
549
|
+
POSTGRES_DB: ${c.name.replace(/[^a-z0-9_]/gi, "_")}
|
|
550
|
+
ports:
|
|
551
|
+
- "5432:5432"
|
|
552
|
+
volumes:
|
|
553
|
+
- pgdata:/var/lib/postgresql/data
|
|
554
|
+
healthcheck:
|
|
555
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
556
|
+
interval: 5s
|
|
557
|
+
timeout: 5s
|
|
558
|
+
retries: 5
|
|
559
|
+
|
|
560
|
+
volumes:
|
|
561
|
+
pgdata:
|
|
562
|
+
`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
// Upstash Redis (serverless Redis: rate limiting, caching, sessions)
|
|
567
|
+
// ---------------------------------------------------------------------------
|
|
568
|
+
|
|
569
|
+
export function upstashRedis() {
|
|
570
|
+
return `// Server-side only — the REST token grants full database access.
|
|
571
|
+
// Use from scripts, API routes, or server functions, never from browser code.
|
|
572
|
+
import { Redis } from "@upstash/redis";
|
|
573
|
+
|
|
574
|
+
export const redis = new Redis({
|
|
575
|
+
url: process.env.UPSTASH_REDIS_REST_URL!,
|
|
576
|
+
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
|
|
577
|
+
});
|
|
578
|
+
`;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function upstashRatelimit() {
|
|
582
|
+
return `import { Ratelimit } from "@upstash/ratelimit";
|
|
583
|
+
import { redis } from "./redis";
|
|
584
|
+
|
|
585
|
+
// Allow 10 requests per 10 seconds per identifier (IP, user id, ...).
|
|
586
|
+
// Usage:
|
|
587
|
+
// const { success } = await ratelimit.limit(identifier);
|
|
588
|
+
// if (!success) return new Response("Too many requests", { status: 429 });
|
|
589
|
+
export const ratelimit = new Ratelimit({
|
|
590
|
+
redis,
|
|
591
|
+
limiter: Ratelimit.slidingWindow(10, "10 s"),
|
|
592
|
+
});
|
|
593
|
+
`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ---------------------------------------------------------------------------
|
|
597
|
+
// Better Auth (via the official Convex component)
|
|
598
|
+
// ---------------------------------------------------------------------------
|
|
599
|
+
|
|
600
|
+
/** convex/convex.config.ts — registers the Better Auth component. */
|
|
601
|
+
export function convexConfigBetterAuth() {
|
|
602
|
+
return `import betterAuth from "@convex-dev/better-auth/convex.config";
|
|
603
|
+
import { defineApp } from "convex/server";
|
|
604
|
+
|
|
605
|
+
const app = defineApp();
|
|
606
|
+
app.use(betterAuth);
|
|
607
|
+
|
|
608
|
+
export default app;
|
|
609
|
+
`;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** convex/auth.ts (Better Auth flavor). */
|
|
613
|
+
export function convexBetterAuthTs() {
|
|
614
|
+
return `import { createClient, type GenericCtx } from "@convex-dev/better-auth";
|
|
615
|
+
import { convex, crossDomain } from "@convex-dev/better-auth/plugins";
|
|
616
|
+
import { betterAuth } from "better-auth/minimal";
|
|
617
|
+
import { components } from "./_generated/api";
|
|
618
|
+
import type { DataModel } from "./_generated/dataModel";
|
|
619
|
+
import { query } from "./_generated/server";
|
|
620
|
+
import authConfig from "./auth.config";
|
|
621
|
+
|
|
622
|
+
const siteUrl = process.env.SITE_URL!;
|
|
623
|
+
|
|
624
|
+
export const authComponent = createClient<DataModel>(components.betterAuth);
|
|
625
|
+
|
|
626
|
+
export const createAuth = (ctx: GenericCtx<DataModel>) => {
|
|
627
|
+
return betterAuth({
|
|
628
|
+
baseURL: process.env.CONVEX_SITE_URL,
|
|
629
|
+
trustedOrigins: [siteUrl],
|
|
630
|
+
database: authComponent.adapter(ctx),
|
|
631
|
+
emailAndPassword: {
|
|
632
|
+
enabled: true,
|
|
633
|
+
requireEmailVerification: false,
|
|
634
|
+
},
|
|
635
|
+
plugins: [crossDomain({ siteUrl }), convex({ authConfig })],
|
|
636
|
+
});
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
export const getCurrentUser = query({
|
|
640
|
+
args: {},
|
|
641
|
+
handler: async (ctx) => authComponent.getAuthUser(ctx),
|
|
642
|
+
});
|
|
643
|
+
`;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** src/lib/auth-client.ts (Better Auth React client). */
|
|
647
|
+
export function betterAuthClient() {
|
|
648
|
+
return `import {
|
|
649
|
+
convexClient,
|
|
650
|
+
crossDomainClient,
|
|
651
|
+
} from "@convex-dev/better-auth/client/plugins";
|
|
652
|
+
import { createAuthClient } from "better-auth/react";
|
|
653
|
+
|
|
654
|
+
export const authClient = createAuthClient({
|
|
655
|
+
baseURL: import.meta.env.VITE_CONVEX_SITE_URL,
|
|
656
|
+
plugins: [convexClient(), crossDomainClient()],
|
|
657
|
+
});
|
|
658
|
+
`;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ---------------------------------------------------------------------------
|
|
662
|
+
// Prisma (fallback schema if `prisma init` is unavailable)
|
|
663
|
+
// ---------------------------------------------------------------------------
|
|
664
|
+
|
|
665
|
+
export function prismaTaskModel() {
|
|
666
|
+
return `
|
|
667
|
+
model Task {
|
|
668
|
+
id Int @id @default(autoincrement())
|
|
669
|
+
text String
|
|
670
|
+
isCompleted Boolean @default(false)
|
|
671
|
+
createdAt DateTime @default(now())
|
|
672
|
+
}
|
|
673
|
+
`;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export function prismaFallbackSchema() {
|
|
677
|
+
return `// Prisma schema — docs: https://pris.ly/d/prisma-schema
|
|
678
|
+
|
|
679
|
+
generator client {
|
|
680
|
+
provider = "prisma-client-js"
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
datasource db {
|
|
684
|
+
provider = "postgresql"
|
|
685
|
+
url = env("DATABASE_URL")
|
|
686
|
+
}
|
|
687
|
+
${prismaTaskModel()}`;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
691
|
+
// AI SDK (no Convex backend) — server-side example
|
|
692
|
+
// ---------------------------------------------------------------------------
|
|
693
|
+
|
|
694
|
+
export function aiExample(c) {
|
|
695
|
+
const ai = AI_MODELS[c.ai];
|
|
696
|
+
return `// Example: generating text with the AI SDK (${ai.pkg}).
|
|
697
|
+
//
|
|
698
|
+
// IMPORTANT: AI provider calls must run on a server (Node script, API route,
|
|
699
|
+
// edge function, Convex action, ...). Never expose ${ai.envKey} to the browser.
|
|
700
|
+
//
|
|
701
|
+
// Try it:
|
|
702
|
+
// 1. Set ${ai.envKey} in .env.local
|
|
703
|
+
// 2. Run: npx tsx --env-file=.env.local examples/ai.ts
|
|
704
|
+
|
|
705
|
+
import { ${ai.importName} } from "${ai.pkg}";
|
|
706
|
+
import { generateText } from "ai";
|
|
707
|
+
|
|
708
|
+
const { text } = await generateText({
|
|
709
|
+
model: ${ai.importName}("${ai.model}"),
|
|
710
|
+
prompt: "Write a haiku about React.",
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
console.log(text);
|
|
714
|
+
`;
|
|
715
|
+
}
|