start-vibing-stacks 2.8.0 → 2.10.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.
@@ -6,11 +6,14 @@
6
6
 
7
7
  **Branch:** main
8
8
  **Date:** {{DATE}}
9
- **Summary:** Initial project setup with start-vibing-stacks (PHP)
9
+ **Summary:** Initial project setup with start-vibing-stacks (PHP — API-first React SPA)
10
10
 
11
11
  ## 30 Seconds Overview
12
12
 
13
- {{PROJECT_NAME}} is a PHP 8.3+ project using {{FRAMEWORK}}.
13
+ {{PROJECT_NAME}} is a PHP 8.3+ project using {{FRAMEWORK}}, serving a JSON API
14
+ consumed by a React 19 + Vite SPA via Axios. Authentication uses Laravel
15
+ Sanctum SPA (HttpOnly session cookie + CSRF). Pages render shell + skeleton
16
+ instantly; data is fetched async via the `api` Axios instance.
14
17
 
15
18
  ## Stack
16
19
 
@@ -19,65 +22,117 @@
19
22
  | Language | PHP >= 8.3 |
20
23
  | Framework | {{FRAMEWORK}} |
21
24
  | Server | Octane + RoadRunner |
22
- | Frontend | ReactJS 19+ / Inertia.js / TailwindCSS 4+ |
25
+ | Auth | Sanctum SPA (HttpOnly cookie + CSRF) |
26
+ | API contract | JSON Resource + paginate (data + meta + links) |
27
+ | Frontend | React 19 + Vite + TailwindCSS 4 + React Router |
28
+ | HTTP client | Axios (`withCredentials`, `withXSRFToken`, interceptors) |
29
+ | Server cache | TanStack Query (recommended) |
23
30
  | Database | {{DATABASE}} |
24
- | Package Manager | Composer |
31
+ | Package Manager | Composer / npm (or pnpm/bun) |
25
32
  | Static Analysis | PHPStan (level 6) |
26
- | Testing | PHPUnit |
33
+ | Testing | PHPUnit / Pest |
27
34
  | Code Style | PHP-CS-Fixer (PSR-12) |
28
35
 
29
- ## Architecture
36
+ ## Architecture (API-first — DEFAULT)
37
+
38
+ ```
39
+ Browser (React Vite SPA) Laravel 12 + Octane (RoadRunner)
40
+ ───────────────────────── ───────────────────────────────────────
41
+ GET / → routes/web.php catch-all → app.blade (Vite shell)
42
+ GET /sanctum/csrf-cookie → XSRF-TOKEN cookie set
43
+ POST /login → AuthController → session cookie set
44
+ GET /api/orders → Route → Controller → FormRequest (rules + Policy)
45
+ → Service (DB / business)
46
+ → Resource (JSON whitelist)
47
+ → JsonResponse
48
+ ↓ async via Axios api instance
49
+ React Page renders shell + skeleton instantly; data arrives → renders rows.
50
+ ```
51
+
52
+ ### Pipeline — One Responsibility per Layer
53
+
54
+ | Layer | File | Responsibility |
55
+ |-------|------|------------------|
56
+ | React Page | `resources/js/Pages/*/Index.jsx` | Render shell + skeleton; `api.get(...)` |
57
+ | Axios | `resources/js/lib/api.js` | `withCredentials`, CSRF, 401/403/419/422/5xx interceptors |
58
+ | Route | `routes/api.php` | URL ↔ Controller; group by `auth:sanctum` + `throttle` |
59
+ | Controller | `app/Http/Controllers/Api/*Controller.php` | Receive Request, call Service, return Resource |
60
+ | FormRequest | `app/Http/Requests/*/...Request.php` | `rules()` validate + `authorize()` → Policy |
61
+ | Policy | `app/Policies/*Policy.php` | `before()` super-admin bypass; per-action checks |
62
+ | Service | `app/Services/*Service.php` | Business logic, transactions, scoping |
63
+ | Resource | `app/Http/Resources/*Resource.php` | Eloquent → safe JSON; whitelist fields |
30
64
 
