start-vibing-stacks 2.2.0 → 2.4.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.
@@ -114,6 +114,104 @@ export async function generateMetadata({ params }: { params: { id: string } }) {
114
114
  }
115
115
  ```
116
116
 
117
+ ## Environment Variables & API Security (MANDATORY)
118
+
119
+ > **NEXT_PUBLIC_ vars are embedded in the browser JS bundle.** Anyone can see them in DevTools.
120
+
121
+ ### Server vs Client Environment
122
+
123
+ | Prefix | Accessible from | Safe for |
124
+ |--------|----------------|----------|
125
+ | No prefix | Server Components, Route Handlers, Server Actions | API keys, secrets, tokens, DB URLs |
126
+ | `NEXT_PUBLIC_*` | Server + Browser (embedded in JS) | Public URLs, analytics IDs, publishable keys |
127
+
128
+ ### FORBIDDEN Environment Patterns
129
+
130
+ ```bash
131
+ # NEVER DO THIS — secret exposed in browser bundle
132
+ NEXT_PUBLIC_OPENAI_KEY=sk-abc123
133
+ NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_abc
134
+ NEXT_PUBLIC_DATABASE_URL=postgresql://user:pass@host/db
135
+
136
+ # CORRECT — server-only (no NEXT_PUBLIC_ prefix)
137
+ OPENAI_KEY=sk-abc123
138
+ STRIPE_SECRET_KEY=sk_live_abc
139
+ DATABASE_URL=postgresql://user:pass@host/db
140
+
141
+ # OK as NEXT_PUBLIC_ — no secret value
142
+ NEXT_PUBLIC_APP_URL=https://myapp.com
143
+ NEXT_PUBLIC_STRIPE_KEY=pk_live_abc
144
+ NEXT_PUBLIC_GA_ID=G-XXXXX
145
+ ```
146
+
147
+ ### API Proxy Pattern (MANDATORY)
148
+
149
+ External API calls with secrets MUST go through server-side Route Handlers:
150
+
151
+ ```tsx
152
+ // app/api/ai/route.ts — Secret stays on server
153
+ import { NextRequest, NextResponse } from 'next/server';
154
+
155
+ export async function POST(req: NextRequest) {
156
+ const { prompt } = await req.json();
157
+
158
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
159
+ method: 'POST',
160
+ headers: {
161
+ 'Content-Type': 'application/json',
162
+ Authorization: `Bearer ${process.env['OPENAI_KEY']}`,
163
+ },
164
+ body: JSON.stringify({
165
+ model: 'gpt-4',
166
+ messages: [{ role: 'user', content: prompt }],
167
+ }),
168
+ });
169
+
170
+ if (!response.ok) {
171
+ return NextResponse.json({ error: 'AI request failed' }, { status: 502 });
172
+ }
173
+
174
+ return NextResponse.json(await response.json());
175
+ }
176
+ ```
177
+
178
+ ```tsx
179
+ // components/chat.tsx — Client calls YOUR route, not external API
180
+ 'use client';
181
+
182
+ async function sendMessage(prompt: string) {
183
+ const res = await fetch('/api/ai', {
184
+ method: 'POST',
185
+ headers: { 'Content-Type': 'application/json' },
186
+ body: JSON.stringify({ prompt }),
187
+ });
188
+ return res.json();
189
+ }
190
+ ```
191
+
192
+ ### Server Actions for Mutations
193
+
194
+ Server Actions also keep secrets server-side:
195
+
196
+ ```tsx
197
+ // app/actions/payment.ts
198
+ 'use server';
199
+
200
+ import Stripe from 'stripe';
201
+
202
+ const stripe = new Stripe(process.env['STRIPE_SECRET_KEY']!);
203
+
204
+ export async function createCheckout(priceId: string) {
205
+ const session = await stripe.checkout.sessions.create({
206
+ mode: 'payment',
207
+ line_items: [{ price: priceId, quantity: 1 }],
208
+ success_url: `${process.env['APP_URL']}/success`,
209
+ cancel_url: `${process.env['APP_URL']}/cancel`,
210
+ });
211
+ return { url: session.url };
212
+ }
213
+ ```
214
+
117
215
  ## FORBIDDEN
118
216
 
119
217
  1. **`'use client'` on server-capable components** — default to server
@@ -121,3 +219,6 @@ export async function generateMetadata({ params }: { params: { id: string } }) {
121
219
  3. **`getServerSideProps` / `getStaticProps`** — App Router uses async components
122
220
  4. **API routes for server-only data** — use server components directly
123
221
  5. **Prop drilling through layouts** — use parallel routes or context
222
+ 6. **`NEXT_PUBLIC_` with API keys, secrets, or tokens** — secrets leak to browser bundle
223
+ 7. **Calling external APIs from client components** — use Route Handlers as proxy
224
+ 8. **`process.env['SECRET']` in `'use client'` files** — only `NEXT_PUBLIC_*` vars work client-side
@@ -1,29 +1,13 @@
1
1
  {
2
2
  "id": "nodejs",
3
3
  "name": "Node.js / TypeScript",
4
- "icon": "\ud83d\udce6",
4
+ "icon": "📦",
5
5
  "runtime": "Bun / Node.js 20+",
6
6
  "minVersion": "20.0.0",
7
7
  "packageManager": "bun|npm|pnpm",
8
- "extensions": [
9
- ".ts",
10
- ".tsx",
11
- ".js",
12
- ".jsx",
13
- ".mjs",
14
- ".cjs"
15
- ],
16
- "testExtensions": [
17
- "*.test.ts",
18
- "*.spec.ts",
19
- "*.test.tsx"
20
- ],
21
- "detectFiles": [
22
- "package.json",
23
- "tsconfig.json",
24
- "bun.lockb",
25
- "next.config.js"
26
- ],
8
+ "extensions": [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
9
+ "testExtensions": ["*.test.ts", "*.spec.ts", "*.test.tsx"],
10
+ "detectFiles": ["package.json", "tsconfig.json", "bun.lockb", "bun.lock", "next.config.js"],
27
11
  "commands": {
28
12
  "test": "bun run test",
29
13
  "lint": "bun run lint",
@@ -33,146 +17,95 @@
33
17
  "typecheck": "bun run typecheck"
34
18
  },
35
19
  "qualityGates": [
36
- {
37
- "name": "TypeCheck",
38
- "command": "bun run typecheck",
39
- "required": true,
40
- "order": 1
41
- },
42
- {
43
- "name": "Lint",
44
- "command": "bun run lint",
45
- "required": true,
46
- "order": 2
47
- },
48
- {
49
- "name": "Tests",
50
- "command": "bun run test",
51
- "required": true,
52
- "order": 3
53
- },
54
- {
55
- "name": "Build",
56
- "command": "bun run build",
57
- "required": true,
58
- "order": 4
59
- }
20
+ { "name": "TypeCheck", "command": "bun run typecheck", "required": true, "order": 1 },
21
+ { "name": "Lint", "command": "bun run lint", "required": true, "order": 2 },
22
+ { "name": "Tests", "command": "bun run test", "required": true, "order": 3 },
23
+ { "name": "Build", "command": "bun run build", "required": true, "order": 4 }
60
24
  ],
61
25
  "frameworks": [
62
26
  {
63
27
  "id": "nextjs",
64
28
  "name": "Next.js (App Router)",
65
- "icon": "\u25b2",
66
- "detectFiles": [
67
- "next.config.js",
68
- "next.config.ts",
69
- "next.config.mjs"
70
- ]
29
+ "icon": "",
30
+ "detectFiles": ["next.config.js", "next.config.ts", "next.config.mjs"],
31
+ "default": true,
32
+ "skills": ["nextjs-app-router"]
71
33
  },
72
34
  {
73
35
  "id": "nuxt",
74
36
  "name": "Nuxt",
75
- "icon": "\ud83d\udc9a",
76
- "detectFiles": [
77
- "nuxt.config.ts"
78
- ]
37
+ "icon": "💚",
38
+ "detectFiles": ["nuxt.config.ts"],
39
+ "skills": []
79
40
  },
80
41
  {
81
42
  "id": "astro",
82
43
  "name": "Astro",
83
- "icon": "\ud83d\ude80",
84
- "detectFiles": [
85
- "astro.config.mjs"
86
- ]
44
+ "icon": "🚀",
45
+ "detectFiles": ["astro.config.mjs"],
46
+ "skills": []
87
47
  },
88
48
  {
89
49
  "id": "express",
90
50
  "name": "Express",
91
- "icon": "\u26a1"
51
+ "icon": "",
52
+ "skills": ["trpc-api"]
92
53
  },
93
54
  {
94
55
  "id": "fastify",
95
56
  "name": "Fastify",
96
- "icon": "\ud83c\udfce\ufe0f"
57
+ "icon": "🏎️",
58
+ "skills": ["trpc-api"]
97
59
  },
98
60
  {
99
61
  "id": "vanilla",
100
62
  "name": "Vanilla Node.js",
101
- "icon": "\ud83d\udcc4"
63
+ "icon": "📄",
64
+ "skills": []
102
65
  }
103
66
  ],
104
67
  "databases": [
105
- {
106
- "id": "mongodb",
107
- "name": "MongoDB",
108
- "icon": "\ud83c\udf43"
109
- },
110
- {
111
- "id": "postgresql",
112
- "name": "PostgreSQL",
113
- "icon": "\ud83d\udc18"
114
- },
115
- {
116
- "id": "mysql",
117
- "name": "MySQL / MariaDB",
118
- "icon": "\ud83d\udc2c"
119
- },
120
- {
121
- "id": "sqlite",
122
- "name": "SQLite (Turso / libSQL)",
123
- "icon": "\ud83d\udcc1"
124
- },
125
- {
126
- "id": "redis",
127
- "name": "Redis (Upstash)",
128
- "icon": "\ud83d\udd34"
129
- },
130
- {
131
- "id": "none",
132
- "name": "None",
133
- "icon": "\u274c"
134
- }
68
+ { "id": "mongodb", "name": "MongoDB", "icon": "🍃" },
69
+ { "id": "postgresql", "name": "PostgreSQL", "icon": "🐘" },
70
+ { "id": "mysql", "name": "MySQL / MariaDB", "icon": "🐬" },
71
+ { "id": "sqlite", "name": "SQLite (Turso / libSQL)", "icon": "📁" },
72
+ { "id": "redis", "name": "Redis (Upstash)", "icon": "🔴" },
73
+ { "id": "none", "name": "None", "icon": "❌" }
135
74
  ],
136
75
  "frontendOptions": [
137
76
  {
138
77
  "id": "react-tailwind",
139
78
  "name": "React 19+ / TailwindCSS 4+",
140
- "icon": "\u269b\ufe0f"
79
+ "icon": "⚛️",
80
+ "skillsDir": "react",
81
+ "default": true,
82
+ "frameworks": ["nextjs", "express", "fastify", "vanilla"]
141
83
  },
142
84
  {
143
85
  "id": "vue",
144
86
  "name": "Vue.js / Nuxt",
145
- "icon": "\ud83d\udc9a"
87
+ "icon": "💚",
88
+ "frameworks": ["nuxt"]
146
89
  },
147
90
  {
148
91
  "id": "svelte",
149
92
  "name": "Svelte / SvelteKit",
150
- "icon": "\ud83d\udd25"
151
- },
152
- {
153
- "id": "shadcn",
154
- "name": "shadcn/ui + Tailwind",
155
- "icon": "\ud83c\udfa8"
93
+ "icon": "🔥",
94
+ "frameworks": ["astro", "vanilla"]
156
95
  },
157
96
  {
158
97
  "id": "none",
159
- "name": "API only \u2014 no frontend",
160
- "icon": "\u274c"
98
+ "name": "API only no frontend",
99
+ "icon": ""
161
100
  }
162
101
  ],
163
102
  "deployTargets": [
164
- {
165
- "id": "github",
166
- "name": "GitHub (git push)",
167
- "icon": "\ud83d\udc19"
168
- }
103
+ { "id": "github", "name": "GitHub (git push)", "icon": "🐙" }
169
104
  ],
170
105
  "skills": [
171
106
  "typescript-strict",
172
- "react-patterns",
173
- "nextjs-app-router",
174
- "zod-validation",
175
- "vitest-testing"
107
+ "bun-runtime",
108
+ "zod-validation"
176
109
  ],
177
110
  "requirements": [
178
111
  {
@@ -185,17 +118,6 @@
185
118
  "linux": "curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt install -y nodejs"
186
119
  },
187
120
  "versionRegex": "v?(\\d+\\.\\d+\\.\\d+)"
188
- },
189
- {
190
- "name": "Bun",
191
- "command": "bun",
192
- "versionFlag": "--version",
193
- "minVersion": "1.0.0",
194
- "installCommand": {
195
- "macos": "brew install oven-sh/bun/bun",
196
- "linux": "curl -fsSL https://bun.sh/install | bash"
197
- },
198
- "versionRegex": "(\\d+\\.\\d+\\.\\d+)"
199
121
  }
200
122
  ]
201
123
  }
@@ -1,92 +1,170 @@
1
1
  # Laravel Octane (RoadRunner) Patterns
2
2
 
3
- ## Critical Rules
3
+ ## How Octane Works
4
4
 
5
- Octane runs your app in a **long-lived process** different from traditional PHP.
5
+ Octane runs your app in a **long-lived worker process**. The application boots ONCE and handles many requests without restarting. This is fundamentally different from traditional PHP where each request boots a fresh process.
6
6
 
7
- ### DO
7
+ **Consequence:** Any state stored in static properties, globals, or singletons persists across requests and can leak between users.
8
8
 
9
- ```php
10
- // Dependency Injection over resolve()
11
- public function __construct(
12
- private readonly UserService $userService,
13
- ) {}
9
+ ## Critical Rules
14
10
 
