Feature: Notes (Catch-All)
Version: 1.1.0 Last Reviewed: 2026-02-18 Status: Approved
User Story
As a caregiver, I can log a free-form note with a category so we can track things not yet supported (pumping, meds, health observations).
MVP Scope
- Timestamp (defaults to "now", editable)
- Category: fixed list (pumping, health, medication, other)
- Text: free-form, required, max 10,000 characters
- Title: optional, max 200 characters
- Full CRUD operations
- Category-based filtering on list
- Time-based filtering on list
- Integration with timeline API (specified in timeline-api.md)
NOT in MVP
- Custom categories (Post-MVP #8)
- Attachments, photos, rich text
- Note templates
- Full-text search across note text
- Note reminders or scheduling
- Markdown formatting support
API Contract
Create Note
POST /api/v1/children/:childId/notes
Authorization: Bearer <token>
Request:
{
"category": "medication",
"text": "Gave 2ml of vitamin D drops"
}
Request (with title and custom timestamp):
{
"timestamp": "2026-02-10T14:30:00.000Z",
"category": "pumping",
"title": "Morning pump session",
"text": "Pumped 4oz from left side, 3.5oz from right."
}
Response 201:
{
"note": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"child_id": "123e4567-e89b-12d3-a456-426614174000",
"timestamp": "2026-02-10T14:30:00.000Z",
"category": "pumping",
"title": "Morning pump session",
"text": "Pumped 4oz from left side, 3.5oz from right.",
"created_by": "789e4567-e89b-12d3-a456-426614174000",
"created_at": "2026-02-10T14:30:15.000Z",
"updated_by": "789e4567-e89b-12d3-a456-426614174000",
"updated_at": "2026-02-10T14:30:15.000Z"
}
}
Get Note by ID
GET /api/v1/children/:childId/notes/:id
Authorization: Bearer <token>
Response 200:
{
"note": { ... }
}
List Notes
GET /api/v1/children/:childId/notes?category=pumping&since=2026-02-10T00:00:00.000Z
Authorization: Bearer <token>
Query Parameters (all optional):
- since: ISO8601 timestamp - return notes after this time (exclusive)
- category: filter by category (pumping, health, medication, other)
- limit: integer - max results (default 100, max 500)
Response 200:
{
"notes": [
{
"id": "...",
"child_id": "...",
"timestamp": "2026-02-10T18:45:00.000Z",
"category": "health",
"title": null,
"text": "First tooth visible on lower gum!",
"created_by": "...",
"created_at": "...",
"updated_by": "...",
"updated_at": "..."
}
],
"count": 1
}
Note: Results ordered by timestamp DESC (newest first).
Update Note
PUT /api/v1/children/:childId/notes/:id
Authorization: Bearer <token>
Request (all fields optional - partial update):
{
"text": "Updated text",
"category": "health",
"title": "Updated title"
}
Response 200:
{
"note": { ... }
}
Delete Note
DELETE /api/v1/children/:childId/notes/:id
Authorization: Bearer <token>
Response 204: (No Content)
Error Responses
Standard error format:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request body validation failed",
"details": [
{ "field": "text", "message": "Required and cannot be empty", "code": "required" }
]
}
}
Error codes: 400 VALIDATION_ERROR, 401 UNAUTHORIZED, 403 FORBIDDEN, 404 NOT_FOUND
Business Rules
Validation
- category: REQUIRED. Must be exactly "pumping", "health", "medication", or "other" (case-sensitive).
- text: REQUIRED. Min 1 character after trimming whitespace. Max 10,000 characters. Whitespace is trimmed on storage. Supports UTF-8 including emojis.
- title: Optional. Max 200 characters. Empty string after trim is treated as null. Stored trimmed.
- timestamp: Optional, defaults to server's current time. Must be valid ISO8601 if provided.
Access Control
- All operations require authentication
- User must have ChildAccess record (owner or caregiver) for the child
- No access → 403 FORBIDDEN
- Child/note not found → 404 NOT_FOUND
Audit Trail
- All CUD operations create an AuditLog entry
- Create: changes = full entity
- Update: changes = { before: {...}, after: {...} }
- Delete: changes = full entity being deleted
Query Behavior
- List orders by timestamp descending, ties broken by created_at descending
- "since" filters exclusively (timestamp > since)
- Category filter is exact match, case-sensitive
- Combined filters use AND logic
- Default limit 100, max 500
- Responses wrapped:
{ "notes": [...], "count": N }
Acceptance Criteria
- Can create notes with each category (pumping, health, medication, other)
- Timestamp defaults to now if not provided
- Can get a single note by ID
- Can list notes with category filter
- Can list notes with since filter
- Can combine category + since filters
- Can edit text, category, and timestamp
- Can delete a note (returns 204)
- Text is trimmed on storage
- Empty/whitespace-only text returns 400
- Text max 10,000 characters enforced
- UTF-8 and emojis preserved correctly
- List returns notes wrapped with count
- Validation errors return 400 with details array
- No access returns 403, not found returns 404
- All CUD operations create AuditLog entries
Test Cases
Create - Happy Path
- Pumping note: category=pumping, text="..." → 201
- Health note: category=health → 201
- Medication note: category=medication → 201
- Other note: category=other → 201
- Custom timestamp: timestamp=past date → 201, timestamp matches
- No timestamp: → 201, server sets current time
- Long text (5000 chars): → 201, full text stored
- UTF-8 and emojis: text="今日は 😊👶" → 201, preserved
Create - Validation
- Missing category: {text: "..."} → 400
- Invalid category: category=custom → 400
- Category case sensitive: category=Pumping → 400
- Missing text: {category: "health"} → 400
- Empty text: text="" → 400
- Whitespace only text: text=" " → 400 (trimmed to empty)
- Text too long: 10,001 chars → 400
- Text at max: 10,000 chars → 201
Create - Access Control
- No auth: → 401
- No access: → 403
- Caregiver access: → 201
- Child not found: → 404
Get by ID
- Get existing note: → 200, { note: {...} }
- Not found: → 404
- No access: → 403
List
- List all: → 200, { notes: [...], count: N }
- Filter by category: category=pumping → only pumping notes
- Filter by since: since=T1 → notes after T1 only (exclusive)
- Combined filters: category=pumping&since=T1 → AND logic
- List empty: → 200, { notes: [], count: 0 }
- List with limit: 5 notes, limit=2 → 2 most recent
- Invalid category filter: category=invalid → 400
- Ordering: newest first, ties by created_at
Update
- Update text: → 200
- Update category: → 200
- Update timestamp: → 200
- Update all fields: → 200
- Partial update: only text → category unchanged
- Text trimmed on update: " spaces " → "spaces"
- Empty text on update: text="" → 400
- Audit log: before/after captured
- Not found: → 404
- No access: → 403
Delete
- Delete note: → 204
- Delete then list: note gone
- Not found: → 404
- No access: → 403
- Audit log: deleted entity captured
Edge Cases
- Newlines preserved: text with \n → stored correctly
- Future timestamp: → 201 (allowed)
- Distant past timestamp: → 201 (allowed)
- Concurrent updates: last write wins, both audited
Boundaries
- No custom categories (fixed list for MVP)
- No full-text search
- No attachments or rich text
- No pagination beyond limit parameter
- Timeline integration defined in timeline-api.md, not here
- Category filter is case-sensitive exact match