31
65
  ```
32
66
  project/
33
67
  ├── app/
34
- │ ├── Console/ # Artisan commands
35
- │ ├── Exceptions/ # Exception handlers
68
+ │ ├── Console/ # Artisan commands
69
+ │ ├── Exceptions/ # Exception handlers
36
70
  │ ├── Http/
37
- │ │ ├── Controllers/ # Thin controllers (Inertia::render + Services)
38
- │ │ ├── Middleware/ # HTTP middleware (HandleInertiaRequests)
39
- │ │ └── Requests/ # Form request validation
40
- │ ├── Models/ # Eloquent models (UUIDs, $fillable)
41
- ├── Providers/ # Service providers
42
- │ ├── Services/ # Business logic (single responsibility)
43
- │ │ └── Helpers/ # Extracted logic for complex services
44
- ├── Support/ # Helper classes (InertiaShare)
45
- │ ├── Traits/ # Shared traits (Loggable, FormatsDatesForApi)
46
- └── Jobs/ # Queue jobs (idempotent, chunked)
47
- ├── bootstrap/ # Framework bootstrap
48
- ├── config/ # Configuration (immutable at runtime)
49
- └── translations_inertia.php # Per-page translation mapping
71
+ │ │ ├── Controllers/
72
+ │ │ │ └── Api/ # JSON-only controllers (DEFAULT)
73
+ │ │ ├── Middleware/ # SecurityHeaders, RequestId, throttle, ...
74
+ ├── Requests/ # FormRequests grouped by resource
75
+ │ │ └── Order/
76
+ │ │ ├── IndexOrderRequest.php
77
+ │ │ └── StoreOrderRequest.php
78
+ │ └── Resources/ # JsonResource classes (date trait, whitelist)
79
+ │ ├── Models/ # Eloquent models (UUIDs, $fillable)
80
+ ├── Policies/ # OrderPolicy, UserPolicy, ... (before() bypass)
81
+ ├── Providers/ # AppServiceProvider (RateLimiter, Octane prepare)
82
+ ├── Services/ # Business logic (single responsibility)
83
+ │ ├── External/ # 3rd-party API services (with DTOs)
84
+ │ │ └── Helpers/ # Extracted logic for complex services
85
+ │ ├── Traits/ # FormatsDatesForApi, Auditable, ApiResponse
86
+ │ └── Jobs/ # Idempotent queue jobs
87
+ ├── bootstrap/
88
+ │ └── app.php # ->statefulApi() + middleware aliases
89
+ ├── config/
90
+ │ ├── sanctum.php # SANCTUM_STATEFUL_DOMAINS
91
+ │ ├── cors.php # supports_credentials=true, specific origin
92
+ │ ├── session.php # http_only, secure, same_site=lax
93
+ │ └── services.php # bridge env() ↔ application
50
94
  ├── database/
51
- │ ├── factories/ # Model factories
52
- │ ├── migrations/ # Incremental migrations ONLY
53
- │ └── seeders/ # Database seeders
95
+ │ ├── factories/
96
+ │ ├── migrations/ # incremental ONLY
97
+ │ └── seeders/
54
98
  ├── lang/
55
- │ ├── en/ # English translations
56
- │ └── pt/ # Portuguese translations
57
- ├── public/ # Web root (index.php)
99
+ │ ├── en/ # PHP arrays — single source for all i18n
100
+ │ └── pt/
101
+ ├── public/
58
102
  ├── resources/
59
- │ ├── views/ # Blade root template (app.blade.php)
60
- │ ├── css/ # Stylesheets (TailwindCSS)
103
+ │ ├── css/ # TailwindCSS 4
104
+ │ ├── views/
105
+ │ │ └── app.blade.php # Single Vite shell (catch-all)
61
106
  │ └── js/
62
- │ ├── Pages/ # React page components (mapped by Inertia)
63
- │ ├── Components/ # Reusable React components
64
- │ ├── Layouts/ # Page layouts (Authenticated, Guest)
65
- │ ├── Icons/ # SVG icons (import with ?react)
66
- └── Utils/
67
- └── translate.js # __() translation helper
107
+ │ ├── app.jsx # createRoot + BrowserRouter + QueryClientProvider
108
+ │ ├── App.jsx # <Routes> + <ProtectedRoute>
109
+ │ ├── lib/
110
+ ├── api.js # Axios instance + interceptors
111
+ │ ├── auth.js # login / logout / fetchCurrentUser
112
+ └── queryClient.ts # TanStack Query
113
+ │ ├── store/ # Zustand / Context (auth, ui)
114
+ │ ├── Pages/ # Pages own routing client-side
115
+ │ │ └── Orders/
116
+ │ │ ├── Index.tsx # api.get('/api/orders') + skeleton
117
+ │ │ └── _components/
118
+ │ ├── Components/ # Reusable (ErrorState, EmptyState, ...)
119
+ │ ├── Layouts/ # AuthenticatedLayout, GuestLayout
120
+ │ └── Icons/ # SVG files imported with ?react
68
121
  ├── routes/
69
- │ ├── api.php # API routes (RESTful + action endpoints)
70
- └── web.php # Web routes (Inertia pages)
71
- ├── storage/ # Logs, cache, uploads
122
+ │ ├── api.php # auth:sanctum + throttle:api groups
123
+ ├── web.php # /login, /logout, catch-all → app.blade
124
+ │ └── console.php
125
+ ├── storage/
72
126
  ├── tests/
73
- │ ├── Unit/ # PHPUnit unit tests
74
- │ └── Feature/ # Feature/integration tests
75
- ├── .claude/ # AI agent configuration
76
- ├── artisan # CLI entry point
77
- ├── rr.yaml # RoadRunner config (Octane)
78
- ├── composer.json # Dependencies
79
- ├── phpstan.neon # Static analysis config
80
- └── CLAUDE.md # This file
127
+ │ ├── Unit/
128
+ │ └── Feature/ # API endpoints: happy + 401 + 403 + 422
129
+ ├── .claude/ # AI agent configuration
130
+ ├── artisan
131
+ ├── rr.yaml # RoadRunner / Octane
132
+ ├── vite.config.js # @vitejs/plugin-react + laravel-vite-plugin
133
+ ├── composer.json
134
+ ├── phpstan.neon
135
+ └── CLAUDE.md
81
136
  ```