15
- // Use Request object
16
- public function store(Request $request): JsonResponse
11
+ ### DO: Dependency Injection
12
+
13
+ ```php
14
+ // CORRECT: Constructor injection (resolved fresh per request scope)
15
+ class OrderController extends Controller
17
16
  {
18
- $name = $request->input('name');
17
+ public function __construct(
18
+ private readonly OrderService $orderService,
19
+ private readonly CacheManager $cache,
20
+ ) {}
19
21
  }
20
22
 
21
- // Persistent DB connections with flush
22
- // config/database.php
23
- 'options' => [PDO::ATTR_PERSISTENT => true]
24
- // Octane::prepare to flush connections between requests
23
+ // CORRECT: Method injection in controller actions
24
+ public function store(StoreOrderRequest $request, PaymentGateway $gateway): JsonResponse
25
+ {
26
+ $result = $gateway->charge($request->validated());
27
+ return response()->json($result, 201);
28
+ }
25
29
  ```
26
30
 
27
- ### NEVER
31
+ ### DO: Use the Request Object
28
32
 
29
33
  ```php
30
- // Static state (leaks between requests!)
31
- class Service {
32
- private static array $cache = []; // ❌ LEAKS
34
+ public function store(Request $request): JsonResponse
35
+ {
36
+ $name = $request->input('name');
37
+ $token = $request->bearerToken();
38
+ $ip = $request->ip();
39
+ $user = $request->user();
33
40
  }
34
-
35
- // Global variables
36
- global $user; // ❌
37
-
38
- // Modifying config at runtime
39
- config(['app.name' => 'New']); // ❌ Affects ALL requests
40
-
41
- // die() or exit()
42
- die('error'); // ❌ Kills the worker
43
-
44
- // Superglobals
45
- $_GET['id']; // ❌ Stale between requests
46
- $_POST['name']; // ❌
47
- $_SESSION['user']; // ❌
48
41
  ```
