diff --git a/.planning/phases/12-attachment-research-&-backend-validation/docs/ATTACHMENT_LEGACY.md b/.planning/phases/12-attachment-research-&-backend-validation/docs/ATTACHMENT_LEGACY.md
new file mode 100644
index 000000000..0e1396d1c
--- /dev/null
+++ b/.planning/phases/12-attachment-research-&-backend-validation/docs/ATTACHMENT_LEGACY.md
@@ -0,0 +1,538 @@
+# Legacy AttachmentStore Implementation - Reference Documentation
+
+**Source:** `web/app/assets/javascripts/react-components/stores/AttachmentStore.js.coffee`
+**Purpose:** Reference documentation for porting file upload functionality to React/Redux
+**Date:** 2026-02-02
+
+---
+
+## Overview
+
+The legacy AttachmentStore is a **Reflux store** written in CoffeeScript that handles file attachment uploads for both lesson chat and session chat. It provides client-side validation, FormData construction, AJAX upload, and error handling. The store integrates with the legacy ChatStore to trigger chat message creation after successful uploads.
+
+**Key characteristics:**
+- Reflux store pattern (event-based state management)
+- Supports two attachment types: `notation` and `audio`
+- Client-side validation (10 MB limit)
+- jQuery AJAX for multipart/form-data uploads
+- Hidden file input trigger pattern
+- Separate upload flows for lesson vs session context
+
+---
+
+## 1. File Structure
+
+**Location:** `web/app/assets/javascripts/react-components/stores/AttachmentStore.js.coffee`
+
+**Store Pattern:** Reflux.createStore with listenables
+
+**Public Methods (Actions):**
+- `onStartAttachRecording(lessonId, sessionId)` - Triggers recording selector dialog
+- `onStartAttachNotation(lessonId, sessionId)` - Triggers notation file input
+- `onStartAttachAudio(lessonId, sessionId)` - Triggers audio file input
+- `onUploadNotations(notations, doneCallback, failCallback)` - Uploads notation files
+- `onUploadAudios(notations, doneCallback, failCallback)` - Uploads audio files
+
+**Internal State:**
+- `uploading: boolean` - Prevents concurrent uploads
+- `lessonId: string | null` - Current lesson context
+- `sessionId: string | null` - Current session context
+
+**Integration:**
+- Listens to `AttachmentActions` (Reflux action dispatcher)
+- Listens to `AppStore` for app initialization
+- Uses `context.JK.Rest()` for API calls
+- Uses `@app.layout.showDialog()` for upload progress dialog
+
+---
+
+## 2. Upload Flow
+
+### Step-by-Step Breakdown (Notation Upload)
+
+**1. User Initiates Upload**
+```javascript
+// Translated from CoffeeScript
+onStartAttachNotation(lessonId, sessionId = null) {
+ if (this.uploading) {
+ logger.warn("rejecting onStartAttachNotation attempt as currently busy");
+ return;
+ }
+ this.lessonId = lessonId;
+ this.sessionId = sessionId;
+
+ logger.debug("notation upload started for lesson: " + lessonId);
+ this.triggerNotation(); // Clicks hidden file input
+ this.changed(); // Triggers state update
+}
+```
+
+**2. Hidden File Input Triggered**
+```javascript
+triggerNotation() {
+ if (!this.attachNotationBtn) {
+ this.attachNotationBtn = $('input.attachment-notation').eq(0);
+ }
+ console.log("@attachNotationBtn", this.attachNotationBtn);
+ this.attachNotationBtn.trigger('click');
+}
+```
+
+**3. Files Selected → onUploadNotations Called**
+```javascript
+onUploadNotations(notations, doneCallback, failCallback) {
+ logger.debug("beginning upload of notations", notations);
+ this.uploading = true;
+ this.changed();
+
+ // Client-side validation
+ const formData = new FormData();
+ let maxExceeded = false;
+
+ $.each(notations, (i, file) => {
+ const max = 10 * 1024 * 1024; // 10 MB
+ if (file.size > max) {
+ maxExceeded = true;
+ return false; // Break loop
+ }
+ formData.append('files[]', file);
+ });
+
+ if (maxExceeded) {
+ this.app.notify({
+ title: "Maximum Music Notation Size Exceeded",
+ text: "You can only upload files up to 10 megabytes in size."
+ });
+ failCallback();
+ this.uploading = false;
+ this.changed();
+ return;
+ }
+
+ // Add context parameters
+ if (this.lessonId) {
+ formData.append('lesson_session_id', this.lessonId);
+ } else if (this.sessionId) {
+ formData.append('session_id', this.sessionId);
+ }
+
+ formData.append('attachment_type', 'notation');
+
+ // Show progress dialog
+ this.app.layout.showDialog('music-notation-upload-dialog');
+
+ // Upload via REST API
+ rest.uploadMusicNotations(formData)
+ .done((response) => this.doneUploadingNotations(notations, response, doneCallback, failCallback))
+ .fail((jqXHR) => this.failUploadingNotations(jqXHR, failCallback));
+}
+```
+
+**4. REST API Call (from jam_rest.js)**
+```javascript
+function uploadMusicNotations(formData) {
+ return $.ajax({
+ type: "POST",
+ processData: false, // Don't convert FormData to string
+ contentType: false, // Don't set Content-Type (browser adds multipart boundary)
+ dataType: "json",
+ cache: false,
+ url: "/api/music_notations",
+ data: formData
+ });
+}
+```
+
+**5. Success Handler**
+```javascript
+doneUploadingNotations(notations, response, doneCallback, failCallback) {
+ this.uploading = false;
+ this.changed();
+
+ const error_files = [];
+ $.each(response, (i, music_notation) => {
+ if (music_notation.errors) {
+ error_files.push(notations[i].name);
+ }
+ });
+
+ if (error_files.length > 0) {
+ failCallback();
+ this.app.notifyAlert("Failed to upload notations.", error_files.join(', '));
+ } else {
+ doneCallback();
+ }
+}
+```
+
+**6. Error Handler**
+```javascript
+failUploadingNotations(jqXHR, failCallback) {
+ this.uploading = false;
+ this.changed();
+
+ if (jqXHR.status == 413) {
+ // File too large (server-side check)
+ this.app.notify({
+ title: "Maximum Music Notation Size Exceeded",
+ text: "You can only upload files up to 10 megabytes in size."
+ });
+ } else {
+ this.app.notifyServerError(jqXHR, "Unable to upload music notations");
+ }
+}
+```
+
+---
+
+## 3. Client-Side Validation
+
+### File Size Check
+
+**Limit:** 10 MB (10 × 1024 × 1024 bytes)
+
+**Implementation:**
+```javascript
+const max = 10 * 1024 * 1024;
+if (file.size > max) {
+ maxExceeded = true;
+ return false;
+}
+```
+
+**Error Message:**
+```
+Title: "Maximum Music Notation Size Exceeded"
+Text: "You can only upload files up to 10 megabytes in size."
+```
+
+**When shown:**
+- Immediately after file selection, before upload starts
+- If server returns 413 (Payload Too Large)
+
+### File Type Validation
+
+**Note:** The legacy AttachmentStore does NOT perform file type validation on the client side. File type filtering happens at the file input level via HTML `accept` attribute, and server-side validation happens in the `MusicNotationUploader` whitelist.
+
+**Server-side whitelist (for reference):**
+```ruby
+def extension_white_list
+ %w(pdf png jpg jpeg gif xml mxl txt wav flac ogg aiff aifc au)
+end
+```
+
+---
+
+## 4. Attachment Type Detection
+
+The legacy implementation uses **explicit attachment type** passed as a parameter, not automatic detection based on file extension.
+
+**Two Upload Methods:**
+1. `onUploadNotations()` → `attachment_type: 'notation'`
+2. `onUploadAudios()` → `attachment_type: 'audio'`
+
+**How it works:**
+```javascript
+// For notation files
+formData.append('attachment_type', 'notation');
+
+// For audio files
+formData.append('attachment_type', 'audio');
+```
+
+**File Type to Attachment Type Mapping:**
+
+| File Extensions | Attachment Type | Used For |
+|----------------|----------------|----------|
+| `.pdf, .png, .jpg, .jpeg, .gif, .xml, .mxl, .txt` | `notation` | Music notation, sheet music, images, text |
+| `.wav, .flac, .ogg, .aiff, .aifc, .au` | `audio` | Audio files |
+
+**Note:** The mapping is implicit based on which button the user clicks (Attach Notation vs Attach Audio), not programmatically determined from file extension.
+
+---
+
+## 5. Integration Points
+
+### Integration with ChatStore
+
+The AttachmentStore does NOT directly trigger chat messages. Instead:
+
+1. **Upload completes** → Returns array of created `MusicNotation` objects
+2. **Backend automatically creates ChatMessage** with attachment reference
+3. **Backend broadcasts via WebSocket** → ChatMessage with attachment metadata
+4. **ChatStore receives WebSocket message** → Displays in chat UI
+
+**Key insight:** The attachment upload is decoupled from chat message creation. The backend handles the integration.
+
+### Event Emission Pattern
+
+**Reflux trigger pattern:**
+```javascript
+changed() {
+ this.trigger({
+ lessonId: this.lessonId,
+ uploading: this.uploading
+ });
+}
+```
+
+**When triggered:**
+- On `onStartAttachNotation()` / `onStartAttachAudio()` - Sets `uploading: true`
+- On `doneUploadingNotations()` / `failUploadingNotations()` - Sets `uploading: false`
+
+**UI components consume via:**
+```javascript
+// In React component
+mixins: [Reflux.connect(AttachmentStore, 'attachmentState')]
+
+// Component can access this.state.attachmentState.uploading
+```
+
+### Dialog System Integration
+
+**Upload Progress Dialog:**
+```javascript
+this.app.layout.showDialog('music-notation-upload-dialog');
+```
+
+**Notification System:**
+```javascript
+// Success/Info notification
+this.app.notify({
+ title: "Title",
+ text: "Message"
+});
+
+// Error notification
+this.app.notifyAlert("Error title", "Error details");
+
+// Server error handler
+this.app.notifyServerError(jqXHR, "Context message");
+```
+
+---
+
+## 6. FormData Construction
+
+**Critical Pattern for React Port:**
+
+```javascript
+// Create FormData instance
+const formData = new FormData();
+
+// Append files (supports multiple)
+formData.append('files[]', file); // Array notation for multiple files
+
+// Append session context (one of these)
+formData.append('lesson_session_id', lessonId); // For lesson chat
+formData.append('session_id', sessionId); // For session chat
+
+// Append attachment type
+formData.append('attachment_type', 'notation'); // or 'audio'
+
+// AJAX configuration
+$.ajax({
+ type: "POST",
+ processData: false, // CRITICAL: Don't convert FormData to string
+ contentType: false, // CRITICAL: Don't set Content-Type (browser adds multipart boundary)
+ dataType: "json",
+ url: "/api/music_notations",
+ data: formData
+});
+```
+
+**Why this matters for React port:**
+- `processData: false` → When using fetch(), don't stringify FormData
+- `contentType: false` → Don't manually set `Content-Type` header (browser auto-adds boundary)
+- File array notation `files[]` → Required by Rails params parsing
+- Session context → Only ONE of `lesson_session_id` or `session_id` should be set
+
+---
+
+## 7. Audio Upload Flow
+
+The audio upload flow is nearly identical to notation upload, with these differences:
+
+**Method:** `onUploadAudios()` instead of `onUploadNotations()`
+
+**attachment_type:** `'audio'` instead of `'notation'`
+
+**Error messages:**
+- Title: "Maximum Music Audio Size Exceeded"
+- Failure: "Failed to upload audio files."
+
+**Code Example:**
+```javascript
+onUploadAudios(notations, doneCallback, failCallback) {
+ // ... same validation logic ...
+
+ formData.append('attachment_type', 'audio'); // Only difference
+
+ rest.uploadMusicNotations(formData) // Same endpoint!
+ .done((response) => this.doneUploadingAudios(notations, response, doneCallback, failCallback))
+ .fail((jqXHR) => this.failUploadingAudios(jqXHR, failCallback));
+}
+```
+
+**Note:** Both notation and audio uploads use the same REST endpoint (`/api/music_notations`), differentiated only by the `attachment_type` parameter.
+
+---
+
+## 8. Recording Attachment Flow
+
+**Different from file uploads:** Recording attachments link existing recordings to lessons/sessions, they don't upload files.
+
+```javascript
+recordingsSelected(recordings) {
+ if (this.lessonId) {
+ const options = { id: this.lessonId, recordings: recordings };
+ rest.attachRecordingToLesson(options)
+ .done((response) => this.attachedRecordingsToLesson(response))
+ .fail((jqXHR) => this.attachedRecordingsFail(jqXHR));
+ } else if (this.sessionId) {
+ const options = { id: this.sessionId, recordings: recordings };
+ rest.attachRecordingToSession(options)
+ .done((response) => this.attachedRecordingsToSession(response))
+ .fail((jqXHR) => this.attachedRecordingsFail(jqXHR));
+ }
+}
+```
+
+**Success messages:**
+- Lesson: "Your recording has been associated with this lesson, and can be accessed from the Messages window for this lesson."
+- Session: "Your recording has been associated with this session."
+
+**Note:** This flow is NOT in scope for v1.2 Session Attachments milestone. Recording attachments are handled separately from file uploads.
+
+---
+
+## 9. Key Patterns for React Port
+
+### Pattern 1: Hidden File Input with Button Trigger
+```javascript
+// Legacy uses jQuery trigger
+this.attachNotationBtn = $('input.attachment-notation').eq(0);
+this.attachNotationBtn.trigger('click');
+
+// React equivalent
+const fileInputRef = useRef(null);
+const handleAttachClick = () => {
+ fileInputRef.current?.click();
+};
+
+return (
+ <>
+
+ {
+ if (e.target.files?.[0]) {
+ handleFileSelect(e.target.files[0]);
+ e.target.value = ''; // Reset for re-selection
+ }
+ }}
+ />
+ >
+);
+```
+
+### Pattern 2: Upload State Management
+```javascript
+// Legacy Reflux store
+this.uploading = true;
+this.changed();
+
+// React Redux equivalent
+dispatch(setUploadingState({ uploading: true, error: null }));
+```
+
+### Pattern 3: Client-Side Validation Before Upload
+```javascript
+// Always validate BEFORE creating FormData
+const MAX_SIZE = 10 * 1024 * 1024;
+if (file.size > MAX_SIZE) {
+ dispatch(setUploadError('File exceeds 10 MB limit'));
+ return;
+}
+
+// Then proceed with FormData construction
+```
+
+### Pattern 4: FormData with fetch()
+```javascript
+// Modern fetch equivalent
+const formData = new FormData();
+formData.append('files[]', file);
+formData.append('session_id', sessionId);
+formData.append('attachment_type', 'notation');
+
+const response = await fetch('/api/music_notations', {
+ method: 'POST',
+ credentials: 'include', // Important for session cookies
+ body: formData // Don't set Content-Type!
+});
+
+if (!response.ok) {
+ if (response.status === 413) {
+ throw new Error('File too large - maximum 10 MB');
+ }
+ throw new Error('Upload failed');
+}
+
+return response.json();
+```
+
+---
+
+## 10. Differences Between Legacy and Modern Implementation
+
+| Aspect | Legacy (CoffeeScript) | Modern (React/Redux) |
+|--------|----------------------|---------------------|
+| Store pattern | Reflux with events | Redux Toolkit with slices |
+| AJAX library | jQuery $.ajax | fetch() API |
+| State updates | `this.changed()` triggers listeners | Redux actions with reducers |
+| File input | jQuery trigger on hidden input | useRef + ref.current.click() |
+| Context passing | Store instance variables | Redux state or component props |
+| Callbacks | done/fail callbacks | async/await with try/catch |
+| Error handling | jqXHR status checks | response.ok and response.status |
+| Multiple files | Supported (files[] array) | Implement single file first, extend later |
+| Dialog system | `@app.layout.showDialog()` | Custom React component or state flag |
+
+---
+
+## 11. Implementation Checklist for React Port
+
+Based on the legacy implementation, the React port should include:
+
+- [ ] Redux state for upload status (`uploading`, `progress`, `error`)
+- [ ] Hidden file input with ref-based trigger
+- [ ] Client-side file size validation (10 MB)
+- [ ] Client-side file type validation (match requirements list)
+- [ ] FormData construction with `files[]`, `session_id`, `attachment_type`
+- [ ] fetch() upload with `credentials: include`, no Content-Type header
+- [ ] Error handling for 413 (file too large)
+- [ ] Error handling for 422 (validation errors)
+- [ ] Success/error user feedback (toast notifications)
+- [ ] Disable UI during upload (prevent concurrent uploads)
+- [ ] Reset file input after selection (allow re-selection)
+- [ ] Integration with sessionChatSlice for attachment state
+- [ ] WebSocket message handling for attachment metadata
+
+---
+
+## Summary
+
+The legacy AttachmentStore provides a proven reference implementation for file uploads. Key takeaways:
+
+1. **FormData is critical:** `processData: false` and `contentType: false` equivalent in fetch()
+2. **Client-side validation prevents wasted uploads:** Check size before FormData construction
+3. **Single upload state flag:** Prevents concurrent uploads with simple boolean
+4. **Backend creates chat messages:** Don't manually create chat entries after upload
+5. **Error handling by status code:** 413 gets special user-friendly message
+6. **Hidden file input pattern:** Standard approach, works reliably
+7. **Same endpoint for both types:** Differentiated by `attachment_type` parameter
+
+**Next steps:** Refer to ATTACHMENT_API.md for backend contract details and response formats.