82
137
 
83
138
  ## CLAUDE.md Update Rules
@@ -100,20 +155,59 @@ project/
100
155
 
101
156
  ## Critical Rules
102
157
 
158
+ ### PHP / Laravel
159
+
103
160
  - **PHP >= 8.3** — readonly, enums, typed constants, match expressions
104
161
  - **`declare(strict_types=1)`** in EVERY PHP file
105
162
  - **Octane-safe code** — no static state, no globals, no `die()`/`exit()`
106
163
  - **Dependency Injection** — use DI over `app()` or `resolve()`
107
- - **Thin controllers** — delegate business logic to Service classes
108
- - **Form Requests** — validate input in dedicated request classes
164
+ - **Thin controllers** — delegate business logic to Services
165
+ - **FormRequest + Policy** — every protected endpoint goes through both
166
+ - **API Resource** — every response wrapped in `JsonResource`
109
167
  - **Type everything** — properties, params, returns (no `mixed` without justification)
110
168
  - **UUIDs** — all new models use `HasUuids` trait as primary key
111
- - **Mass assignment** — always define `$fillable` on models
112
- - **Auditing** — critical models use `Loggable` trait
169
+ - **Mass assignment** — always define `$fillable`; never `$guarded = []`
113
170
  - **Eloquent only** — no raw SQL unless strictly justified with parameter binding
114
171
  - **Config immutable** — never use `config()` to SET values at runtime
115
172
  - **Request object** — use `$request->input()`, never `$_GET`/`$_POST`/`$_SESSION`
116
173
 
