# Domain Pitfalls **Domain:** CRM/Admin Member Note Systems **Researched:** 2026-02-13 ## Critical Pitfalls Mistakes that cause rewrites or major issues. ### Pitfall 1: N+1 Query Explosion on Member List **What goes wrong:** Loading note counts without eager loading causes N+1 queries (1 query per member row). **Why it happens:** Developer writes `$member->notes->count()` in Blade loop, triggering lazy load per iteration. **Consequences:** - Member list page load time grows linearly with pagination size - Database connection pool exhaustion (15 members = 15 extra queries) - Poor UX (slow page loads) **Prevention:** ```php // WRONG: Lazy loading in loop $members = Member::paginate(15); // In Blade: {{ $member->notes->count() }} = 15 extra queries // RIGHT: Eager load counts $members = Member::withCount('notes')->paginate(15); // In Blade: {{ $member->notes_count }} = 1 query total ``` **Detection:** Laravel Debugbar shows 15+ queries on member list page; query log shows repeated `SELECT * FROM notes WHERE notable_id = ?`. --- ### Pitfall 2: Allowing Note Edit/Delete Breaks Audit Trail **What goes wrong:** Adding "Edit" or "Delete" buttons on notes destroys compliance value. **Why it happens:** Users request ability to "fix typos" or "remove mistakes"; developer adds feature without considering audit implications. **Consequences:** - Legal/compliance risk (no immutable record of observations) - Loss of trust (admins can rewrite history) - Violates NPO transparency expectations - Healthcare standards require append-only audit trails **Prevention:** - Hard constraint in requirements: NO edit/delete operations - Database triggers to prevent UPDATE/DELETE on notes table (optional) - UI design: no edit/delete buttons, only "Add Correction" button that creates new note - Documentation: explain append-only pattern to stakeholders upfront **Detection:** Warning signs: user stories mention "edit note," "delete note," or "fix note." --- ### Pitfall 3: XSS Vulnerability via Unescaped Note Content **What goes wrong:** User enters `` in note content, executes on admin viewing notes. **Why it happens:** Developer uses `{!! $note->content !!}` (unescaped) instead of `{{ $note->content }}` (escaped) in Blade. **Consequences:** - Security breach (session hijacking, CSRF token theft) - Malicious admin can inject scripts affecting other admins - Compliance violation (data integrity) **Prevention:** ```php // Backend: Strip HTML tags on save $note->content = strip_tags($request->content); // Blade: Always escape (default) {{ $note->content }} // ✓ Safe (auto-escapes) {!! $note->content !!} // ✗ Dangerous (unescaped) // Alpine.js: Use x-text, not x-html
// ✓ Safe
// ✗ Dangerous ``` **Detection:** Penetration testing; submit `` in note form and check if alert fires. --- ### Pitfall 4: CSRF Token Missing on AJAX Requests **What goes wrong:** Axios POST requests fail with 419 CSRF error. **Why it happens:** Axios not configured to send `X-XSRF-TOKEN` header, or cookie not set. **Consequences:** - All note creation fails (HTTP 419) - Poor UX (users can't add notes) - Frustration (appears broken) **Prevention:** ```javascript // Verify in resources/js/bootstrap.js: axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; // Axios automatically reads XSRF-TOKEN cookie and sends as X-XSRF-TOKEN header // Verify in Blade layout: // Laravel automatically sets XSRF-TOKEN cookie on page load ``` **Detection:** Browser console shows `POST /admin/members/1/notes 419 CSRF token mismatch`; Laravel logs show CSRF validation failure. ## Moderate Pitfalls ### Pitfall 5: Storing Author Name (String) Instead of user_id (Foreign Key) **What goes wrong:** Storing `author_name: "John Smith"` instead of `user_id: 42`. **Prevention:** Use foreign key to `users` table; join to get name dynamically. Prevents orphaned author names if user renamed. --- ### Pitfall 6: Forgetting to Index notable_id in Polymorphic Table **What goes wrong:** Queries like `WHERE notable_id = 123 AND notable_type = 'App\\Models\\Member'` become slow without composite index. **Prevention:** Migration includes `$table->index(['notable_type', 'notable_id'])` (morphs() helper does this automatically). --- ### Pitfall 7: Using Global Alpine.js Store for Notes **What goes wrong:** `Alpine.store('notes', {...})` creates shared state; updating notes for Member A affects Member B's UI. **Prevention:** Use component-scoped `x-data` per member row (see ARCHITECTURE.md Pattern 3). --- ### Pitfall 8: Not Handling AJAX Errors Gracefully **What goes wrong:** Network failure causes silent failure; user thinks note saved but it didn't. **Prevention:** Always show user feedback on error: ```javascript catch (error) { if (error.response?.status === 422) { alert('備註內容不得為空或超過 1000 字'); } else { alert('新增備註失敗,請稍後再試'); } } ``` --- ### Pitfall 9: Loading All Notes on Page Load (Performance) **What goes wrong:** Eager loading `with('notes')` on member list loads ALL note content, bloating response. **Prevention:** Use `withCount('notes')` to load only counts; fetch full notes via AJAX when user expands panel. ## Minor Pitfalls ### Pitfall 10: Inconsistent Timestamp Formatting **What goes wrong:** Mixing "2 hours ago" and "2026-02-13 14:30" without context confuses users. **Prevention:** Use relative timestamps for recent (<24h), absolute for older, with tooltip showing full ISO 8601. --- ### Pitfall 11: No Loading State on AJAX Submit **What goes wrong:** User clicks "儲存" multiple times during slow network, creates duplicate notes. **Prevention:** Disable submit button while `isAdding === true`: ```html ``` --- ### Pitfall 12: Not Escaping Traditional Chinese Quotes in JSON **What goes wrong:** Note content with `「quoted text」` breaks JSON parsing if not properly escaped. **Prevention:** Use `json_encode()` (Laravel does automatically); test with Traditional Chinese punctuation. --- ### Pitfall 13: Missing Empty State Message **What goes wrong:** Expanded note panel shows blank space when member has no notes; user confused. **Prevention:** ```html

