create-dstack 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,8 @@ Scaffold a production-ready Next.js app with a single command.
6
6
  bunx create-dstack my-app
7
7
  ```
8
8
 
9
+ ![Demo](./assets/demo.gif)
10
+
9
11
  ## What's included
10
12
 
11
13
 
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
- #!/usr/bin/env node
4
3
  var __create = Object.create;
5
4
  var __getProtoOf = Object.getPrototypeOf;
6
5
  var __defProp = Object.defineProperty;
@@ -163,15 +162,15 @@ var require_picocolors = __commonJS((exports, module) => {
163
162
 
164
163
  // src/index.ts
165
164
  var {$: $2 } = globalThis.Bun;
166
- import { existsSync, cpSync, mkdirSync, appendFileSync } from "fs";
165
+ import { existsSync, cpSync } from "fs";
167
166
  import { join, resolve, dirname } from "path";
168
167
  import { fileURLToPath } from "url";
169
168
 
170
169
  // node_modules/@clack/core/dist/index.mjs
171
- import { styleText as y } from "node:util";
172
- import { stdout as S, stdin as $ } from "node:process";
173
- import * as _ from "node:readline";
174
- import P from "node:readline";
170
+ import { styleText as y } from "util";
171
+ import { stdout as S, stdin as $ } from "process";
172
+ import * as _ from "readline";
173
+ import P from "readline";
175
174
 
176
175
  // node_modules/fast-string-truncated-width/dist/utils.js
177
176
  var isAmbiguous = (x) => {
@@ -350,7 +349,7 @@ var dist_default2 = fastStringWidth;
350
349
 
351
350
  // node_modules/fast-wrap-ansi/lib/main.js
352
351
  var ESC = "\x1B";
353
- var CSI = "›";
352
+ var CSI = "\x9B";
354
353
  var END_CODE = 39;
355
354
  var ANSI_ESCAPE_BELL = "\x07";
356
355
  var ANSI_CSI = "[";
@@ -559,7 +558,7 @@ function wrapAnsi(string, columns, options) {
559
558
 
560
559
  // node_modules/@clack/core/dist/index.mjs
561
560
  var import_sisteransi = __toESM(require_src(), 1);
562
- import { ReadStream as D } from "node:tty";
561
+ import { ReadStream as D } from "tty";
563
562
  function d(r, t, e) {
564
563
  if (!e.some((o) => !o.disabled))
565
564
  return r;
@@ -620,12 +619,6 @@ function z({ input: r = $, output: t = S, overwrite: e = true, hideCursor: s = t
620
619
  }
621
620
  var O = (r) => ("columns" in r) && typeof r.columns == "number" ? r.columns : 80;
622
621
  var A = (r) => ("rows" in r) && typeof r.rows == "number" ? r.rows : 20;
623
- function R(r, t, e, s = e) {
624
- const i = O(r ?? S);
625
- return wrapAnsi(t, i - e.length, { hard: true, trim: false }).split(`
626
- `).map((n, o) => `${o === 0 ? s : e}${n}`).join(`
627
- `);
628
- }
629
622
  var p = class {
630
623
  input;
631
624
  output;
@@ -784,7 +777,7 @@ var H = class extends p {
784
777
  if (!this.userInput)
785
778
  return y(["inverse", "hidden"], "_");
786
779
  if (this._cursor >= this.userInput.length)
787
- return `${this.userInput}█`;
780
+ return `${this.userInput}\u2588`;
788
781
  const t = this.userInput.slice(0, this._cursor), [e, ...s] = this.userInput.slice(this._cursor);
789
782
  return `${t}${y("inverse", e)}${s.join("")}`;
790
783
  }
@@ -832,24 +825,6 @@ var H = class extends p {
832
825
  }
833
826
  }
834
827
  };
835
-
836
- class Q extends p {
837
- get cursor() {
838
- return this.value ? 0 : 1;
839
- }
840
- get _value() {
841
- return this.cursor === 0;
842
- }
843
- constructor(t) {
844
- super(t, false), this.value = !!t.initialValue, this.on("userInput", () => {
845
- this.value = this._value;
846
- }), this.on("confirm", (e) => {
847
- this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = e, this.state = "submit", this.close();
848
- }), this.on("cursor", () => {
849
- this.value = !this.value;
850
- });
851
- }
852
- }
853
828
  var X = { Y: { type: "year", len: 4 }, M: { type: "month", len: 2 }, D: { type: "day", len: 2 } };
854
829
  function L(r) {
855
830
  return [...r].map((t) => X[t]);
@@ -954,7 +929,7 @@ class et extends p {
954
929
  }
955
930
  }
956
931
  #f(t, e) {
957
- if (e?.name === "backspace" || e?.sequence === "" || e?.sequence === "\b" || t === "" || t === "\b") {
932
+ if (e?.name === "backspace" || e?.sequence === "\x7F" || e?.sequence === "\b" || t === "\x7F" || t === "\b") {
958
933
  this.inlineError = "";
959
934
  const s = this.#u();
960
935
  if (!s)
@@ -1086,7 +1061,7 @@ class at extends p {
1086
1061
  return this.userInput;
1087
1062
  const t = this.userInput;
1088
1063
  if (this.cursor >= t.length)
1089
- return `${this.userInput}█`;
1064
+ return `${this.userInput}\u2588`;
1090
1065
  const e = t.slice(0, this.cursor), [s, ...i] = t.slice(this.cursor);
1091
1066
  return `${e}${y("inverse", s)}${i.join("")}`;
1092
1067
  }
@@ -1103,8 +1078,8 @@ class at extends p {
1103
1078
  }
1104
1079
 
1105
1080
  // node_modules/@clack/prompts/dist/index.mjs
1106
- import { styleText as t, stripVTControlCharacters as ne } from "node:util";
1107
- import P2 from "node:process";
1081
+ import { styleText as t, stripVTControlCharacters as ne } from "util";
1082
+ import P2 from "process";
1108
1083
  var import_sisteransi2 = __toESM(require_src(), 1);
1109
1084
  function Ze() {
1110
1085
  return P2.platform !== "win32" ? P2.env.TERM !== "linux" : !!P2.env.CI || !!P2.env.WT_SESSION || !!P2.env.TERMINUS_SUBLIME || P2.env.ConEmuTask === "{cmd::Cmder}" || P2.env.TERM_PROGRAM === "Terminus-Sublime" || P2.env.TERM_PROGRAM === "vscode" || P2.env.TERM === "xterm-256color" || P2.env.TERM === "alacritty" || P2.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
@@ -1112,31 +1087,31 @@ function Ze() {
1112
1087
  var ee = Ze();
1113
1088
  var ae = () => process.env.CI === "true";
1114
1089
  var w2 = (e, i) => ee ? e : i;
1115
- var _e = w2("", "*");
1116
- var oe = w2("", "x");
1117
- var ue = w2("", "x");
1118
- var F2 = w2("", "o");
1119
- var le = w2("", "T");
1120
- var d2 = w2("", "|");
1121
- var E2 = w2("", "");
1122
- var Ie = w2("", "T");
1123
- var Ee = w2("", "");
1124
- var z2 = w2("", ">");
1125
- var H2 = w2("", " ");
1126
- var te = w2("", "[]");
1127
- var U = w2("", "[+]");
1128
- var J2 = w2("", "[ ]");
1129
- var xe = w2("", "");
1130
- var se = w2("", "-");
1131
- var ce = w2("", "+");
1132
- var Ge = w2("", "+");
1133
- var $e = w2("", "+");
1134
- var de = w2("", "+");
1135
- var Oe = w2("", "+");
1136
- var he = w2("", "");
1137
- var pe = w2("", "*");
1138
- var me = w2("", "!");
1139
- var ge = w2("", "x");
1090
+ var _e = w2("\u25C6", "*");
1091
+ var oe = w2("\u25A0", "x");
1092
+ var ue = w2("\u25B2", "x");
1093
+ var F2 = w2("\u25C7", "o");
1094
+ var le = w2("\u250C", "T");
1095
+ var d2 = w2("\u2502", "|");
1096
+ var E2 = w2("\u2514", "\u2014");
1097
+ var Ie = w2("\u2510", "T");
1098
+ var Ee = w2("\u2518", "\u2014");
1099
+ var z2 = w2("\u25CF", ">");
1100
+ var H2 = w2("\u25CB", " ");
1101
+ var te = w2("\u25FB", "[\u2022]");
1102
+ var U = w2("\u25FC", "[+]");
1103
+ var J2 = w2("\u25FB", "[ ]");
1104
+ var xe = w2("\u25AA", "\u2022");
1105
+ var se = w2("\u2500", "-");
1106
+ var ce = w2("\u256E", "+");
1107
+ var Ge = w2("\u251C", "+");
1108
+ var $e = w2("\u256F", "+");
1109
+ var de = w2("\u2570", "+");
1110
+ var Oe = w2("\u256D", "+");
1111
+ var he = w2("\u25CF", "\u2022");
1112
+ var pe = w2("\u25C6", "*");
1113
+ var me = w2("\u25B2", "!");
1114
+ var ge = w2("\u25A0", "x");
1140
1115
  var V2 = (e) => {
1141
1116
  switch (e) {
1142
1117
  case "initial":
@@ -1150,61 +1125,6 @@ var V2 = (e) => {
1150
1125
  return t("green", F2);
1151
1126
  }
1152
1127
  };
1153
- var ot2 = (e) => {
1154
- const i = e.active ?? "Yes", s = e.inactive ?? "No";
1155
- return new Q({ active: i, inactive: s, signal: e.signal, input: e.input, output: e.output, initialValue: e.initialValue ?? true, render() {
1156
- const r = e.withGuide ?? u.withGuide, u2 = `${V2(this.state)} `, n = r ? `${t("gray", d2)} ` : "", o = R(e.output, e.message, n, u2), c2 = `${r ? `${t("gray", d2)}
1157
- ` : ""}${o}
1158
- `, a = this.value ? i : s;
1159
- switch (this.state) {
1160
- case "submit": {
1161
- const l = r ? `${t("gray", d2)} ` : "";
1162
- return `${c2}${l}${t("dim", a)}`;
1163
- }
1164
- case "cancel": {
1165
- const l = r ? `${t("gray", d2)} ` : "";
1166
- return `${c2}${l}${t(["strikethrough", "dim"], a)}${r ? `
1167
- ${t("gray", d2)}` : ""}`;
1168
- }
1169
- default: {
1170
- const l = r ? `${t("cyan", d2)} ` : "", $2 = r ? t("cyan", E2) : "";
1171
- return `${c2}${l}${this.value ? `${t("green", z2)} ${i}` : `${t("dim", H2)} ${t("dim", i)}`}${e.vertical ? r ? `
1172
- ${t("cyan", d2)} ` : `
1173
- ` : ` ${t("dim", "/")} `}${this.value ? `${t("dim", H2)} ${t("dim", s)}` : `${t("green", z2)} ${s}`}
1174
- ${$2}
1175
- `;
1176
- }
1177
- }
1178
- } }).prompt();
1179
- };
1180
- var O2 = { message: (e = [], { symbol: i = t("gray", d2), secondarySymbol: s = t("gray", d2), output: r = process.stdout, spacing: u2 = 1, withGuide: n } = {}) => {
1181
- const o = [], c2 = n ?? u.withGuide, a = c2 ? s : "", l = c2 ? `${i} ` : "", $2 = c2 ? `${s} ` : "";
1182
- for (let p2 = 0;p2 < u2; p2++)
1183
- o.push(a);
1184
- const y2 = Array.isArray(e) ? e : e.split(`
1185
- `);
1186
- if (y2.length > 0) {
1187
- const [p2, ...m] = y2;
1188
- p2.length > 0 ? o.push(`${l}${p2}`) : o.push(c2 ? i : "");
1189
- for (const g of m)
1190
- g.length > 0 ? o.push(`${$2}${g}`) : o.push(c2 ? s : "");
1191
- }
1192
- r.write(`${o.join(`
1193
- `)}
1194
- `);
1195
- }, info: (e, i) => {
1196
- O2.message(e, { ...i, symbol: t("blue", he) });
1197
- }, success: (e, i) => {
1198
- O2.message(e, { ...i, symbol: t("green", pe) });
1199
- }, step: (e, i) => {
1200
- O2.message(e, { ...i, symbol: t("green", F2) });
1201
- }, warn: (e, i) => {
1202
- O2.message(e, { ...i, symbol: t("yellow", me) });
1203
- }, warning: (e, i) => {
1204
- O2.warn(e, i);
1205
- }, error: (e, i) => {
1206
- O2.message(e, { ...i, symbol: t("red", ge) });
1207
- } };
1208
1128
  var pt = (e = "", i) => {
1209
1129
  const s = i?.output ?? process.stdout, r = i?.withGuide ?? u.withGuide ? `${t("gray", E2)} ` : "";
1210
1130
  s.write(`${r}${t("red", e)}