174
+ ### React / Frontend
175
+
176
+ - **Page contract** — render shell + skeleton on first paint; fetch via `api.get()`
177
+ - **NO `Inertia::render()`** for new endpoints — pure JSON API + Axios
178
+ - **`api` instance only** — never raw `axios` or `fetch` in components
179
+ - **422 binding** — render validation errors inline under fields (not toast)
180
+ - **LABELS + STYLES const** above the component (stable refs, no Hook abuse)
181
+ - **No tokens in `localStorage`** — Sanctum SPA cookie only
182
+ - **Skeletons match shape** of the loaded content (per-component)
183
+ - **Filters in URL** via `useSearchParams` — bookmarkable views
184
+
185
+ ### Sanctum SPA Auth (MANDATORY config)
186
+
187
+ ```php
188
+ // bootstrap/app.php
189
+ $middleware->statefulApi();
190
+
191
+ // config/cors.php
192
+ 'supports_credentials' => true,
193
+ 'allowed_origins' => [env('FRONTEND_URL')], // SPECIFIC origin
194
+
195
+ // config/session.php (production)
196
+ 'http_only' => true,
197
+ 'secure' => true, // HTTPS only
198
+ 'same_site' => 'lax', // 'none' only if cross-origin (also requires secure)
199
+
200
+ // .env
201
+ SANCTUM_STATEFUL_DOMAINS=app.example.com
202
+ SESSION_DOMAIN=.example.com // shared subdomains
203
+ ```
204
+
205
+ ```js
206
+ // resources/js/lib/api.js (Axios)
207
+ axios.defaults.withCredentials = true;
208
+ axios.defaults.withXSRFToken = true;
209
+ ```
210
+
117
211
  ### Environment Variables & Secrets (MANDATORY)
118
212
 
119
213
  > **NEVER use `env()` outside config files.** After `config:cache`, `env()` returns null everywhere except config files.
@@ -122,17 +216,14 @@ project/
122
216
  |----------|--------|----------|
123
217
  | `.env` | `env()` inside `config/*.php` only | API keys, DB credentials, secrets |
124
218
  | `config/*.php` | `config('services.stripe.key')` | Application code access |
125
- | Frontend (Inertia) | `InertiaShare` or controller props | Public data ONLY |
219
+ | Frontend (`VITE_*`) | `import.meta.env.VITE_API_URL` | PUBLIC values ONLY (bundled) |
126
220
 
127
221
  ```php
128
222
  // config/services.php — Bridge between .env and application
129
223
  return [
130
- 'openai' => [
131
- 'key' => env('OPENAI_KEY'),
132
- ],
133
224
  'stripe' => [
134
- 'key' => env('STRIPE_KEY'), // Publishable (sent to frontend)
135
- 'secret' => env('STRIPE_SECRET'), // NEVER send to frontend
225
+ 'key' => env('STRIPE_KEY'), // publishable (public ok)
226
+ 'secret' => env('STRIPE_SECRET'), // server-side ONLY
136
227
  'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
137
228
  ],
138
229
  ];
@@ -140,76 +231,84 @@ return [
140
231
  // In code: ALWAYS use config()
141
232
  $apiKey = config('services.openai.key');
142
233
 
143
- // FORBIDDEN:
144
- $apiKey = env('OPENAI_KEY'); // Returns null when config is cached!
234
+ // FORBIDDEN
235
+ $apiKey = env('OPENAI_KEY'); // returns null when config is cached
145
236
  ```
146
237
 
147
238
  ### Frontend Secret Isolation (MANDATORY)
148
239
 
149
- > **NEVER send API keys, secrets, or tokens to the frontend via Inertia props or JavaScript.**
240
+ > **NEVER send API secrets to the frontend bundle.** Anything in `VITE_*` or
241
+ > in JSON returned to the browser is PUBLIC.
150
242
 