尚無備註

``` --- ### Pitfall 14: Forgetting Dark Mode Styling **What goes wrong:** Note panel has white background in dark mode, blinds users. **Prevention:** Use Tailwind `dark:` prefix on all color classes (see ARCHITECTURE.md). ## Phase-Specific Warnings | Phase Topic | Likely Pitfall | Mitigation | |-------------|---------------|------------| | **Database Migration** | Forgetting `notable_type` and `notable_id` index | Use `$table->morphs('notable')` (auto-creates index) | | **Model Relationships** | Using `hasMany` instead of `morphMany` | Follow polymorphic pattern from start (future-proof) | | **Controller JSON Responses** | Returning raw model instead of Resource | Always wrap in `new NoteResource($note)` for consistency | | **Alpine.js AJAX** | Using Fetch API without CSRF token | Use Axios (already configured with CSRF in bootstrap.js) | | **Blade Templates** | Unescaping note content | Always use `{{ }}` not `{!! !!}` for user input | | **Query Optimization** | Lazy loading note counts | Use `withCount('notes')` in controller query | | **Audit Logging** | Forgetting to log note creation | Add `AuditLogger::log()` call in controller `store()` method | | **Access Control** | Adding new permission instead of reusing middleware | Reuse existing `admin` middleware (all admin roles share notes) | | **UI/UX** | Full page reload after note submit | Use AJAX with Alpine.js (no navigation) | | **Testing** | Not testing with Traditional Chinese input | Test with `「」、。!?` characters in note content | ## Real-World Failure Modes ### Scenario 1: The Disappeared Notes **What happened:** Developer used `onDelete('cascade')` on `user_id` foreign key. When an admin user account was deleted, all their notes disappeared. **Impact:** Lost audit trail; compliance violation. **Fix:** Use `onDelete('restrict')` on `user_id` (prevent user deletion if they authored notes) OR use soft deletes on users table. --- ### Scenario 2: The Slow Member List **What happened:** 15-member list took 3 seconds to load due to N+1 queries loading note counts. **Impact:** Poor UX; users complained about slowness. **Fix:** Changed `Member::paginate(15)` to `Member::withCount('notes')->paginate(15)`. Load time dropped to 200ms. --- ### Scenario 3: The XSS Attack **What happened:** Malicious admin entered `` in note. When chairman viewed notes, session cookie leaked. **Impact:** Session hijacking; chairman account compromised. **Fix:** Added `strip_tags()` on backend save, changed Blade to `{{ }}`, changed Alpine.js to `x-text`. --- ### Scenario 4: The CSRF 419 Mystery **What happened:** All note submissions failed with 419 error after developer added custom Axios instance without CSRF config. **Impact:** Feature completely broken; users frustrated. **Fix:** Reverted to global `axios` instance configured in `bootstrap.js` (includes CSRF token automatically). ## Sources - [10 Essential Audit Trail Best Practices for 2026](https://signal.opshub.me/audit-trail-best-practices/) - Append-only logging - [Addendums to Progress Notes - Healthcare Compliance](https://support.sessionshealth.com/article/393-addendum) - Why no editing - [Laravel Debugbar N+1 Query Detection](https://github.com/barryvdh/laravel-debugbar) - Performance monitoring - [Laravel 10.x CSRF Protection](https://laravel.com/docs/10.x/csrf) - CSRF token handling - [Laravel 10.x Blade Security](https://laravel.com/docs/10.x/blade#displaying-data) - XSS prevention - [Alpine.js Security Best Practices](https://alpinejs.dev/essentials/templating#security) - x-text vs x-html - [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) - Input sanitization --- *Pitfalls research for: Member Notes System (會員備註系統)* *Researched: 2026-02-13*