@@ -1224,7 +1144,7 @@ ${t("gray", E2)} ` : "";
1224
1144
  `);
1225
1145
  };
1226
1146
  var Ct = (e) => t("magenta", e);
1227
- var fe = ({ indicator: e = "dots", onCancel: i, output: s = process.stdout, cancelMessage: r, errorMessage: u2, frames: n = ee ? ["", "", "", ""] : ["", "o", "O", "0"], delay: o = ee ? 80 : 120, signal: c2, ...a } = {}) => {
1147
+ var fe = ({ indicator: e = "dots", onCancel: i, output: s = process.stdout, cancelMessage: r, errorMessage: u2, frames: n = ee ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"], delay: o = ee ? 80 : 120, signal: c2, ...a } = {}) => {
1228
1148
  const l = ae();
1229
1149
  let $2, y2, p2 = false, m = false, g = "", S2, h = performance.now();
1230
1150
  const f = O(s), v = a?.styleFrame ?? Ct, T2 = (_2) => {
@@ -1281,7 +1201,7 @@ var fe = ({ indicator: e = "dots", onCancel: i, output: s = process.stdout, canc
1281
1201
  return m;
1282
1202
  } };
1283
1203
  };
1284
- var Ve = { light: w2("", "-"), heavy: w2("", "="), block: w2("", "#") };
1204
+ var Ve = { light: w2("\u2500", "-"), heavy: w2("\u2501", "="), block: w2("\u2588", "#") };
1285
1205
  var je = `${t("gray", d2)} `;
1286
1206
  var Ot = (e) => new at({ validate: e.validate, placeholder: e.placeholder, defaultValue: e.defaultValue, initialValue: e.initialValue, output: e.output, signal: e.signal, input: e.input, render() {
1287
1207
  const i = e?.withGuide ?? u.withGuide, s = `${`${i ? `${t("gray", d2)}
@@ -1338,11 +1258,6 @@ async function main() {
1338
1258
  }
1339
1259
  projectName = input;
1340
1260
  }
1341
- const useStackAuth = await ot2({ message: "Add Stack Auth?" });
1342
- if (q(useStackAuth)) {
1343
- pt("Cancelled.");
1344
- process.exit(0);
1345
- }
1346
1261
  const projectDir = resolve(process.cwd(), projectName);
1347
1262
  if (existsSync(projectDir)) {
1348
1263
  pt(`Directory "${projectName}" already exists.`);
@@ -1350,7 +1265,7 @@ async function main() {
1350
1265
  }
1351
1266
  const s = fe();
1352
1267
  s.start("preheating the oven...");
1353
- await $2`bunx create-next-app@latest ${projectName} --typescript --tailwind --no-eslint --app --src-dir --import-alias "@/*" --use-bun`.quiet();
1268
+ await $2`bunx create-next-app@latest ${projectName} --typescript --tailwind --no-eslint --app --no-src-dir --import-alias "@/*" --use-bun`.quiet();
1354
1269
  s.stop("next.js is in the chat");
1355
1270
  s.start("calling convex off the bench...");
1356
1271
  await $2`bun add convex`.cwd(projectDir).quiet();
@@ -1361,29 +1276,8 @@ async function main() {
1361
1276
  s.start("adding the drip (shadcn)...");
1362
1277
  await $2`bunx shadcn@latest init -d`.cwd(projectDir).quiet();
1363
1278
  s.stop("looking fire already");
1364
- if (useStackAuth) {
1365
- s.start("sliding stack auth into the dm...");
1366
- await $2`bun add @stackframe/stack`.cwd(projectDir).quiet();
1367
- s.stop("auth is cooked and ready");
1368
- mkdirSync(join(projectDir, "convex"), { recursive: true });
1369
- copyTemplate(join(TEMPLATES_DIR, "convex", "auth.config.ts"), join(projectDir, "convex", "auth.config.ts"));
1370
- appendFileSync(join(projectDir, ".env.local"), [
1371
- "",
1372
- "# Stack Auth \u2014 fill in from https://app.stack-auth.com",
1373
- "NEXT_PUBLIC_STACK_PROJECT_ID=",
1374
- "NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=",
1375
- "STACK_SECRET_SERVER_KEY=",
1376
- ""
1377
- ].join(`
1378
- `));
1379
- O2.warn(`Stack Auth needs manual steps:
1380
- ` + ` 1. Create a project at ${import_picocolors.default.cyan("https://app.stack-auth.com")}
1381
- ` + ` 2. Fill in ${import_picocolors.default.dim(".env.local")} with your keys
1382
- ` + ` 3. Set the same vars in your Convex dashboard
1383
- ` + ` 4. Run ${import_picocolors.default.cyan("bunx @stackframe/stack-cli@latest init")} inside the project to finish wiring up the provider`);
1384
- }
1385
1279
  s.start("putting the secret sauce on it...");
1386
- copyTemplate(join(TEMPLATES_DIR, "globals.css"), join(projectDir, "src", "app", "globals.css"));
1280
+ copyTemplate(join(TEMPLATES_DIR, "globals.css"), join(projectDir, "app", "globals.css"));
1387
1281
  copyTemplate(join(TEMPLATES_DIR, "oxlint.json"), join(projectDir, "oxlint.json"));
1388
1282
  copyTemplate(join(TEMPLATES_DIR, "oxfmt.json"), join(projectDir, "oxfmt.json"));
1389
1283
  if (existsSync(join(TEMPLATES_DIR, ".claude"))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dstack",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Create a new dstack project — Next.js, Bun, Convex, Shadcn, Oxlint, Oxfmt",
5
5
  "type": "module",
6
6
  "repository": {
@@ -11,7 +11,7 @@
11
11
  "create-dstack": "./dist/index.js"
12
12
  },
13
13
  "scripts": {
14
- "build": "bun build ./src/index.ts --outfile ./dist/index.js --target node --banner '#!/usr/bin/env node'",
14
+ "build": "bun build ./src/index.ts --outfile ./dist/index.js --target bun",
15
15
  "start": "bun run ./src/index.ts",
16
16
  "prepublishOnly": "bun run build"
17
17
  },
@@ -19,7 +19,6 @@ bunx oxfmt --check . # Check formatting
19
19
  - React 19
20
20
  - TypeScript 5 with strict mode
21
21
  - Convex for DB
22
- - StackAuth for auth, teams, user metadata
23
22
  - All database types in Convex
24
23
 
25
24
  ## Conventions
@@ -49,7 +48,6 @@ API routes: mirror domains (app/api/mail/, app/api/profile/)
49
48
 
50
49
  ## Authentication & Security
51
50
 
52
- - User endpoints: StackAuth server session for API routes and Server Actions; `useUser` only in client components
53
51
  - System/cron: Bearer token
54
52
  - Error handling: `throwOnError` or `if (error)` patterns
55
53
 
@@ -19,7 +19,6 @@ bunx oxfmt --check . # Check formatting
19
19
  - React 19
20
20
  - TypeScript 5 with strict mode
21
21
  - Convex for DB
22
- - StackAuth for auth, teams, user metadata
23
22
  - All database types in Convex
24
23
 
25
24
  ## Conventions
@@ -49,7 +48,6 @@ API routes: mirror domains (app/api/mail/, app/api/profile/)
49
48
 
50
49
  ## Authentication & Security
51
50
 
52
- - User endpoints: StackAuth server session for API routes and Server Actions; `useUser` only in client components
53
51
  - System/cron: Bearer token
54
52
  - Error handling: `throwOnError` or `if (error)` patterns
55
53
 
@@ -1,198 +0,0 @@
1
- ---
2
- name: api
3
- description: Whenever designing or working on API routes
4
- ---
5
-
6
- # API Development Rules
7
-
8
- Every API route is either user-authenticated or system-authenticated. No exceptions.
9
-
10
- ## 1. Route Handlers
11
-
12
- - Use standard Next.js `NextRequest` and `NextResponse` types only
13
- - No custom middleware that wraps handlers
14
-
15
- ## 2. Authentication
16
-
17
- ### User APIs (browser/app requests)
18
-
19
- Resolve the signed-in user with **StackAuth on the server**. In Route Handlers and Server Actions, use Stack's server-side session APIs—**not** the React `useUser` hook (that is client-only).
20
-
21
- After auth, read and write data through **Convex** (`fetchQuery`, `fetchMutation`, `fetchAction` from `convex/nextjs`) so authorization can rely on `ctx.auth` inside Convex functions. Pass the Convex auth token when your setup requires it (same pattern as other OIDC providers: token option on `fetch*`).
22
-
23
- ```typescript
24
- import { fetchMutation } from "convex/nextjs";
25
- import { api } from "@/convex/_generated/api";
26
- import type { NextRequest } from "next/server";
27
-
28
- export async function PATCH(request: NextRequest) {
29
- // 1. Resolve user with StackAuth server APIs; return 401 if missing
30
- // 2. Build Convex token if your Stack+Convex integration uses JWT forwarding
31
- const token = await getConvexAuthTokenFromRequest(request);
32
- const args = await request.json();
33
- await fetchMutation(api.example.updateThing, args, { token });
34
- }
35
- ```
36
-
37
- ### Cron/System APIs
38
-
39
- Use Bearer token + trusted Convex entry points:
40
-
41
- ```typescript
42
- const authHeader = request.headers.get("authorization");
43
- if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
44
- return new Response("Unauthorized", { status: 401 });
45
- }
46
-
47
- await fetchMutation(api.jobs.runScheduled, payload);
48
- ```
49
-
50
- Use **deployment-appropriate** Convex access for system jobs (e.g. internal mutations or HTTP actions that validate the same secret). Never reuse a "god mode" client for user-initiated work.
51
-
52
- ### Access shape
53
-
54
- | Caller | Auth | Data layer |
55
- | ----------------- | ----------------- | ----------------------------------- |
56
- | User API route | StackAuth session | Convex via `fetch*` + user token |
57
- | System / cron API | Bearer secret | Convex mutation/action for batch/system work |
58
-
59
- Never bypass user-level Convex auth for operations that should be scoped to a single user.
60
-
61
- ## 3. Validation
62
-
63
- **Every API route MUST validate with Zod.** No exceptions.
64
-
65
- ### Validation Order: Fail Fast
66
-
67
- 1. Authenticate
68
- 2. Parse and validate request body immediately
69
- 3. Then do expensive operations (Convex calls, external APIs)
70
-
71
- ### Schema Patterns
72
-
73
- - Use `.trim()` for strings, `.email()` for emails
74
- - Use `.refine()` for custom business logic
75
- - Use `z.discriminatedUnion()` for conditional validation
76
-
77
- ## 4. Error Response Format
78
-
79
- All errors use Stripe-style format:
80
-
81
- ```typescript
82
- {
83
- error: {
84
- type: string; // Required: validation_error, authentication_error, api_error, etc.
85
- message: string; // Required: Human-readable
86
- code?: string; // Optional: MISSING_EMAIL, QUOTA_EXCEEDED, etc.
87
- param?: string; // Optional: Field name for validation errors
88
- details?: unknown; // Optional: Zod errors array
89
- }
90
- }
91
- ```
92
-
93
- ### Error Types by Status
94
-
95
- - `400` validation_error - Invalid input
96
- - `401` authentication_error - Not authenticated
97
- - `403` authorization_error - Not authorized
98
- - `404` not_found_error - Resource missing
99
- - `429` rate_limit_error - Quota exceeded
100
- - `500` api_error - Internal error
101
- - `503` service_unavailable_error - External service down
102
-
103
- ### Forbidden Formats
104
-
105
- ```typescript
106
- // NEVER use these:
107
- { success: false, error: "message" }
108
- { error: "string" } // Must be object
109
- { message: "..." } // Must use error.message
110
- ```
111
-
112
- ## 5. Standard API Template
113
-
114
- ```typescript
115
- import { NextRequest, NextResponse } from "next/server";
116
- import { z } from "zod";
117
- import { fetchMutation } from "convex/nextjs";
118
- import { api } from "@/convex/_generated/api";
119
-
120
- const schema = z.object({
121
- email: z.string().email(),
122
- name: z.string().min(1),
123
- });
124
-
125
- export async function POST(request: NextRequest) {
126
- const user = await getStackAuthUserFromRequest(request);
127
- if (!user) {
128
- return NextResponse.json(
129
- {
130
- error: { type: "authentication_error", message: "Unauthorized" },
131
- },
132
- { status: 401 },
133
- );
134
- }
135
-
136
- try {
137
- const rawBody = await request.json();
138
- const data = schema.parse(rawBody);
139
-
140
- const token = await getConvexAuthTokenFromRequest(request, user);
141
- const result = await fetchMutation(api.example.createProfile, data, { token });
142
-
143
- return NextResponse.json({ success: true, data: result });
144
- } catch (error) {
145
- if (error instanceof z.ZodError) {
146
- return NextResponse.json(
147
- {
148
- error: {
149
- type: "validation_error",
150
- message: "Validation failed",
151
- details: error.issues,
152
- },
153
- },
154
- { status: 400 },
155
- );
156
- }
157
- console.error("API Error:", error);
158
- return NextResponse.json(
159
- {
160
- error: { type: "api_error", message: "Internal server error" },
161
- },
162
- { status: 500 },
163
- );
164
- }
165
- }
166
- ```
167
-
168
- Replace `getStackAuthUserFromRequest` / `getConvexAuthTokenFromRequest` with the project's StackAuth server helpers and Convex token bridge.
169
-
170
- ## 6. Security
171
-
172
- - Always sanitize inputs: `.trim()`, `.toLowerCase()` for emails
173
- - Check user quotas before expensive operations via `user.clientReadOnlyMetadata`
174
- - Use Supabase query builders, never raw SQL with user input
175
- - Validate environment variables exist before using
176
- - There is no SQL layer; do not concatenate user input into query strings for external services
177
-
178
- ## 7. Database Best Practices (Convex)
179
-
180
- - Define schema and indexes in `convex/schema.ts`; query with indexed fields
181
- - Limit list reads (e.g. `.take(n)` / bounded queries); avoid unbounded scans
182
- - Use Convex argument validators on every function; keep Zod at the HTTP boundary
183
- - Use `Promise.allSettled()` for concurrent operations that can fail independently
184
- - Use `pLimit` for controlled concurrency against external APIs
185
-
186
- ## 8. Logging
187
-
188
- - Use `console.error()` for errors with context (userId, error message, stack)
189
- - Use `console.log()` sparingly for important business events
190
- - Never log sensitive data (passwords, tokens, PII)
191
- - Logs are auto-captured by Vercel
192
-
193
- ## 9. Testing
194
-
195
- - Test files in `__tests__/` within API route folders
196
- - Mock StackAuth server user resolution and Convex `fetch*` helpers when testing handlers in isolation
197
- - Mock `request.json()` for body parsing
198
- - Use `{} as NextRequest` if handler doesn't use request object
@@ -1,77 +0,0 @@
1
- ---
2
- name: api-timing-logs
3
- description: Adds readable one-line phase timing logs for API routes and server handlers (local debugging and production correlation). Use when instrumenting slow routes, debugging latency, adding request tracing, or when the user asks for timing logs, speed stats, or structured server logging.
4
- ---
5
-
6
- # API phase timing logs (gold standard)
7
-
8
- ## Goals
9
-
10
- - **Scannable in a terminal**: one line per phase, aligned columns, no nested objects on the happy path.
11
- - **Grep-friendly**: fixed bracket tag per feature (e.g. `[ck-gen]`, `[mail-send]`).
12
- - **Minimal PII**: log `user=…` **once** at start; repeat **only** on error lines that need correlation.
13
-
14
- ## Log line shape
15
-
16
- ```
17
- [TAG] begin user=<full-user-id>
18
- [TAG] <step> <ms>ms <optional key=value ...>
19
- ```
20
-
21
- - **`TAG`**: short, stable, unique within the codebase (2–8 chars after the bracket is enough if unambiguous).
22
- - **`step`**: lowercase, use **dots** for sub-phases (`db.company_row`, `storage.sign`, `llm`). Pad `step` to a fixed width (e.g. 18 chars) so `ms` columns line up.
23
- - **`ms`**: wall time for **that phase only** (`Date.now() - phaseStart`), not cumulative unless the step name says `total`.
24
- - **Detail tail**: space-separated `key=value`. Prefer counts and compact units over raw dumps.
25
-
26
- ## Helpers (TypeScript pattern)
27
-
28
- Copy/adapt per route; keep helpers **file-local** unless three+ routes need the same tag.
29
-
30
- ```ts
31
- const TAG = "[feature-tag]";
32
-
33
- function elapsedMs(start: number) {
34
- return Date.now() - start;
35
- }
36
-
37
- function phaseLog(step: string, ms: number, detail?: string) {
38
- const tail = detail ? ` ${detail}` : "";
39
- console.info(`${TAG} ${step.padEnd(18)} ${String(ms).padStart(5)}ms${tail}`);
40
- }
41
-
42
- function phaseWarn(bucket: string, detail: string) {
43
- console.warn(`${TAG} ${bucket} ${detail}`);
44
- }
45
-
46
- function phaseErr(step: string, userId: string, detail: string) {
47
- console.error(`${TAG} ${step} user=${userId} ${detail}`);
48
- }
49
- ```
50
-
51
- After auth (or once `userId` is known): `console.info(\`${TAG} begin user=${userId}\`);`then use`phaseLog`for every subsequent phase **without** repeating`userId`.
52
-
53
- ## What to log per phase
54
-
55
- - **External I/O**: hub pull, DB reads/writes, storage sign/download, HTTP fetches, LLM calls—each gets its own line with duration.
56
- - **CPU-ish work** only if it can be large (parse/format of huge payloads); otherwise merge with the phase that produced the bytes.
57
- - **Final line**: `done` or `total` with end-to-end `ms` plus 1–3 summary stats (`docs=`, `llmIn=34500ch`, etc.).
58
-
59
- ## Units and compaction
60
-
61
- - Prefer **`kch`** (thousands of **characters**) when approximating payload size from string length. Do **not** label char counts as `kB` (misleading).
62
- - Rough token hints are OK: `~8676tok` with a comment in code that it is approximate (e.g. chars/4).
63
- - For multiple similar items, one compact tail beats an array of objects:
64
- `ok=3/3 raw~1200kch [a.json:400kch@800ms, b.pdf:…]`
65
- - Cap list length in logs (e.g. first 3 files + `+2 more`) if noise gets large.
66
-
67
- ## Errors
68
-
69
- - Use **`phaseErr`** for failures where you need to find the user in logs: include `user=<id>` and a **short** reason (message string, not full stack objects).
70
- - Keep generic `console.error` for unexpected exceptions if you already logged `begin` with `user=`.
71
-
72
- ## Anti-patterns
73
-
74
- - Repeating `userId` on every `info` line.
75
- - Dumping **full prompts**, document bodies, or tokens in logs.
76
- - Large **JSON blobs** for routine success paths—use one line + counts.
77
- - Inconsistent tags (e.g. mixing `console.info("feature: …")` and `[tag] …` styles) in the same route.