151
- ```php
152
- // WRONG — secret exposed in page source / DevTools
153
- return Inertia::render('Dashboard', [
154
- 'stripeSecret' => config('services.stripe.secret'),
155
- 'openaiKey' => config('services.openai.key'),
156
- ]);
157
-
158
- // CORRECT only public/publishable data to frontend
159
- return Inertia::render('Dashboard', [
160
- 'stripePublicKey' => config('services.stripe.key'), // pk_ only
161
- ]);
162
-
163
- // For operations requiring secrets: use backend API routes
164
- // Frontend calls /api/payment → backend uses secret server-side
243
+ ```js
244
+ // FORBIDDEN — secret embedded in bundle
245
+ const stripeSecret = import.meta.env.VITE_STRIPE_SECRET;
246
+
247
+ // CORRECT — only publishable keys cross the wire
248
+ const stripePublishable = import.meta.env.VITE_STRIPE_KEY; // pk_...
249
+
250
+ // For operations needing the secret: call your Laravel API
251
+ // Browser POST /api/payment → backend uses the SECRET server-side → JSON
165
252
  ```
166
253
 
167
254
  ## FORBIDDEN
168
255
 
169
- ### Security (CRITICAL)
256
+ ### Architecture (CRITICAL)
257
+
258
+ | Action | Reason |
259
+ |--------|--------|
260
+ | `Inertia::render()` for new endpoints | Blocks first paint on DB query — use API + Axios |
261
+ | Eloquent query inside a Controller | Move it to a Service |
262
+ | FormRequest `authorize(): true` blindly | Must call Policy or be index w/ Service scope |
263
+ | Resource performing DB queries | Use `whenLoaded()` and pre-load in Service |
264
+ | `axios.get(...)` directly in components | Bypasses interceptors — use `@/lib/api` |
265
+ | `localStorage.setItem('token', ...)` | XSS-readable — use HttpOnly Sanctum cookie |
266
+ | Page that blocks render on first fetch | Defeats the API-first model |
267
+
268
+ ### Security
170
269
 
171
270
  | Action | Reason |
172
271
  |--------|--------|
173
272
  | `env()` outside config files | Returns null when config is cached — use `config()` |
174
- | Send API keys/secrets via Inertia props | Exposed in page source — keep secrets server-side |
175
- | `$guarded = []` on models | Allows mass assignment of any field — use `$fillable` |
176
- | `DB::raw()` with user input | SQL injection — use Eloquent or parameterized queries |
273
+ | Send API secrets to the browser | Embedded in bundle — keep server-side |
274
+ | `$guarded = []` on models | Allows mass assignment — use `$fillable` |
275
+ | `DB::raw()` with user input | SQL injection — use Eloquent / parameterized |
177
276
  | `{!! $userInput !!}` | XSS — use `{{ }}` (auto-escaped) |
178
277
  | Dynamic code execution functions | Remote code execution risk |
179
278
  | `md5()` / `sha1()` for passwords | Weak hashing — use `Hash::make()` |
180
- | `unserialize()` on user data | Object injection — use JSON with model casts |
181
- | CORS `allowed_origins: ['*']` | Open APIrestrict to your domain |
279
+ | `unserialize()` on user data | Object injection — use JSON casts |
280
+ | `'allowed_origins' => ['*']` with credentials | Browser rejectslist specific origins |
182
281
  | `createToken('x', ['*'])` | Over-privileged — use specific abilities |
183
282
  | Trust `X-Forwarded-For` directly | Spoofable — use trusted proxies config |
184
283
  | `$_GET` / `$_POST` / `$_SESSION` | Stale in Octane — use `$request->input()` |
284
+ | Login route on `/api/*` | Login goes on `routes/web.php` (session middleware) |
185
285
 
186
- ### Backend
286
+ ### Backend (Octane safety)
187
287
 
188
288
  | Action | Reason |
189
289
  |--------|--------|
190
290
  | Business logic in controllers | Move to Service classes |
191
- | `static` properties on services | Memory leaks in Octane workers |
291
+ | `static` properties on services | Memory leaks across requests |
192
292
  | Global variables / superglobals | Stale state in Octane |
193
- | `die()` / `exit()` | Kills the Octane worker process |
293
+ | `die()` / `exit()` / `dd()` | Kills the worker process |
194
294
  | `config(['key' => 'val'])` at runtime | Affects all concurrent requests |
195
295
  | `migrate:fresh` / `db:wipe` / `db:reset` | Destroys production data |
196
- | `app()` / `resolve()` in constructors | Use constructor DI instead |
197
- | `Inertia::render()` after POST/PUT/DELETE | Use `redirect()->route()` instead |
198
- | `dd()` / `dump()` in production code | Use structured `Log::info()` |
296
+ | `app()` / `resolve()` in constructors | Use constructor DI |
297
+ | `dump()` / `dd()` in production | Use structured `Log::info()` |
199
298
 
200
299
  ### Frontend (React)
201
300
 
202
301
  | Action | Reason |
203
302
  |--------|--------|
204
- | Call external APIs with secrets from JS | Exposes tokens route through Laravel API |
205
- | `__()` inside JSX / render | Hook violationdefine as CONST before hooks |
206
- | `fetch()` / `axios` for page data | Bypasses Inertia — use props from controller |
207
- | `<a href>` for internal links | Full page reload — use `<Link>` |
208
- | `window.location` for navigation | Full reload — use `router.visit()` |
303
+ | `Inertia::render()` / `useForm()` from Inertia | Use API + Axios (Inertia is LEGACY) |
304
+ | `fetch()` for page data | Bypasses interceptorsuse `api.get()` |
305
+ | `<a href>` for internal links | Full page reload — use `<Link>` from react-router-dom |
306
+ | `window.location` for SPA navigation | Full reload — use `useNavigate()` |
209
307
  | Inline SVGs in JSX | Bloats components — use SVG files with `?react` |
210
308
  | Raw `console.log` | Uncontrolled — use debug constant pattern |
211
309
  | Inline Tailwind class soup | Unreadable — use STYLES const object |
212
- | `axios.post()` for forms | No CSRF/errors — use `useForm().post()` |
310
+ | `useEffect` to derive state | Anti-pattern — use `useMemo` |
311
+ | Skipping skeleton/empty/error states | Bad UX — all three are mandatory per page |
213
312
 
214
313
  ## UI/UX Design Intelligence
215
314
 
@@ -219,9 +318,11 @@ return Inertia::render('Dashboard', [
219
318
 
220
319
  ```bash
221
320
  vendor/bin/phpstan analyse --level=6 # Static analysis
222
- vendor/bin/phpunit # Tests
321
+ vendor/bin/phpunit # Tests (or Pest)
223
322
  vendor/bin/php-cs-fixer fix --dry-run # Code style
224
323
  php artisan test # Laravel test runner
324
+ npx tsc --noEmit # Frontend type check
325
+ npx eslint resources/js/ # Frontend lint (optional)
225
326
  ```
226
327
 
227
328
  ## Database
@@ -249,15 +350,15 @@ $results = DB::select('SELECT * FROM users WHERE id = ?', [$id]);
249
350
  ## Workflow
250
351
 
251
352
  ```
252
- 0. TODO LIST → Create detailed todo list from prompt
253
- 1. BRANCH → Create feature/ | fix/ | refactor/ | test/
254
- 2. RESEARCH → Run research-web agent for NEW features
255
- 3. IMPLEMENT → Types, strict mode, Octane-safe, DI
256
- 4. TEST → Run PHPStan + PHPUnit
257
- 5. DOCUMENT → Run documenter agent for modified files
258
- 6. UPDATE → Update THIS FILE (CLAUDE.md) with changes
259
- 7. QUALITY → PHPStan + PHPUnit + PHP-CS-Fixer
260
- 8. COMMIT → Conventional commits, merge to main
353
+ 0. TODO LIST → Detailed plan from prompt; identify affected layer(s)
354
+ 1. BRANCH → feature/ | fix/ | refactor/ | test/
355
+ 2. RESEARCH → research-web for new features (cite sources)
356
+ 3. IMPLEMENT → Bottom-up: FormRequest+Policy Service → Resource → Controller → Route → React page
357
+ 4. TEST → PHPUnit feature tests (happy + 401 + 403 + 422); React: api.test.ts
358
+ 5. DOCUMENT → documenter agent for modified files; update domain docs
359
+ 6. UPDATE → Update THIS FILE (CLAUDE.md) Last Change + relevant sections
360
+ 7. QUALITY → PHPStan + PHPUnit + PHP-CS-Fixer + tsc + eslint
361
+ 8. COMMIT → Conventional commits; merge to main
261
362
  ```
262
363
 
263
364
  ## Domain Documentation