49
42
 
50
- ### AppServiceProvider Connection Flush
43
+ ### DO: Persistent DB Connections with Flush
51
44
 
52
45
  ```php
53
- // app/Providers/AppServiceProvider.php
54
- use Laravel\Octane\Facades\Octane;
55
-
56
- public function boot(): void
57
- {
58
- // MANDATORY: flush DB connections between requests
59
- Octane::prepare(fn ($sandbox) => $sandbox->flushDatabaseConnections());
60
- }
46
+ // config/database.php
47
+ 'mysql' => [
48
+ 'options' => [
49
+ PDO::ATTR_PERSISTENT => true,
50
+ ],
51
+ ],
52
+
53
+ // Octane automatically flushes connections between requests
54
+ // via Octane::prepare() — no manual work needed
61
55
  ```
62
56
 
63
- **Rule:** ALWAYS pair persistent connections with `Octane::prepare` flush in AppServiceProvider.
64
-
65
- ### Memoization in Workers
57
+ ### DO: Instance-scoped Memoization
66
58
 
67
59
  ```php
68
60
  // ✅ Scoped to instance, cleared per request
69
61
  class ProcessLeadsJob implements ShouldQueue
70
62
  {
71
63
  private array $lookupCache = [];
72
-
64
+
73
65
  public function handle(): void
74
66
  {
75
67
  foreach ($this->leads as $lead) {
76
- $result = $this->lookupCache[$lead->key]
68
+ $result = $this->lookupCache[$lead->key]
77
69
  ??= $this->expensiveLookup($lead->key);
78
70
  }
79
- // Cache is GC'd when job instance is destroyed
71
+ // Cache is garbage collected when job instance is destroyed
80
72
  }
81
73
  }
74
+ ```
75
+
76
+ ## NEVER Do These in Octane
77
+
78
+ ### No Static State
79
+
80
+ ```php
81
+ // WRONG: Static properties persist across ALL requests
82
+ class AuthService {
83
+ private static ?User $currentUser = null; // LEAKS between users!
84
+ private static array $cache = []; // Grows forever!
85
+ }
82
86
 
83
- // Static cache (persists across jobs in Octane!)
84
- private static array $cache = [];
87
+ // CORRECT: Instance properties (scoped to request)
88
+ class AuthService {
89
+ private ?User $currentUser = null;
90
+ private array $cache = [];
91
+ }
92
+ ```
93
+
94
+ ### No Global Variables
95
+
96
+ ```php
97
+ // WRONG
98
+ global $config;
99
+ $GLOBALS['user'] = $user;
100
+
101
+ // CORRECT: Use DI or config()
102
+ $value = config('app.name');
103
+ ```
104
+
105
+ ### No Runtime Config Mutation
106
+
107
+ ```php
108
+ // WRONG: Affects ALL concurrent requests in the worker
109
+ config(['app.timezone' => 'America/Sao_Paulo']);
110
+ config(['services.stripe.key' => $newKey]);
111
+
112
+ // CORRECT: Pass values explicitly
113
+ $this->service->processWithTimezone('America/Sao_Paulo');
114
+ ```
115
+
116
+ ### No Process Termination
117
+
118
+ ```php
119
+ // WRONG: Kills the entire worker process
120
+ die('Something went wrong');
121
+ exit(1);
122
+ dd($variable);
123
+
124
+ // CORRECT: Throw exceptions (handled by Laravel)
125
+ throw new RuntimeException('Something went wrong');
126
+ abort(500, 'Internal error');
127
+
128
+ // For debugging: use dump() without die
129
+ dump($variable); // Outputs without killing worker
130
+ Log::debug('Debug info', ['var' => $variable]);
131
+ ```
132
+
133
+ ### No Superglobals
134
+
135
+ ```php
136
+ // WRONG: Stale data from previous requests
137
+ $_GET['id'];
138
+ $_POST['name'];
139
+ $_SESSION['user'];
140
+ $_SERVER['HTTP_AUTHORIZATION'];
141
+ $_COOKIE['token'];
142
+
143
+ // CORRECT: Use Request object
144
+ $request->input('id');
145
+ $request->post('name');
146
+ $request->session()->get('user');
147
+ $request->header('Authorization');
148
+ $request->cookie('token');
149
+ ```
150
+
151
+ ### No Singleton Abuse
152
+
153
+ ```php
154
+ // WRONG: Singleton state leaks
155
+ app()->singleton('cart', fn () => new Cart());
156
+ // The same Cart instance serves ALL users!
157
+
158
+ // CORRECT: Use scoped bindings
159
+ app()->scoped('cart', fn () => new Cart());
160
+ // Fresh instance per request, cleared between requests
85
161
  ```
86
162
 
87
163
  ## RoadRunner Configuration (rr.yaml)
88
164
 
89
165
  ```yaml
166
+ version: '3'
167
+
90
168
  server:
91
169
  command: "php artisan octane:start --server=roadrunner --host=0.0.0.0 --port=8000"
92
170
 
@@ -94,17 +172,41 @@ http:
94
172
  address: 0.0.0.0:8000
95
173
  pool:
96
174
  num_workers: 4
97
- max_jobs: 500 # Restart worker after 500 requests (memory safety)
175
+ max_jobs: 500
98
176
  supervisor:
99
- max_worker_memory: 128 # MB
177
+ max_worker_memory: 128
100
178
 
101
179
  logs:
102
180
  level: info
181
+ encoding: json
103
182
  ```
104
183
 
184
+ ### Worker Tuning
185
+
186
+ | Setting | Default | Description |
187
+ |---------|---------|-------------|
188
+ | `num_workers` | CPU cores | Number of worker processes |
189
+ | `max_jobs` | 500 | Restart worker after N requests (memory safety) |
190
+ | `max_worker_memory` | 128 MB | Kill worker if exceeds memory |
191
+
192
+ **Rule:** Always set `max_jobs` to prevent memory leaks from accumulating.
193
+
105
194
  ## Database Safety
106
195
 
107
196
  - **NEVER** execute `db:wipe`, `migrate:fresh`, `migrate:refresh`, `db:reset`
108
197
  - **ALWAYS** use incremental migrations (`make:migration`)
109
- - If migration fails fix the file or create a new one
198
+ - If migration fails, fix the file or create a new one
110
199
  - Assume **all environments contain critical data**
200
+
201
+ ## Octane Checklist
202
+
203
+ - [ ] No `static` properties on service classes
204
+ - [ ] No global variables or `$GLOBALS`
205
+ - [ ] No `config()` mutations at runtime
206
+ - [ ] No `die()`, `exit()`, or `dd()` in production code
207
+ - [ ] No superglobals (`$_GET`, `$_POST`, `$_SESSION`, `$_SERVER`)
208
+ - [ ] No `app()->singleton()` for request-scoped data (use `scoped`)
209
+ - [ ] All services use constructor DI (not `app()` or `resolve()`)
210
+ - [ ] Memoization scoped to instance, not static
211
+ - [ ] `max_jobs` configured in `rr.yaml` for memory safety
212
+ - [ ] Persistent DB connections enabled with Octane flush