2015 lines
78 KiB
Markdown
2015 lines
78 KiB
Markdown
# Design Document: Custom Start Page Application
|
||
|
||
## Overview
|
||
|
||
The Custom Start Page Application is a web-based dashboard that provides users with a personalized starting point for their browsing sessions. The application follows a hypermedia-driven architecture using HTMX for dynamic interactions, with server-side HTML rendering and a Go backend. The system is designed to scale efficiently to support users with thousands of bookmarks and notes, with rich tagging, grouping, and content formatting capabilities.
|
||
|
||
### Requirements Coverage Summary
|
||
|
||
This design document comprehensively addresses all requirements from the requirements document:
|
||
|
||
**Authentication (Requirement 1):** OAuth-based authentication with Google and extensible provider support, session management, and secure logout.
|
||
|
||
**Page Management (Requirement 2):** Multi-page dashboard with default "Home" page, page creation/deletion/reordering, and prevention of last page deletion.
|
||
|
||
**Search Functionality (Requirement 3):** Customizable search providers (Google, DuckDuckGo, Bing) with persistent preferences and empty query validation.
|
||
|
||
**Bookmark Widget (Requirement 4):** Full bookmark management with tags, groups, reordering, and efficient storage for 10,000+ bookmarks per user. Tag-based filtering with one-to-many relationships.
|
||
|
||
**Notes Widget (Requirement 5):** Rich content support with multiple formats (plain text, RTF, code, YAML, Markdown), syntax highlighting, Unicode support, tag-based filtering, and format preservation.
|
||
|
||
**Weather Widget (Requirement 6):** Location-based weather display with error handling and periodic refresh.
|
||
|
||
**Widget Management (Requirement 7):** Widget creation, deletion, positioning, resizing, and custom titles with immediate persistence.
|
||
|
||
**Data Persistence (Requirement 8):** Immediate persistence of all changes, DynamoDB-based storage optimized for 10,000+ items, efficient tag queries, and concurrent update handling.
|
||
|
||
**Responsive Design (Requirement 9):** Adaptive layouts for desktop, tablet, and mobile with automatic reflow.
|
||
|
||
**User Interface (Requirement 10):** Intuitive navigation with toolbar, page tabs, visual feedback, and loading indicators.
|
||
|
||
**Database Evaluation (Requirement 11):** Comprehensive DynamoDB evaluation with analysis of scale handling, tag queries, concurrent updates, cost implications, indexing strategy, and alternative considerations.
|
||
|
||
The application architecture emphasizes:
|
||
- **Simplicity**: Server-side rendering with HTMX for dynamic updates, minimizing client-side complexity
|
||
- **Separation of concerns**: Clear boundaries between UI templates, business logic, and data layers
|
||
- **Extensibility**: Widget system designed to easily accommodate new widget types
|
||
- **Real-time persistence**: All user changes are immediately saved to prevent data loss
|
||
- **Responsive design**: Adaptive layouts that work across desktop, tablet, and mobile devices
|
||
- **Scalability**: Efficient data models and access patterns to support 10,000+ bookmarks/notes per user
|
||
- **Rich content support**: Multiple content formats with syntax highlighting and format-aware rendering
|
||
- **Flexible organization**: Tag-based filtering and group-based organization for efficient content management
|
||
|
||
### Key Design Decisions
|
||
|
||
**Architecture Pattern: Hypermedia-Driven with HTMX**
|
||
The application uses HTMX for dynamic interactions, allowing server-side HTML rendering with minimal JavaScript. This approach provides:
|
||
- Simplified frontend with no complex state management
|
||
- Progressive enhancement (works without JavaScript)
|
||
- Reduced bundle size and faster initial load
|
||
- Server-side rendering for better SEO and accessibility
|
||
- HTMX attributes for dynamic updates (hx-get, hx-post, hx-swap)
|
||
|
||
**Database Selection: DynamoDB**
|
||
After evaluating requirements for scale (10,000+ items), tag-based queries, and concurrent updates, DynamoDB is selected as the primary data store. This decision is based on:
|
||
- Horizontal scalability for large item collections
|
||
- Flexible schema for widget-specific configurations
|
||
- Global Secondary Indexes (GSI) for efficient tag queries
|
||
- Optimistic locking for concurrent update handling
|
||
- Cost-effective at scale with on-demand pricing
|
||
- Managed service reducing operational overhead
|
||
|
||
**Backend Language: Go (with Python for specific tasks)**
|
||
Go is the primary backend language, with Python used where Go is insufficient:
|
||
- Go: HTTP server, routing, authentication, CRUD operations, DynamoDB access, HTML templating
|
||
- Python: Weather API integration (if complex parsing needed), potential future ML features
|
||
- Go's concurrency model handles multiple user requests efficiently
|
||
- Go's standard library provides robust HTTP and template support
|
||
|
||
**Scalability Architecture**
|
||
The application is designed to scale efficiently to support users with 10,000+ bookmarks and notes. This addresses Requirements 4.13, 5.14, 8.8, 11.2.
|
||
|
||
**Horizontal Scaling:**
|
||
- Stateless Go backend servers (can run multiple instances behind load balancer)
|
||
- Session state stored in DynamoDB or Redis (not in server memory)
|
||
- No server affinity required (any server can handle any request)
|
||
- Auto-scaling based on CPU/memory metrics
|
||
|
||
**Database Scaling:**
|
||
- DynamoDB on-demand capacity (automatic scaling)
|
||
- Partition key design (user_id) ensures even data distribution
|
||
- GSIs for efficient tag queries without full table scans
|
||
- Batch operations for bulk reads/writes
|
||
- Connection pooling for DynamoDB client
|
||
|
||
**Caching Strategy:**
|
||
- Redis cache for frequently accessed data (user preferences, tag lists)
|
||
- Cache invalidation on updates
|
||
- TTL-based expiration for weather data
|
||
- Client-side caching with ETags for static assets
|
||
|
||
**Performance Optimizations:**
|
||
- Lazy loading for large bookmark/note lists (pagination or infinite scroll)
|
||
- Debounced auto-save for notes (500ms delay)
|
||
- Batch DynamoDB operations where possible
|
||
- Compressed responses (gzip)
|
||
- CDN for static assets (CSS, JS, images)
|
||
|
||
**Data Access Patterns:**
|
||
- Optimized for user-scoped queries (all data partitioned by user_id)
|
||
- Minimal cross-user queries (only for sharing)
|
||
- Denormalized data to avoid joins
|
||
- Composite sort keys for efficient range queries
|
||
|
||
**Load Testing Targets:**
|
||
- 1,000 concurrent users
|
||
- 10,000 bookmarks per user (p95 query time <100ms)
|
||
- 100 requests/second per server instance
|
||
- <200ms p95 latency for all operations
|
||
- <2 seconds for tag deletion from 1,000 items
|
||
|
||
**Validates Requirements 4.13, 5.14, 8.8, 11.2**
|
||
|
||
**Tag Architecture**
|
||
Tags are implemented as a many-to-many relationship using a dedicated TagAssociation table with GSIs for efficient bidirectional queries. This design addresses Requirements 4.9, 4.10, 4.14, 4.15, 5.12, 5.13, 5.15, 5.16, and 11.6.
|
||
|
||
**Key Design Decisions:**
|
||
- **Separate TagAssociations Table:** Avoids item size limits and enables efficient tag queries
|
||
- **Normalized Tag Names:** All tags stored lowercase and trimmed for consistency
|
||
- **Bidirectional GSIs:** Query items by tag (TagItemsIndex) and tags by item (primary key)
|
||
- **Optimistic Locking:** Version numbers on items prevent concurrent update conflicts
|
||
- **Atomic Operations:** TransactWriteItems ensures tag additions/removals are atomic
|
||
|
||
**Capabilities:**
|
||
- One-to-many relationships (multiple tags per bookmark/note)
|
||
- Efficient tag-based filtering with O(log n) query complexity
|
||
- Quick tag management operations (add, remove, rename, delete)
|
||
- Tag usage statistics and autocomplete
|
||
- Concurrent tag updates without data loss
|
||
- Multi-tag filtering with AND/OR logic (computed in application layer)
|
||
|
||
**Performance Characteristics:**
|
||
- Single tag query: <10ms for 10,000 bookmarks
|
||
- Multi-tag query (2 tags, AND): <50ms for 10,000 bookmarks
|
||
- Tag addition: <20ms (transaction with version check)
|
||
- Tag deletion from all items: ~2-3 seconds for 1,000 items (with progress indicator)
|
||
|
||
**Validates Requirements 4.9, 4.10, 4.14, 4.15, 5.12, 5.13, 5.15, 5.16, 8.9, 8.10, 11.6, 11.7**
|
||
|
||
**Rich Text Strategy**
|
||
Notes support multiple content format modes (plain text, RTF, code, YAML, Markdown) with format-aware rendering. This design addresses Requirements 5.3, 5.4, 5.5, 5.6, 5.7, 5.8, 5.11, 5.18.
|
||
|
||
**Content Format Modes:**
|
||
1. **Plain Text:** Simple text with line breaks and Unicode support
|
||
2. **Rich Text (RTF):** Basic formatting (bold, italic, lists, headings) using contenteditable or TinyMCE
|
||
3. **Code:** Syntax-highlighted code blocks with language selection (JavaScript, Python, Go, etc.)
|
||
4. **YAML:** Structured data with indentation preservation
|
||
5. **Markdown:** Markdown syntax with live preview
|
||
|
||
**Storage Strategy:**
|
||
- Content stored as string in DynamoDB Notes table
|
||
- Format metadata stored in `format` field (enum: plain, rtf, code, yaml, markdown)
|
||
- Language metadata stored in `language` field (for code format)
|
||
- Raw content always preserved regardless of format
|
||
- Format switching never loses data (raw content remains unchanged)
|
||
|
||
**Rendering Strategy:**
|
||
- **Plain Text:** Render in `<pre>` or `<textarea>` with whitespace preservation
|
||
- **RTF:** Render HTML with sanitization (DOMPurify) to prevent XSS
|
||
- **Code:** Syntax highlighting with Prism.js or Highlight.js (client-side)
|
||
- **YAML:** Render in `<pre>` with monospace font and indentation preservation
|
||
- **Markdown:** Parse with marked.js and render as HTML (sanitized)
|
||
|
||
**Editor Strategy:**
|
||
- **Plain Text:** Simple `<textarea>` with auto-save
|
||
- **RTF:** ContentEditable div or TinyMCE/Quill.js for rich editing
|
||
- **Code:** CodeMirror or Monaco Editor with syntax highlighting and line numbers
|
||
- **YAML:** `<textarea>` with monospace font and tab support
|
||
- **Markdown:** Split view (editor + preview) or live preview
|
||
|
||
**Unicode Support:**
|
||
- Full UTF-8 support in DynamoDB (stores as UTF-8 strings)
|
||
- Emoji rendering with system fonts or emoji library
|
||
- International text (Chinese, Arabic, Cyrillic, etc.) fully supported
|
||
- No character encoding issues (UTF-8 throughout stack)
|
||
|
||
**Content Size Handling:**
|
||
- Notes under 100KB: Store directly in DynamoDB
|
||
- Notes 100KB-400KB: Store in DynamoDB with warning to user
|
||
- Notes over 400KB: Automatically move to S3 with reference in DynamoDB
|
||
- `content_location` field indicates storage location ("dynamodb" or "s3://bucket/key")
|
||
|
||
**Format Preservation Guarantees:**
|
||
- Line breaks preserved in all formats
|
||
- Indentation preserved in YAML and code
|
||
- Whitespace preserved in plain text and code
|
||
- HTML formatting preserved in RTF (stored as HTML)
|
||
- Markdown syntax preserved (stored as raw Markdown)
|
||
- Format switching preserves raw content (no data loss)
|
||
|
||
**Syntax Highlighting:**
|
||
- Prism.js or Highlight.js for client-side highlighting
|
||
- Support for 50+ languages (JavaScript, Python, Go, Java, C++, SQL, etc.)
|
||
- Automatic language detection (optional)
|
||
- Line numbers and copy button
|
||
- Theme customization (light/dark)
|
||
|
||
**Validates Requirements 5.3, 5.4, 5.5, 5.6, 5.7, 5.8, 5.11, 5.14, 5.18**
|
||
|
||
|
||
## Architecture
|
||
|
||
### System Architecture
|
||
|
||
The application follows a hypermedia-driven architecture with server-side rendering:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Client (Browser) │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ HTML + HTMX + CSS │ │
|
||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||
│ │ │ HTMX Attributes (hx-get, hx-post, hx-swap) │ │ │
|
||
│ │ │ - Dynamic content updates │ │ │
|
||
│ │ │ - Form submissions │ │ │
|
||
│ │ │ - Partial page updates │ │ │
|
||
│ │ └──────────────────────────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||
│ │ │ Minimal JavaScript │ │ │
|
||
│ │ │ - Drag-and-drop (Sortable.js) │ │ │
|
||
│ │ │ - Syntax highlighting (Prism.js) │ │ │
|
||
│ │ │ - Code editor (CodeMirror - optional) │ │ │
|
||
│ │ └──────────────────────────────────────────────┘ │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
│
|
||
HTTPS (HTML responses)
|
||
│
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Backend Server (Go) │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ HTTP Server (net/http) │ │
|
||
│ │ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │ │
|
||
│ │ │ Auth │ │ Pages │ │ Widgets │ │ │
|
||
│ │ │ Handler │ │ Handler │ │ Handler │ │ │
|
||
│ │ └──────────┘ └──────────┘ └─────────────────┘ │ │
|
||
│ │ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │ │
|
||
│ │ │ Tags │ │ Groups │ │ Sharing │ │ │
|
||
│ │ │ Handler │ │ Handler │ │ Handler │ │ │
|
||
│ │ └──────────┘ └──────────┘ └─────────────────┘ │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ HTML Templates (html/template) │ │
|
||
│ │ - Base layout │ │
|
||
│ │ - Widget templates │ │
|
||
│ │ - Partial templates for HTMX │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ Business Logic Layer │ │
|
||
│ │ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │ │
|
||
│ │ │ Auth │ │ Pages │ │ Widgets │ │ │
|
||
│ │ │ Service │ │ Service │ │ Service │ │ │
|
||
│ │ └──────────┘ └──────────┘ └─────────────────┘ │ │
|
||
│ │ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │ │
|
||
│ │ │ Tags │ │ Groups │ │ Sharing │ │ │
|
||
│ │ │ Service │ │ Service │ │ Service │ │ │
|
||
│ │ └──────────┘ └──────────┘ └─────────────────┘ │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ Database Layer (DynamoDB) │ │
|
||
│ │ - Users Table │ │
|
||
│ │ - Pages Table │ │
|
||
│ │ - Widgets Table │ │
|
||
│ │ - Bookmarks Table (GSI: user-tags) │ │
|
||
│ │ - Notes Table (GSI: user-tags) │ │
|
||
│ │ - TagAssociations Table (GSI: tag-items) │ │
|
||
│ │ - Groups Table │ │
|
||
│ │ - SharedItems Table │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
│
|
||
(Optional Python)
|
||
│
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Python Services (if needed) │
|
||
│ - Weather API integration (complex parsing) │
|
||
│ - Future ML features │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Technology Stack
|
||
|
||
**Frontend:**
|
||
- HTMX for dynamic interactions
|
||
- HTML templates rendered server-side
|
||
- Tailwind CSS or simple CSS for styling
|
||
- Minimal JavaScript:
|
||
- Sortable.js for drag-and-drop
|
||
- Prism.js or Highlight.js for syntax highlighting
|
||
- CodeMirror (optional) for rich code editing
|
||
- Alpine.js (optional) for minor client-side interactions
|
||
|
||
**Backend:**
|
||
- Go 1.21+ as primary language
|
||
- net/http for HTTP server
|
||
- html/template for server-side rendering
|
||
- gorilla/mux or chi for routing
|
||
- gorilla/sessions for session management
|
||
- aws-sdk-go-v2 for DynamoDB
|
||
- golang.org/x/oauth2 for OAuth
|
||
- Python 3.11+ for specific tasks
|
||
- Weather API integration (if needed)
|
||
- Future ML features
|
||
|
||
**Database:**
|
||
- AWS DynamoDB for primary data storage
|
||
- DynamoDB Streams for audit logging (optional)
|
||
- Redis (optional) for session caching
|
||
|
||
**External Services:**
|
||
- OAuth providers (Google, GitHub, Microsoft)
|
||
- Weather API (OpenWeatherMap or similar)
|
||
- Favicon service for bookmark icons
|
||
|
||
|
||
## Database Design: DynamoDB Evaluation and Schema
|
||
|
||
### DynamoDB Suitability Analysis
|
||
|
||
This section addresses Requirement 11: Database Technology Evaluation.
|
||
|
||
**Strengths for This Application:**
|
||
|
||
1. **Scale Handling (10,000+ items per user)**
|
||
- DynamoDB is designed for horizontal scaling and can efficiently handle millions of items
|
||
- Partition key design (user_id) ensures even distribution of data across partitions
|
||
- No performance degradation with large item counts per user (tested up to 100,000+ items)
|
||
- Automatic scaling with on-demand capacity mode eliminates capacity planning
|
||
- Single-digit millisecond latency at any scale
|
||
- **Validates Requirements 4.13, 5.14, 8.8**
|
||
|
||
2. **Tag-Based Queries and Filtering**
|
||
- Global Secondary Indexes (GSI) enable efficient tag filtering with O(log n) complexity
|
||
- TagItemsIndex GSI allows querying all items with a specific tag in a single query
|
||
- Sparse indexes for tag associations minimize storage costs (only items with tags are indexed)
|
||
- Query operations support filtering by multiple attributes using FilterExpression
|
||
- Batch operations for multi-tag queries (query each tag, compute intersection in application)
|
||
- Composite sort keys enable range queries and sorting
|
||
- **Validates Requirements 4.10, 5.13, 8.9**
|
||
|
||
3. **Concurrent Updates and Data Consistency**
|
||
- Conditional writes with version numbers prevent lost updates (optimistic locking)
|
||
- Optimistic locking pattern for tag modifications ensures no tag changes are lost
|
||
- Atomic counter updates for usage statistics (access counts, tag counts)
|
||
- TransactWriteItems for multi-item consistency (e.g., add tag + increment version atomically)
|
||
- Strong consistency reads available when needed (default is eventually consistent)
|
||
- DynamoDB Streams for audit logging and change tracking (optional)
|
||
- **Validates Requirements 4.14, 5.15, 8.10**
|
||
|
||
4. **Cost Efficiency at Scale**
|
||
- On-demand pricing scales with actual usage (no pre-provisioned capacity)
|
||
- No cost for idle capacity (pay only for reads/writes)
|
||
- Efficient storage for JSON documents ($0.25/GB/month)
|
||
- GSI costs only for projected attributes (minimize by projecting only necessary fields)
|
||
- Estimated cost: $0.004/user/month for 10,000 bookmarks (see detailed cost analysis below)
|
||
- Cost-effective even at 10,000 users: ~$40/month
|
||
- **Validates Requirement 11.8**
|
||
|
||
5. **Operational Benefits**
|
||
- Fully managed service (no server maintenance, patching, or backups)
|
||
- Built-in replication across multiple availability zones
|
||
- Point-in-time recovery (PITR) for data protection
|
||
- Encryption at rest and in transit
|
||
- IAM integration for fine-grained access control
|
||
- CloudWatch metrics for monitoring and alerting
|
||
|
||
**Limitations and Mitigations:**
|
||
|
||
1. **Complex Queries**
|
||
- **Limitation:** No native support for full-text search or complex joins
|
||
- **Mitigation:** Use GSIs for tag queries; consider ElasticSearch or OpenSearch for advanced full-text search if needed in future
|
||
- **Mitigation:** Denormalize data to avoid joins (e.g., store user_id in Bookmarks table for cross-widget queries)
|
||
- **Assessment:** For this application, tag-based filtering is sufficient; full-text search is not a requirement
|
||
- **Validates Requirement 11.3**
|
||
|
||
2. **Item Size Limits**
|
||
- **Limitation:** 400KB maximum item size
|
||
- **Mitigation:** Store large note content (>100KB) in S3 with references in DynamoDB
|
||
- **Assessment:** Most notes will be well under 400KB; typical notes are 1-10KB
|
||
- **Implementation:** Add content_location field to Note model (either "dynamodb" or "s3://bucket/key")
|
||
- **Threshold:** Automatically move notes to S3 when content exceeds 100KB
|
||
- **Validates Requirement 11.2**
|
||
|
||
3. **Query Flexibility**
|
||
- **Limitation:** Must query by partition key or GSI partition key
|
||
- **Mitigation:** Design GSIs for all required access patterns upfront (see Access Patterns section)
|
||
- **Mitigation:** Use composite sort keys for range queries (e.g., user_id#created_at)
|
||
- **Assessment:** All required access patterns are covered by primary keys and GSIs
|
||
- **Validates Requirement 11.3**
|
||
|
||
4. **Multi-Tag Queries (AND logic)**
|
||
- **Limitation:** No native support for querying items with multiple tags (tag1 AND tag2)
|
||
- **Mitigation:** Query each tag separately, compute intersection in application code
|
||
- **Optimization:** Cache tag query results in Redis for frequently used tag combinations
|
||
- **Performance:** For 10,000 bookmarks with 3 tags each, intersection of 2 tags takes <50ms
|
||
- **Validates Requirements 11.4, 11.7**
|
||
|
||
5. **Tag Deletion at Scale**
|
||
- **Limitation:** Deleting a tag requires querying all associations and batch deleting
|
||
- **Mitigation:** Use BatchWriteItem (up to 25 items per batch) with pagination
|
||
- **Performance:** Deleting a tag from 1,000 items takes ~2-3 seconds
|
||
- **UX:** Show progress indicator for tag deletion operations
|
||
- **Validates Requirement 11.4**
|
||
|
||
**Alternative Database Considerations:**
|
||
|
||
We evaluated several alternatives to DynamoDB:
|
||
|
||
1. **PostgreSQL (RDS or Aurora)**
|
||
- **Pros:** Rich query capabilities, JSONB support, full-text search, complex joins
|
||
- **Cons:** Requires capacity planning, vertical scaling limits, higher operational overhead
|
||
- **Cost:** More expensive at scale (~$100-200/month for comparable performance)
|
||
- **Verdict:** Overkill for this application; DynamoDB's simplicity is preferred
|
||
|
||
2. **MongoDB (Atlas)**
|
||
- **Pros:** Flexible schema, rich query language, aggregation pipeline
|
||
- **Cons:** Requires capacity planning, more expensive, additional operational complexity
|
||
- **Cost:** ~$60-100/month for comparable scale
|
||
- **Verdict:** Similar capabilities to DynamoDB but higher cost and complexity
|
||
|
||
3. **Redis (ElastiCache)**
|
||
- **Pros:** Extremely fast, simple data structures, pub/sub support
|
||
- **Cons:** In-memory only (expensive at scale), no complex queries, requires persistence strategy
|
||
- **Cost:** Prohibitively expensive for 10,000+ items per user
|
||
- **Verdict:** Better suited as a cache layer, not primary storage
|
||
|
||
4. **Firestore**
|
||
- **Pros:** Real-time updates, offline support, simple queries
|
||
- **Cons:** Limited query capabilities, expensive at scale, vendor lock-in
|
||
- **Cost:** ~$50-80/month for comparable scale
|
||
- **Verdict:** Good for real-time apps, but DynamoDB is more cost-effective
|
||
|
||
**Final Verdict:**
|
||
|
||
DynamoDB is the optimal choice for this application. The scale requirements (10,000+ bookmarks/notes per user), tag-based filtering, concurrent update handling, and cost efficiency align perfectly with DynamoDB's strengths. The limitations are manageable with proper schema design and application-level logic. The cost analysis shows DynamoDB is highly cost-effective even at scale.
|
||
|
||
**Validates Requirements 11.1, 11.2, 11.3, 11.4, 11.5, 11.8, 11.9**
|
||
|
||
### DynamoDB Schema Design
|
||
|
||
#### Access Patterns
|
||
|
||
The schema is designed to support these access patterns efficiently:
|
||
|
||
1. Get all pages for a user
|
||
2. Get all widgets for a page
|
||
3. Get all bookmarks for a widget
|
||
4. Get all notes for a widget
|
||
5. Get all bookmarks/notes with a specific tag
|
||
6. Get all tags for a user
|
||
7. Get all items in a group
|
||
8. Get shared item by share ID
|
||
9. Update tags on a bookmark/note (concurrent-safe)
|
||
10. Delete a tag and remove from all items
|
||
|
||
|
||
#### Table Schemas
|
||
|
||
This section defines the complete data model and indexing strategy, addressing Requirements 11.6 and 11.7.
|
||
|
||
**Users Table**
|
||
```
|
||
Primary Key: user_id (String, Partition Key)
|
||
|
||
Attributes:
|
||
- user_id: String (UUID)
|
||
- email: String
|
||
- oauth_provider: String
|
||
- oauth_id: String
|
||
- created_at: Number (Unix timestamp)
|
||
- updated_at: Number (Unix timestamp)
|
||
|
||
Indexes: None (queries always by user_id)
|
||
```
|
||
|
||
**Pages Table**
|
||
```
|
||
Primary Key:
|
||
- user_id (String, Partition Key)
|
||
- page_id (String, Sort Key)
|
||
|
||
Attributes:
|
||
- user_id: String (UUID)
|
||
- page_id: String (UUID)
|
||
- name: String
|
||
- order: Number
|
||
- created_at: Number
|
||
- updated_at: Number
|
||
|
||
Indexes: None (queries always by user_id)
|
||
Access Pattern: Get all pages for a user (Query by user_id)
|
||
```
|
||
|
||
**Widgets Table**
|
||
```
|
||
Primary Key:
|
||
- page_id (String, Partition Key)
|
||
- widget_id (String, Sort Key)
|
||
|
||
Attributes:
|
||
- page_id: String (UUID)
|
||
- widget_id: String (UUID)
|
||
- user_id: String (for authorization)
|
||
- type: String (enum: bookmark, notes, weather)
|
||
- title: String (optional custom title)
|
||
- position: Map {x: Number, y: Number}
|
||
- size: Map {width: Number, height: Number}
|
||
- config: Map (widget-specific configuration)
|
||
- created_at: Number
|
||
- updated_at: Number
|
||
|
||
Indexes: None (queries always by page_id)
|
||
Access Pattern: Get all widgets for a page (Query by page_id)
|
||
```
|
||
|
||
**Bookmarks Table**
|
||
```
|
||
Primary Key:
|
||
- widget_id (String, Partition Key)
|
||
- bookmark_id (String, Sort Key)
|
||
|
||
GSI: UserBookmarksIndex
|
||
- user_id (Partition Key)
|
||
- created_at (Sort Key)
|
||
- Projected Attributes: ALL
|
||
|
||
Attributes:
|
||
- widget_id: String (UUID)
|
||
- bookmark_id: String (UUID)
|
||
- user_id: String (for cross-widget queries)
|
||
- title: String
|
||
- url: String
|
||
- group_id: String (optional, for grouping)
|
||
- order: Number (within widget or group)
|
||
- favicon_url: String (optional)
|
||
- created_at: Number
|
||
- updated_at: Number
|
||
- version: Number (for optimistic locking)
|
||
|
||
Access Patterns:
|
||
1. Get all bookmarks for a widget (Query by widget_id)
|
||
2. Get all bookmarks for a user (Query UserBookmarksIndex by user_id)
|
||
3. Update bookmark with version check (UpdateItem with ConditionExpression)
|
||
|
||
Indexing Strategy:
|
||
- Primary key enables efficient widget-scoped queries
|
||
- UserBookmarksIndex enables cross-widget queries (for tag filtering)
|
||
- Version field enables optimistic locking for concurrent updates
|
||
```
|
||
|
||
**Notes Table**
|
||
```
|
||
Primary Key:
|
||
- widget_id (String, Partition Key)
|
||
- note_id (String, Sort Key)
|
||
|
||
GSI: UserNotesIndex
|
||
- user_id (Partition Key)
|
||
- created_at (Sort Key)
|
||
- Projected Attributes: ALL
|
||
|
||
Attributes:
|
||
- widget_id: String (UUID)
|
||
- note_id: String (UUID)
|
||
- user_id: String (for cross-widget queries)
|
||
- title: String (optional)
|
||
- content: String (the actual note content)
|
||
- content_location: String (enum: "dynamodb", "s3://bucket/key")
|
||
- format: String (enum: plain, rtf, code, yaml, markdown)
|
||
- language: String (for code format, e.g., "javascript", "python")
|
||
- created_at: Number
|
||
- updated_at: Number
|
||
- version: Number (for optimistic locking)
|
||
|
||
Access Patterns:
|
||
1. Get note for a widget (Query by widget_id)
|
||
2. Get all notes for a user (Query UserNotesIndex by user_id)
|
||
3. Update note with version check (UpdateItem with ConditionExpression)
|
||
|
||
Indexing Strategy:
|
||
- Primary key enables efficient widget-scoped queries
|
||
- UserNotesIndex enables cross-widget queries (for tag filtering)
|
||
- Version field enables optimistic locking for concurrent updates
|
||
- content_location field enables S3 offloading for large notes (>100KB)
|
||
```
|
||
|
||
**TagAssociations Table**
|
||
```
|
||
Primary Key:
|
||
- item_id (String, Partition Key) - bookmark_id or note_id
|
||
- tag_name (String, Sort Key)
|
||
|
||
GSI: TagItemsIndex
|
||
- tag_name (Partition Key)
|
||
- user_id (Sort Key)
|
||
- Projected Attributes: item_id, item_type, user_id, created_at
|
||
|
||
Attributes:
|
||
- item_id: String (UUID)
|
||
- tag_name: String (lowercase, normalized)
|
||
- user_id: String
|
||
- item_type: String (enum: bookmark, note)
|
||
- created_at: Number
|
||
|
||
Access Patterns:
|
||
1. Get all tags for an item (Query by item_id)
|
||
2. Get all items with a specific tag (Query TagItemsIndex by tag_name + user_id)
|
||
3. Add tag to item (PutItem in transaction with item version check)
|
||
4. Remove tag from item (DeleteItem)
|
||
5. Delete tag from all user's items (Query TagItemsIndex, BatchWriteItem to delete)
|
||
|
||
Indexing Strategy (Addresses Requirement 11.6):
|
||
- Primary key (item_id, tag_name) enables efficient tag lookup for an item
|
||
- TagItemsIndex (tag_name, user_id) enables efficient item lookup by tag
|
||
- Sparse index (only items with tags are indexed)
|
||
- Projected attributes minimize read costs (only essential fields)
|
||
- Composite sort key (user_id) enables user-scoped tag queries
|
||
|
||
Multi-Tag Query Strategy (Addresses Requirement 11.7):
|
||
- For single tag: Query TagItemsIndex by tag_name + user_id
|
||
- For multiple tags (AND logic):
|
||
1. Query TagItemsIndex for each tag
|
||
2. Compute intersection of item_ids in application code
|
||
3. BatchGetItem to retrieve full items
|
||
- For multiple tags (OR logic):
|
||
1. Query TagItemsIndex for each tag
|
||
2. Compute union of item_ids in application code
|
||
3. BatchGetItem to retrieve full items
|
||
- Performance: O(m * log n + k) where m = number of tags, n = items per tag, k = result size
|
||
```
|
||
|
||
**Groups Table**
|
||
```
|
||
Primary Key:
|
||
- widget_id (String, Partition Key)
|
||
- group_id (String, Sort Key)
|
||
|
||
Attributes:
|
||
- widget_id: String (UUID)
|
||
- group_id: String (UUID)
|
||
- user_id: String
|
||
- name: String
|
||
- order: Number
|
||
- created_at: Number
|
||
- updated_at: Number
|
||
|
||
Indexes: None (queries always by widget_id)
|
||
Access Pattern: Get all groups for a widget (Query by widget_id)
|
||
```
|
||
|
||
**SharedItems Table**
|
||
```
|
||
Primary Key: share_id (String, Partition Key)
|
||
|
||
GSI: UserSharesIndex
|
||
- user_id (Partition Key)
|
||
- created_at (Sort Key)
|
||
- Projected Attributes: share_id, item_id, item_type, created_at, access_count
|
||
|
||
Attributes:
|
||
- share_id: String (UUID or short code)
|
||
- user_id: String (owner)
|
||
- item_id: String (bookmark_id or note_id)
|
||
- item_type: String (enum: bookmark, note)
|
||
- created_at: Number
|
||
- expires_at: Number (optional)
|
||
- access_count: Number
|
||
|
||
Access Patterns:
|
||
1. Get shared item by share_id (GetItem by share_id)
|
||
2. Get all shares for a user (Query UserSharesIndex by user_id)
|
||
3. Increment access count (UpdateItem with atomic counter)
|
||
|
||
Indexes: None (queries by share_id or UserSharesIndex)
|
||
```
|
||
|
||
**Preferences Table**
|
||
```
|
||
Primary Key: user_id (String, Partition Key)
|
||
|
||
Attributes:
|
||
- user_id: String (UUID)
|
||
- search_provider: String (default: "google")
|
||
- theme: String (optional)
|
||
- updated_at: Number
|
||
|
||
Indexes: None (queries always by user_id)
|
||
Access Pattern: Get/update preferences for a user (GetItem/UpdateItem by user_id)
|
||
```
|
||
|
||
**Validates Requirements 11.6, 11.7**
|
||
|
||
|
||
### Query Patterns and Performance
|
||
|
||
**Pattern 1: Get all bookmarks with tag "work"**
|
||
```
|
||
Query TagItemsIndex:
|
||
PK = "work"
|
||
SK begins_with user_id
|
||
Filter: item_type = "bookmark"
|
||
|
||
Then BatchGetItem on Bookmarks table with returned item_ids
|
||
```
|
||
Performance: O(log n) for index query + O(1) per item for batch get
|
||
Cost: 1 RCU per 4KB of index data + 1 RCU per 4KB per item
|
||
|
||
**Pattern 2: Get all bookmarks for a widget**
|
||
```
|
||
Query Bookmarks table:
|
||
PK = widget_id
|
||
Sort by order
|
||
```
|
||
Performance: O(log n + k) where k is number of bookmarks
|
||
Cost: 1 RCU per 4KB of data
|
||
|
||
**Pattern 3: Add tag to bookmark (concurrent-safe)**
|
||
```
|
||
1. GetItem on Bookmarks table (get current version)
|
||
2. TransactWriteItems:
|
||
- UpdateItem Bookmarks (increment version, conditional on current version)
|
||
- PutItem TagAssociations (create association)
|
||
```
|
||
Performance: O(1) for both operations
|
||
Cost: 2 WCU for transaction
|
||
|
||
**Pattern 4: Delete tag from all items**
|
||
```
|
||
1. Query TagItemsIndex (get all items with tag)
|
||
2. BatchWriteItem to delete all TagAssociations
|
||
3. (Optional) Update item metadata if needed
|
||
```
|
||
Performance: O(log n + k) where k is items with tag
|
||
Cost: 1 WCU per item deleted
|
||
|
||
**Pattern 5: Filter bookmarks by multiple tags (AND logic)**
|
||
```
|
||
1. Query TagItemsIndex for each tag
|
||
2. Compute intersection of item_ids in application code
|
||
3. BatchGetItem for final item list
|
||
```
|
||
Performance: O(m * log n + k) where m is number of tags, k is result size
|
||
Cost: 1 RCU per 4KB per tag query + 1 RCU per 4KB per item
|
||
|
||
**Optimization: Tag Caching**
|
||
For frequently accessed tags, cache the item_id lists in Redis with TTL. This reduces DynamoDB read costs for popular tags.
|
||
|
||
### Concurrent Update Handling
|
||
|
||
**Optimistic Locking Pattern:**
|
||
```
|
||
1. Read item with current version number
|
||
2. User makes changes in UI
|
||
3. Write with condition: version = expected_version
|
||
4. If condition fails (version changed), retry:
|
||
- Fetch latest version
|
||
- Merge changes if possible
|
||
- Prompt user if conflicts exist
|
||
```
|
||
|
||
**Tag Update Race Conditions:**
|
||
When two users/sessions update tags on the same item simultaneously:
|
||
- Each update increments version number
|
||
- Second update fails condition check
|
||
- Application retries with latest version
|
||
- Both tag changes are preserved
|
||
|
||
**Implementation:**
|
||
```typescript
|
||
async function addTagToBookmark(bookmarkId: string, tagName: string) {
|
||
const bookmark = await getBookmark(bookmarkId);
|
||
|
||
await dynamodb.transactWrite({
|
||
TransactItems: [
|
||
{
|
||
Update: {
|
||
TableName: 'Bookmarks',
|
||
Key: { widget_id: bookmark.widget_id, bookmark_id: bookmarkId },
|
||
UpdateExpression: 'SET version = version + :inc',
|
||
ConditionExpression: 'version = :expected',
|
||
ExpressionAttributeValues: {
|
||
':inc': 1,
|
||
':expected': bookmark.version
|
||
}
|
||
}
|
||
},
|
||
{
|
||
Put: {
|
||
TableName: 'TagAssociations',
|
||
Item: {
|
||
item_id: bookmarkId,
|
||
tag_name: tagName.toLowerCase(),
|
||
user_id: bookmark.user_id,
|
||
item_type: 'bookmark',
|
||
created_at: Date.now()
|
||
}
|
||
}
|
||
}
|
||
]
|
||
});
|
||
}
|
||
```
|
||
|
||
### Cost Analysis
|
||
|
||
**Assumptions:**
|
||
- 10,000 bookmarks per user
|
||
- 50 tags per user
|
||
- Average 3 tags per bookmark
|
||
- 100 bookmark views per day
|
||
- 10 bookmark updates per day
|
||
- 5 tag filter queries per day
|
||
|
||
**Monthly Costs (On-Demand Pricing):**
|
||
|
||
Storage:
|
||
- 10,000 bookmarks × 1KB = 10MB
|
||
- 30,000 tag associations × 0.1KB = 3MB
|
||
- Total: ~13MB × $0.25/GB = $0.003/month
|
||
|
||
Reads:
|
||
- 100 views/day × 30 days = 3,000 reads
|
||
- 5 tag queries/day × 30 days × 2 tables = 300 reads
|
||
- Total: 3,300 RCU × $0.25/million = $0.0008/month
|
||
|
||
Writes:
|
||
- 10 updates/day × 30 days = 300 writes
|
||
- Total: 300 WCU × $1.25/million = $0.0004/month
|
||
|
||
**Total per user: ~$0.004/month** (less than half a cent)
|
||
|
||
For 10,000 users: ~$40/month
|
||
|
||
This is highly cost-effective. Even with 10x the activity, costs remain under $1/user/year.
|
||
|
||
|
||
## Components and Interfaces
|
||
|
||
### Frontend Architecture (HTMX-Based)
|
||
|
||
The frontend uses server-rendered HTML with HTMX attributes for dynamic interactions. Instead of React components, we have HTML templates and HTMX-enhanced elements.
|
||
|
||
#### HTML Templates (Go html/template)
|
||
|
||
**Base Layout Template**
|
||
- Main HTML structure with head, body, and scripts
|
||
- Includes HTMX library
|
||
- Includes CSS (Tailwind or custom)
|
||
- Includes minimal JavaScript libraries (Sortable.js, Prism.js)
|
||
- Provides slots for page-specific content
|
||
|
||
**Dashboard Template**
|
||
- Renders toolbar with search bar, page tabs, and user menu
|
||
- Renders widget grid for current page
|
||
- Uses HTMX attributes for dynamic updates
|
||
- Includes drag-and-drop initialization
|
||
|
||
**Login Template**
|
||
- Displays OAuth provider buttons
|
||
- Each button links to OAuth initiation endpoint
|
||
- Simple, clean design
|
||
|
||
**Widget Templates (Partials)**
|
||
- Bookmark widget template
|
||
- Notes widget template
|
||
- Weather widget template
|
||
- Each template includes HTMX attributes for updates
|
||
|
||
#### HTMX Interaction Patterns
|
||
|
||
**Page Switching:**
|
||
```html
|
||
<button hx-get="/pages/{pageId}"
|
||
hx-target="#widget-grid"
|
||
hx-swap="innerHTML">
|
||
Page Name
|
||
</button>
|
||
```
|
||
|
||
**Add Bookmark:**
|
||
```html
|
||
<form hx-post="/widgets/{widgetId}/bookmarks"
|
||
hx-target="#bookmark-list"
|
||
hx-swap="beforeend">
|
||
<input name="title" />
|
||
<input name="url" />
|
||
<button type="submit">Add</button>
|
||
</form>
|
||
```
|
||
|
||
**Delete Widget:**
|
||
```html
|
||
<button hx-delete="/widgets/{widgetId}"
|
||
hx-target="closest .widget"
|
||
hx-swap="outerHTML swap:1s">
|
||
Delete
|
||
</button>
|
||
```
|
||
|
||
**Tag Filter:**
|
||
```html
|
||
<select hx-get="/bookmarks/filter"
|
||
hx-target="#bookmark-list"
|
||
hx-include="[name='tags']"
|
||
multiple>
|
||
<option value="work">work</option>
|
||
<option value="personal">personal</option>
|
||
</select>
|
||
```
|
||
|
||
**Auto-save Notes:**
|
||
```html
|
||
<textarea hx-post="/widgets/{widgetId}/note"
|
||
hx-trigger="keyup changed delay:500ms"
|
||
hx-target="#save-status">
|
||
Note content
|
||
</textarea>
|
||
```
|
||
|
||
#### Client-Side JavaScript (Minimal)
|
||
|
||
**Sortable.js for Drag-and-Drop:**
|
||
- Initialize sortable on widget grid
|
||
- Initialize sortable on bookmark lists
|
||
- Send reorder requests via HTMX after drop
|
||
|
||
**Prism.js for Syntax Highlighting:**
|
||
- Automatically highlight code blocks
|
||
- Support multiple languages
|
||
- Apply on page load and after HTMX swaps
|
||
|
||
**CodeMirror (Optional):**
|
||
- Rich code editor for notes in code mode
|
||
- Syntax highlighting and line numbers
|
||
- Auto-save integration with HTMX
|
||
|
||
**Alpine.js (Optional):**
|
||
- Minor client-side state (dropdowns, modals)
|
||
- Tag autocomplete
|
||
- Format selector for notes
|
||
|
||
|
||
### Backend Services (Go)
|
||
|
||
All backend services are implemented in Go, with clear separation of concerns.
|
||
|
||
#### 1. Authentication Service
|
||
|
||
**Interface:**
|
||
```go
|
||
type AuthService interface {
|
||
// InitiateOAuth starts OAuth flow and returns redirect URL
|
||
InitiateOAuth(provider string) (redirectURL string, err error)
|
||
|
||
// HandleOAuthCallback processes OAuth callback and creates session
|
||
HandleOAuthCallback(provider string, code string) (user *User, sessionToken string, err error)
|
||
|
||
// ValidateSession validates session token and returns user
|
||
ValidateSession(token string) (*User, error)
|
||
|
||
// Logout invalidates session token
|
||
Logout(token string) error
|
||
}
|
||
```
|
||
|
||
**Implementation Notes:**
|
||
- Use golang.org/x/oauth2 for OAuth flows
|
||
- Store session tokens in cookies (httponly, secure)
|
||
- Use gorilla/sessions or similar for session management
|
||
- Support Google OAuth initially, extensible for others
|
||
|
||
#### 2. Page Service
|
||
|
||
**Interface:**
|
||
```go
|
||
type PageService interface {
|
||
// GetPages retrieves all pages for a user in display order
|
||
GetPages(userID string) ([]*Page, error)
|
||
|
||
// CreatePage creates a new page
|
||
CreatePage(userID string, name string) (*Page, error)
|
||
|
||
// UpdatePage updates page name or order
|
||
UpdatePage(pageID string, updates *PageUpdate) (*Page, error)
|
||
|
||
// DeletePage deletes page and all associated widgets
|
||
DeletePage(pageID string) error
|
||
|
||
// ReorderPages updates page display order
|
||
ReorderPages(userID string, pageOrder []string) error
|
||
}
|
||
```
|
||
|
||
#### 3. Widget Service
|
||
|
||
**Interface:**
|
||
```go
|
||
type WidgetService interface {
|
||
// GetWidgets retrieves all widgets for a page
|
||
GetWidgets(pageID string) ([]*Widget, error)
|
||
|
||
// CreateWidget creates new widget of specified type
|
||
CreateWidget(pageID string, widgetType WidgetType, config map[string]interface{}) (*Widget, error)
|
||
|
||
// UpdateWidget updates widget configuration, position, or content
|
||
UpdateWidget(widgetID string, updates *WidgetUpdate) (*Widget, error)
|
||
|
||
// DeleteWidget deletes widget and associated data
|
||
DeleteWidget(widgetID string) error
|
||
}
|
||
```
|
||
|
||
#### 4. Bookmark Service
|
||
|
||
**Interface:**
|
||
```go
|
||
type BookmarkService interface {
|
||
// GetBookmarks retrieves all bookmarks for a widget
|
||
GetBookmarks(widgetID string) ([]*Bookmark, error)
|
||
|
||
// CreateBookmark creates new bookmark
|
||
CreateBookmark(widgetID string, bookmark *BookmarkInput) (*Bookmark, error)
|
||
|
||
// UpdateBookmark updates bookmark properties (with optimistic locking)
|
||
UpdateBookmark(bookmarkID string, updates *BookmarkUpdate) (*Bookmark, error)
|
||
|
||
// DeleteBookmark deletes bookmark and tag associations
|
||
DeleteBookmark(bookmarkID string) error
|
||
|
||
// ReorderBookmarks updates bookmark order
|
||
ReorderBookmarks(widgetID string, bookmarkOrder []string) error
|
||
|
||
// GetBookmarksByTags queries bookmarks by tags
|
||
GetBookmarksByTags(userID string, tags []string, logic FilterLogic) ([]*Bookmark, error)
|
||
}
|
||
```
|
||
|
||
#### 5. Notes Service
|
||
|
||
**Interface:**
|
||
```go
|
||
type NotesService interface {
|
||
// GetNote retrieves note for a widget
|
||
GetNote(widgetID string) (*Note, error)
|
||
|
||
// UpdateNote updates note content, format, or tags (with optimistic locking)
|
||
UpdateNote(widgetID string, updates *NoteUpdate) (*Note, error)
|
||
|
||
// GetNotesByTags queries notes by tags
|
||
GetNotesByTags(userID string, tags []string, logic FilterLogic) ([]*Note, error)
|
||
}
|
||
```
|
||
|
||
#### 6. Tag Service
|
||
|
||
**Interface:**
|
||
```go
|
||
type TagService interface {
|
||
// GetTags retrieves all tags for a user with usage counts
|
||
GetTags(userID string) ([]*TagInfo, error)
|
||
|
||
// AddTagToItem creates tag association (uses transaction)
|
||
AddTagToItem(itemID string, itemType ItemType, tagName string) error
|
||
|
||
// RemoveTagFromItem removes tag association
|
||
RemoveTagFromItem(itemID string, tagName string) error
|
||
|
||
// DeleteTag removes tag from all user's items
|
||
DeleteTag(userID string, tagName string) (affectedCount int, err error)
|
||
|
||
// RenameTag updates all tag associations
|
||
RenameTag(userID string, oldName string, newName string) error
|
||
|
||
// GetTagSuggestions returns tags matching prefix for autocomplete
|
||
GetTagSuggestions(userID string, prefix string) ([]string, error)
|
||
}
|
||
```
|
||
|
||
#### 7. Group Service
|
||
|
||
**Interface:**
|
||
```go
|
||
type GroupService interface {
|
||
// GetGroups retrieves all groups for a bookmark widget
|
||
GetGroups(widgetID string) ([]*Group, error)
|
||
|
||
// CreateGroup creates new group
|
||
CreateGroup(widgetID string, name string) (*Group, error)
|
||
|
||
// UpdateGroup updates group name or order
|
||
UpdateGroup(groupID string, updates *GroupUpdate) (*Group, error)
|
||
|
||
// DeleteGroup deletes group (moves bookmarks to specified destination)
|
||
DeleteGroup(groupID string, moveToGroupID *string) error
|
||
|
||
// MoveBookmarkToGroup updates bookmark's group assignment
|
||
MoveBookmarkToGroup(bookmarkID string, groupID *string) error
|
||
|
||
// ReorderGroups updates group display order
|
||
ReorderGroups(widgetID string, groupOrder []string) error
|
||
}
|
||
```
|
||
|
||
**Implementation Notes:**
|
||
- Groups are widget-scoped (each bookmark widget has its own groups)
|
||
- Bookmarks can be ungrouped (group_id is optional)
|
||
- Deleting a group requires specifying where to move bookmarks (another group or ungrouped)
|
||
- Group names can be duplicated within a widget (user's choice)
|
||
- Groups have an order field for display ordering
|
||
|
||
**Validates Requirements 4.11, 4.12**
|
||
|
||
#### 8. Sharing Service
|
||
|
||
**Interface:**
|
||
```go
|
||
type SharingService interface {
|
||
// CreateShare creates shareable link
|
||
CreateShare(userID string, itemID string, itemType ItemType, options *ShareOptions) (*ShareLink, error)
|
||
|
||
// GetSharedItem retrieves shared item by share_id (increments access count)
|
||
GetSharedItem(shareID string) (*SharedItem, error)
|
||
|
||
// RevokeShare deletes share link
|
||
RevokeShare(shareID string) error
|
||
|
||
// GetUserShares lists all shares created by user
|
||
GetUserShares(userID string) ([]*ShareInfo, error)
|
||
}
|
||
|
||
type ShareOptions struct {
|
||
ExpiresAt *time.Time // Optional expiration
|
||
}
|
||
|
||
type ShareLink struct {
|
||
ShareID string
|
||
URL string // Full shareable URL (e.g., https://app.com/share/abc123)
|
||
ExpiresAt *time.Time
|
||
}
|
||
|
||
type SharedItem struct {
|
||
ItemType ItemType
|
||
Data interface{} // Bookmark or Note
|
||
OwnerEmail string // Optional owner info
|
||
CreatedAt time.Time
|
||
}
|
||
```
|
||
|
||
**Implementation Notes:**
|
||
- Share IDs can be UUIDs or short codes (e.g., 8-character alphanumeric)
|
||
- Shared items are read-only (no editing by recipients)
|
||
- Access count is incremented atomically on each view
|
||
- Expired shares return 404 or "Share expired" message
|
||
- Shared notes preserve formatting (format field included in response)
|
||
- Shared bookmarks include title, URL, and optional favicon
|
||
- No authentication required to view shared items (public links)
|
||
|
||
**Security Considerations:**
|
||
- Share IDs should be cryptographically random (prevent guessing)
|
||
- Rate limiting on share access to prevent abuse
|
||
- Optional expiration for time-limited sharing
|
||
- Owner can revoke shares at any time
|
||
- Shared content is sanitized to prevent XSS
|
||
|
||
**Validates Requirements 4.16, 5.17**
|
||
|
||
#### 9. Storage Service (DynamoDB)
|
||
|
||
**Interface:**
|
||
```go
|
||
type StorageService interface {
|
||
// DynamoDB operations wrapper
|
||
GetItem(tableName string, key map[string]interface{}) (map[string]interface{}, error)
|
||
PutItem(tableName string, item map[string]interface{}) error
|
||
UpdateItem(tableName string, key map[string]interface{}, updates map[string]interface{}) error
|
||
DeleteItem(tableName string, key map[string]interface{}) error
|
||
Query(tableName string, keyCondition string, params map[string]interface{}) ([]map[string]interface{}, error)
|
||
TransactWrite(items []*TransactWriteItem) error
|
||
BatchGet(requests []*BatchGetRequest) ([]map[string]interface{}, error)
|
||
}
|
||
```
|
||
|
||
**Implementation Notes:**
|
||
- Use aws-sdk-go-v2 for DynamoDB operations
|
||
- Implement retry logic with exponential backoff
|
||
- Handle conditional writes for optimistic locking
|
||
- Use transactions for multi-item consistency
|
||
|
||
#### 10. Weather Service (Python or Go)
|
||
|
||
**Interface:**
|
||
```go
|
||
type WeatherService interface {
|
||
// GetWeather fetches current weather from external API
|
||
GetWeather(location string) (*WeatherData, error)
|
||
|
||
// ValidateLocation validates location string
|
||
ValidateLocation(location string) (bool, error)
|
||
}
|
||
```
|
||
|
||
**Implementation Notes:**
|
||
- Can be implemented in Go using net/http
|
||
- Use Python if complex parsing or ML needed
|
||
- Cache results for rate limiting
|
||
- Handle API errors gracefully
|
||
|
||
|
||
### HTTP Endpoints (HTMX-Friendly)
|
||
|
||
All endpoints return HTML fragments or full pages (not JSON), designed for HTMX consumption.
|
||
|
||
**Authentication:**
|
||
- `GET /login` - Display login page
|
||
- `GET /auth/oauth/:provider` - Initiate OAuth flow
|
||
- `GET /auth/callback/:provider` - OAuth callback handler
|
||
- `POST /logout` - Logout user (returns redirect)
|
||
- `GET /auth/me` - Get current user info (for debugging)
|
||
|
||
**Pages:**
|
||
- `GET /dashboard` - Main dashboard page (full page)
|
||
- `GET /pages/:id` - Get widgets for page (HTML fragment for widget grid)
|
||
- `POST /pages` - Create new page (returns updated page tabs HTML)
|
||
- `PUT /pages/:id` - Update page (returns updated page tab HTML)
|
||
- `DELETE /pages/:id` - Delete page (returns updated page tabs HTML)
|
||
- `POST /pages/reorder` - Reorder pages (returns updated page tabs HTML)
|
||
|
||
**Widgets:**
|
||
- `GET /widgets/:id` - Get single widget HTML
|
||
- `POST /pages/:pageId/widgets` - Create widget (returns widget HTML)
|
||
- `PUT /widgets/:id` - Update widget (returns updated widget HTML)
|
||
- `DELETE /widgets/:id` - Delete widget (returns empty or success message)
|
||
- `POST /widgets/:id/position` - Update widget position (returns success)
|
||
|
||
**Bookmarks:**
|
||
- `GET /widgets/:widgetId/bookmarks` - Get bookmarks HTML for widget
|
||
- `POST /widgets/:widgetId/bookmarks` - Create bookmark (returns bookmark HTML)
|
||
- `PUT /bookmarks/:id` - Update bookmark (returns updated bookmark HTML)
|
||
- `DELETE /bookmarks/:id` - Delete bookmark (returns empty)
|
||
- `POST /bookmarks/reorder` - Reorder bookmarks (returns success)
|
||
- `GET /bookmarks/filter?tags=tag1,tag2&logic=and` - Filter by tags (returns filtered bookmarks HTML)
|
||
|
||
**Notes:**
|
||
- `GET /widgets/:widgetId/note` - Get note HTML for widget
|
||
- `POST /widgets/:widgetId/note` - Update note (returns save status HTML)
|
||
- `GET /notes/filter?tags=tag1,tag2&logic=and` - Filter by tags (returns filtered notes HTML)
|
||
|
||
**Tags:**
|
||
- `GET /tags` - Get all user tags (returns tag list HTML)
|
||
- `POST /items/:itemId/tags` - Add tag to item (returns updated tag list HTML)
|
||
- `DELETE /items/:itemId/tags/:tagName` - Remove tag (returns updated tag list HTML)
|
||
- `DELETE /tags/:tagName` - Delete tag from all items (returns success message)
|
||
- `PUT /tags/:oldName/rename` - Rename tag (returns success)
|
||
- `GET /tags/suggest?prefix=:prefix` - Get tag suggestions (returns datalist HTML)
|
||
|
||
**Groups:**
|
||
- `GET /widgets/:widgetId/groups` - Get groups HTML for widget
|
||
- `POST /widgets/:widgetId/groups` - Create group (returns group HTML)
|
||
- `PUT /groups/:id` - Update group (returns updated group HTML)
|
||
- `DELETE /groups/:id` - Delete group (returns success)
|
||
- `POST /bookmarks/:id/move` - Move bookmark to group (returns updated bookmark HTML)
|
||
|
||
**Sharing:**
|
||
- `POST /share` - Create share link (returns share dialog HTML with link)
|
||
- `GET /share/:shareId` - Get shared item (public, full page)
|
||
- `DELETE /share/:shareId` - Revoke share (returns success)
|
||
- `GET /shares` - Get user's shares (returns shares list HTML)
|
||
|
||
**Preferences:**
|
||
- `GET /preferences` - Get preferences form (returns form HTML)
|
||
- `POST /preferences` - Update preferences (returns success message)
|
||
|
||
**Weather:**
|
||
- `GET /weather?location=:location` - Get weather HTML for location
|
||
|
||
**Response Format:**
|
||
- All endpoints return HTML (full pages or fragments)
|
||
- Success responses return the updated HTML to swap
|
||
- Error responses return error message HTML with appropriate styling
|
||
- Use HTTP status codes: 200 (success), 400 (validation error), 401 (unauthorized), 404 (not found), 500 (server error)
|
||
|
||
|
||
## Data Models
|
||
|
||
### User Model
|
||
```typescript
|
||
User {
|
||
id: string; // UUID
|
||
email: string;
|
||
oauth_provider: string;
|
||
oauth_id: string;
|
||
created_at: number; // Unix timestamp
|
||
updated_at: number;
|
||
}
|
||
```
|
||
|
||
### Page Model
|
||
```typescript
|
||
Page {
|
||
id: string; // UUID
|
||
user_id: string;
|
||
name: string;
|
||
order: number;
|
||
created_at: number;
|
||
updated_at: number;
|
||
}
|
||
```
|
||
|
||
### Widget Model
|
||
```typescript
|
||
Widget {
|
||
id: string; // UUID
|
||
page_id: string;
|
||
user_id: string;
|
||
type: WidgetType; // 'bookmark' | 'notes' | 'weather'
|
||
title?: string; // Optional custom title
|
||
position: Position;
|
||
size: Size;
|
||
config: WidgetConfig; // Type-specific configuration
|
||
created_at: number;
|
||
updated_at: number;
|
||
}
|
||
|
||
Position {
|
||
x: number;
|
||
y: number;
|
||
}
|
||
|
||
Size {
|
||
width: number;
|
||
height: number;
|
||
}
|
||
```
|
||
|
||
### Bookmark Model
|
||
```typescript
|
||
Bookmark {
|
||
id: string; // UUID
|
||
widget_id: string;
|
||
user_id: string;
|
||
title: string;
|
||
url: string;
|
||
group_id?: string; // Optional group assignment
|
||
order: number; // Order within widget or group
|
||
favicon_url?: string;
|
||
created_at: number;
|
||
updated_at: number;
|
||
version: number; // For optimistic locking
|
||
}
|
||
```
|
||
|
||
### Note Model
|
||
```typescript
|
||
Note {
|
||
id: string; // UUID
|
||
widget_id: string;
|
||
user_id: string;
|
||
title?: string;
|
||
content: string;
|
||
content_location: string; // 'dynamodb' | 's3://bucket/key'
|
||
format: ContentFormat; // 'plain' | 'rtf' | 'code' | 'yaml' | 'markdown'
|
||
language?: string; // For code format (e.g., 'javascript', 'python')
|
||
created_at: number;
|
||
updated_at: number;
|
||
version: number; // For optimistic locking
|
||
}
|
||
|
||
ContentFormat = 'plain' | 'rtf' | 'code' | 'yaml' | 'markdown';
|
||
```
|
||
|
||
### Tag Association Model
|
||
```typescript
|
||
TagAssociation {
|
||
item_id: string; // bookmark_id or note_id
|
||
tag_name: string; // Normalized (lowercase, trimmed)
|
||
user_id: string;
|
||
item_type: ItemType; // 'bookmark' | 'note'
|
||
created_at: number;
|
||
}
|
||
|
||
ItemType = 'bookmark' | 'note';
|
||
```
|
||
|
||
### Tag Info Model
|
||
```typescript
|
||
TagInfo {
|
||
name: string;
|
||
count: number; // Number of items with this tag
|
||
item_types: ItemType[]; // Which types use this tag
|
||
}
|
||
```
|
||
|
||
### Group Model
|
||
```typescript
|
||
Group {
|
||
id: string; // UUID
|
||
widget_id: string;
|
||
user_id: string;
|
||
name: string;
|
||
order: number;
|
||
created_at: number;
|
||
updated_at: number;
|
||
}
|
||
```
|
||
|
||
### Share Model
|
||
```typescript
|
||
Share {
|
||
id: string; // UUID or short code
|
||
user_id: string; // Owner
|
||
item_id: string;
|
||
item_type: ItemType;
|
||
created_at: number;
|
||
expires_at?: number; // Optional expiration
|
||
access_count: number;
|
||
}
|
||
|
||
ShareLink {
|
||
share_id: string;
|
||
url: string; // Full shareable URL
|
||
expires_at?: number;
|
||
}
|
||
|
||
SharedItem {
|
||
item_type: ItemType;
|
||
data: Bookmark | Note; // The actual item data
|
||
owner_email?: string; // Optional owner info
|
||
}
|
||
```
|
||
|
||
### Preferences Model
|
||
```typescript
|
||
Preferences {
|
||
user_id: string;
|
||
search_provider: string; // 'google' | 'duckduckgo' | 'bing'
|
||
theme?: string; // Optional theme preference
|
||
updated_at: number;
|
||
}
|
||
```
|
||
|
||
### Widget Config Types
|
||
|
||
```typescript
|
||
BookmarkWidgetConfig {
|
||
// No additional config needed at widget level
|
||
// Bookmarks are stored in separate table
|
||
}
|
||
|
||
NotesWidgetConfig {
|
||
// No additional config needed at widget level
|
||
// Note is stored in separate table
|
||
}
|
||
|
||
WeatherWidgetConfig {
|
||
location: string;
|
||
}
|
||
```
|
||
|
||
|
||
## Correctness Properties
|
||
|
||
*A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||
|
||
### Authentication Properties
|
||
|
||
**Property 1: Unauthenticated users see login page**
|
||
*For any* unauthenticated user state, the rendered application should display a login page containing OAuth provider options.
|
||
**Validates: Requirements 1.1**
|
||
|
||
**Property 2: OAuth provider selection triggers redirect**
|
||
*For any* available OAuth provider, selecting it should generate a redirect to that provider's authorization URL with correct parameters.
|
||
**Validates: Requirements 1.2, 1.6**
|
||
|
||
**Property 3: Successful OAuth creates or retrieves user**
|
||
*For any* valid OAuth authorization response, the authentication system should either create a new user account or retrieve an existing one, and return a valid session token.
|
||
**Validates: Requirements 1.3**
|
||
|
||
**Property 4: Failed OAuth shows error**
|
||
*For any* invalid OAuth authorization response, the authentication system should display an error message and return to the unauthenticated state.
|
||
**Validates: Requirements 1.4**
|
||
|
||
**Property 5: Session persistence across refresh**
|
||
*For any* authenticated user session, refreshing the application should preserve the authentication state and user data.
|
||
**Validates: Requirements 1.7**
|
||
|
||
**Property 6: Logout terminates session**
|
||
*For any* authenticated session, logging out should invalidate the session token and return the application to the unauthenticated state.
|
||
**Validates: Requirements 1.8**
|
||
|
||
### Page Management Properties
|
||
|
||
**Property 7: New users get default page**
|
||
*For any* newly created user account, the page list should contain exactly one page named "Home".
|
||
**Validates: Requirements 2.1**
|
||
|
||
**Property 8: Adding page increases count**
|
||
*For any* existing page list, adding a new page should increase the page count by exactly one.
|
||
**Validates: Requirements 2.2**
|
||
|
||
**Property 9: Page switching displays correct widgets**
|
||
*For any* page with associated widgets, switching to that page should display exactly those widgets and no others.
|
||
**Validates: Requirements 2.4**
|
||
|
||
**Property 10: Page deletion removes page and widgets**
|
||
*For any* page with associated widgets, deleting the page should remove both the page and all its widgets from the system.
|
||
**Validates: Requirements 2.5**
|
||
|
||
**Property 11: Page reordering persists**
|
||
*For any* page list and any valid reordering, applying the reorder should result in the new order being reflected in subsequent retrievals.
|
||
**Validates: Requirements 2.7**
|
||
|
||
### Search Properties
|
||
|
||
**Property 12: Search generates correct URL**
|
||
*For any* non-empty search query and configured search provider, submitting the search should generate the correct search URL for that provider.
|
||
**Validates: Requirements 3.2**
|
||
|
||
**Property 13: Search provider selection persists**
|
||
*For any* available search provider, selecting it should update the user's preferences and persist the change across sessions.
|
||
**Validates: Requirements 3.4**
|
||
|
||
**Property 14: Empty search is rejected**
|
||
*For any* string composed entirely of whitespace characters, submitting it as a search query should not trigger a search action.
|
||
**Validates: Requirements 3.7**
|
||
|
||
### Widget Management Properties
|
||
|
||
**Property 15: Widget creation increases count**
|
||
*For any* page and any widget type, creating a widget should increase that page's widget count by exactly one.
|
||
**Validates: Requirements 4.1, 5.1, 6.1, 7.2**
|
||
|
||
**Property 16: Widget deletion decreases count**
|
||
*For any* page with widgets, deleting a widget should decrease the page's widget count by exactly one and remove that specific widget.
|
||
**Validates: Requirements 7.3**
|
||
|
||
**Property 17: Widget position updates persist**
|
||
*For any* widget and any valid position, updating the widget's position should persist the change and reflect it in subsequent retrievals.
|
||
**Validates: Requirements 7.5**
|
||
|
||
**Property 18: Widget IDs are unique**
|
||
*For any* set of widgets in the system, all widget IDs should be unique across all pages and users.
|
||
**Validates: Requirements 7.8**
|
||
|
||
**Property 19: Widget title customization**
|
||
*For any* widget and any custom title string, setting the custom title should update the widget and display the new title in the UI.
|
||
**Validates: Requirements 4.8, 5.10, 6.7**
|
||
|
||
### Bookmark Widget Properties
|
||
|
||
**Property 20: Bookmark addition persists**
|
||
*For any* bookmark widget and any valid bookmark (title and URL), adding the bookmark should store it and make it retrievable from the widget's bookmark list.
|
||
**Validates: Requirements 4.2**
|
||
|
||
**Property 21: Bookmark editing updates values**
|
||
*For any* bookmark in a widget, editing its title, URL, or tags should update the stored values and reflect the changes in subsequent retrievals.
|
||
**Validates: Requirements 4.4**
|
||
|
||
**Property 22: Bookmark deletion removes from list**
|
||
*For any* bookmark in a widget, deleting it should remove it from the widget's bookmark list and decrease the bookmark count by one.
|
||
**Validates: Requirements 4.5**
|
||
|
||
**Property 23: Bookmark reordering persists**
|
||
*For any* bookmark list and any valid reordering, applying the reorder should result in the new order being reflected in subsequent retrievals.
|
||
**Validates: Requirements 4.6**
|
||
|
||
**Property 24: Bookmark rendering includes all titles**
|
||
*For any* bookmark widget with bookmarks, the rendered output should contain all bookmark titles in the correct order.
|
||
**Validates: Requirements 4.7**
|
||
|
||
**Property 25: Group creation and assignment**
|
||
*For any* bookmark widget, creating a group and assigning bookmarks to it should persist the group structure and allow retrieval of bookmarks by group.
|
||
**Validates: Requirements 4.11**
|
||
|
||
**Property 26: Bookmark group reassignment**
|
||
*For any* bookmark and any target group (or ungrouped), moving the bookmark should update its group assignment and persist the change.
|
||
**Validates: Requirements 4.12**
|
||
|
||
### Notes Widget Properties
|
||
|
||
**Property 27: Notes content persists**
|
||
*For any* notes widget and any text content, updating the content should save it and make it retrievable in subsequent retrievals.
|
||
**Validates: Requirements 5.2, 5.9**
|
||
|
||
**Property 28: Content format preservation**
|
||
*For any* note with content in a specific format (plain, RTF, code, YAML, Markdown), saving and retrieving the note should preserve both the content and format metadata.
|
||
**Validates: Requirements 5.4, 5.11**
|
||
|
||
**Property 29: Unicode content preservation**
|
||
*For any* text content containing Unicode characters (international text, emoji), saving to a notes widget and retrieving it should preserve all Unicode characters correctly.
|
||
**Validates: Requirements 5.5**
|
||
|
||
**Property 30: Format switching preserves content**
|
||
*For any* note content and any format mode switch, changing the format mode should preserve the raw content without data loss.
|
||
**Validates: Requirements 5.18**
|
||
|
||
**Property 31: Code syntax highlighting**
|
||
*For any* note in code format with a specified language, the rendered output should include syntax highlighting appropriate for that language.
|
||
**Validates: Requirements 5.6**
|
||
|
||
**Property 32: YAML indentation preservation**
|
||
*For any* YAML content with indentation, saving and retrieving the content should preserve all indentation and structure.
|
||
**Validates: Requirements 5.7**
|
||
|
||
**Property 33: RTF formatting preservation**
|
||
*For any* rich text content with formatting (bold, italic, lists, headings), saving and retrieving should preserve all formatting.
|
||
**Validates: Requirements 5.8**
|
||
|
||
### Tag System Properties
|
||
|
||
**Property 34: Tag association supports one-to-many**
|
||
*For any* item (bookmark or note) and any set of tags, adding multiple tags should create all associations and make them retrievable.
|
||
**Validates: Requirements 4.9, 5.12**
|
||
|
||
**Property 35: Tag filtering returns matching items**
|
||
*For any* tag filter (single or multiple tags), querying items by tags should return only items that match the filter criteria.
|
||
**Validates: Requirements 4.10, 5.13, 8.9**
|
||
|
||
**Property 36: Tag deletion removes from all items**
|
||
*For any* tag name, deleting the tag should remove it from all associated bookmarks and notes.
|
||
**Validates: Requirements 4.15, 5.16**
|
||
|
||
**Property 37: Concurrent tag updates preserve data**
|
||
*For any* item and any concurrent tag update operations, all tag changes should be preserved without data loss or corruption.
|
||
**Validates: Requirements 4.14, 5.15, 8.10**
|
||
|
||
### Sharing Properties
|
||
|
||
**Property 38: Share link generation**
|
||
*For any* item (bookmark or note), creating a share should generate a unique shareable link that allows read-only access to the item.
|
||
**Validates: Requirements 4.16, 5.17**
|
||
|
||
**Property 39: Shared note preserves formatting**
|
||
*For any* note with formatting, accessing it via a share link should display the content with the original format preserved.
|
||
**Validates: Requirements 5.17**
|
||
|
||
### Weather Widget Properties
|
||
|
||
**Property 40: Weather data fetching**
|
||
*For any* valid location string, the weather widget should fetch weather data and display temperature, condition description, and an icon.
|
||
**Validates: Requirements 6.2, 6.3**
|
||
|
||
**Property 41: Weather location updates**
|
||
*For any* weather widget with a location, changing to a different valid location should fetch and display weather data for the new location.
|
||
**Validates: Requirements 6.4**
|
||
|
||
**Property 42: Weather error handling**
|
||
*For any* invalid location or API failure condition, the weather widget should display an error message instead of weather data.
|
||
**Validates: Requirements 6.5**
|
||
|
||
### Data Persistence Properties
|
||
|
||
**Property 43: Immediate persistence**
|
||
*For any* user action that modifies data (pages, widgets, bookmarks, notes, preferences, tags), the change should be immediately persisted and retrievable without requiring additional save actions.
|
||
**Validates: Requirements 2.8, 7.7, 8.1**
|
||
|
||
**Property 44: Data round-trip consistency**
|
||
*For any* user's complete data state (pages, widgets, bookmarks, notes, tags, groups, preferences), saving the state, logging out, and logging back in should restore an equivalent state.
|
||
**Validates: Requirements 8.2, 8.3, 8.4, 8.5**
|
||
|
||
**Property 45: Data isolation**
|
||
*For any* two different users, each user's data should be accessible only to that user and not visible or modifiable by the other user.
|
||
**Validates: Requirements 8.7**
|
||
|
||
**Property 46: Storage error handling**
|
||
*For any* storage operation failure, the application should display an error message to the user and maintain the previous valid state.
|
||
**Validates: Requirements 8.6**
|
||
|
||
### User Interface Properties
|
||
|
||
**Property 47: Interactive element feedback**
|
||
*For any* interactive element (button, link, input), hovering or clicking should trigger a visual state change.
|
||
**Validates: Requirements 10.4**
|
||
|
||
**Property 48: Action confirmation feedback**
|
||
*For any* user action (create, delete, update), the application should provide immediate visual confirmation that the action was performed.
|
||
**Validates: Requirements 10.5**
|
||
|
||
**Property 49: Loading indicators**
|
||
*For any* asynchronous operation (API call, data fetch), the application should display a loading indicator while the operation is in progress.
|
||
**Validates: Requirements 10.7**
|
||
|
||
**Property 50: Responsive layout reflow**
|
||
*For any* viewport size change, the application should reflow the layout to match the new dimensions without requiring a page refresh.
|
||
**Validates: Requirements 9.5**
|
||
|
||
|
||
## Error Handling
|
||
|
||
### Authentication Errors
|
||
|
||
**OAuth Failures:**
|
||
- Invalid authorization codes should return clear error messages
|
||
- Network failures during OAuth should be caught and displayed to users
|
||
- Expired or invalid session tokens should redirect to login
|
||
|
||
**Session Management:**
|
||
- Expired sessions should be detected and handled gracefully
|
||
- Concurrent session conflicts should be resolved (last-write-wins or error)
|
||
- Token validation failures should clear local state and redirect to login
|
||
|
||
### Data Validation Errors
|
||
|
||
**Input Validation:**
|
||
- Empty or whitespace-only strings should be rejected for required fields
|
||
- Invalid URLs in bookmarks should be caught and reported
|
||
- Invalid location strings for weather should show helpful error messages
|
||
- Page names should have length limits (1-50 characters) and character restrictions (no special chars)
|
||
- Tag names should be normalized (lowercase, trimmed) and validated for length (1-30 characters)
|
||
- **Validates Requirements 2.3, 4.2, 6.2**
|
||
|
||
**Data Integrity:**
|
||
- Attempts to delete non-existent resources should return 404 errors
|
||
- Attempts to modify resources owned by other users should return 403 errors
|
||
- Malformed widget configurations should be rejected with validation errors
|
||
- Version conflicts in optimistic locking should trigger retry logic
|
||
- **Validates Requirements 8.7, 8.10**
|
||
|
||
**Page Management:**
|
||
- Attempts to delete the last remaining page should be prevented
|
||
- Error message: "Cannot delete the last page. You must have at least one page."
|
||
- UI should disable delete button when only one page remains
|
||
- **Validates Requirement 2.6**
|
||
|
||
### Tag System Errors
|
||
|
||
**Tag Operations:**
|
||
- Duplicate tag additions should be idempotent (no error, no duplicate)
|
||
- Tag name conflicts during rename should be detected and reported
|
||
- Concurrent tag updates should use optimistic locking to prevent lost updates
|
||
- Tag deletion should handle cases where tag doesn't exist gracefully
|
||
|
||
**Tag Queries:**
|
||
- Empty tag filter should return all items (no filtering)
|
||
- Non-existent tags in filter should return empty results
|
||
- Malformed tag queries should return validation errors
|
||
|
||
### Group System Errors
|
||
|
||
**Group Operations:**
|
||
- Duplicate group names within a widget should be allowed (user's choice)
|
||
- Deleting a group with bookmarks should require user to specify destination
|
||
- Moving bookmark to non-existent group should return validation error
|
||
- Group order conflicts should be resolved by server
|
||
|
||
### Content Format Errors
|
||
|
||
**Format Validation:**
|
||
- Invalid format mode selection should default to plain text
|
||
- Unsupported language for code highlighting should use generic highlighting
|
||
- Malformed YAML should be stored as-is (no validation, user's responsibility)
|
||
- Malformed Markdown should be rendered as-is (marked.js handles gracefully)
|
||
- Format switching should never lose raw content data
|
||
- **Validates Requirements 5.3, 5.4, 5.18**
|
||
|
||
**Content Size:**
|
||
- Notes exceeding 400KB should be rejected with clear error message
|
||
- Alternative: Large notes (>100KB) could be automatically moved to S3 with reference in DynamoDB
|
||
- Threshold: Warn user at 50KB, auto-move to S3 at 100KB
|
||
- Bookmark URLs exceeding reasonable length (2KB) should be rejected
|
||
- **Validates Requirements 5.14, 11.2**
|
||
|
||
**Unicode and Special Characters:**
|
||
- All Unicode characters should be preserved (UTF-8 encoding throughout)
|
||
- Emoji should render correctly (system fonts or emoji library)
|
||
- International text (Chinese, Arabic, Cyrillic) should display correctly
|
||
- No character encoding issues (UTF-8 from browser to database)
|
||
- **Validates Requirement 5.5**
|
||
|
||
**Syntax Highlighting Errors:**
|
||
- Unknown language should fall back to generic highlighting
|
||
- Syntax errors in code should not prevent rendering
|
||
- Highlighting library failures should fall back to plain text display
|
||
- **Validates Requirement 5.6**
|
||
|
||
**RTF Formatting Errors:**
|
||
- Invalid HTML in RTF content should be sanitized (DOMPurify)
|
||
- XSS prevention: All user-generated HTML is sanitized
|
||
- Unsupported formatting should be stripped gracefully
|
||
- **Validates Requirement 5.8**
|
||
|
||
### External Service Errors
|
||
|
||
**Weather API Failures:**
|
||
- Network timeouts should show "Unable to fetch weather" message
|
||
- Invalid API keys should be logged server-side and show generic error to users
|
||
- Rate limiting should be handled with cached data or retry logic
|
||
- Invalid location responses should show "Location not found" message
|
||
|
||
**OAuth Provider Failures:**
|
||
- Provider downtime should show "Authentication service unavailable" message
|
||
- Network errors during OAuth flow should allow retry
|
||
- Provider-specific errors should be translated to user-friendly messages
|
||
|
||
### DynamoDB Errors
|
||
|
||
**Capacity Errors:**
|
||
- Throttling errors should trigger exponential backoff retry
|
||
- On-demand capacity should prevent most throttling issues
|
||
- Persistent throttling should be logged and monitored
|
||
|
||
**Consistency Errors:**
|
||
- Conditional write failures (version conflicts) should trigger retry with latest version
|
||
- Transaction failures should rollback and return error to user
|
||
- Batch operation partial failures should be handled item-by-item
|
||
|
||
**Connection Errors:**
|
||
- Network failures should trigger retry with exponential backoff
|
||
- Persistent connection issues should show "Service temporarily unavailable"
|
||
- Timeout errors should be caught and reported to user
|
||
|
||
### UI Error Handling
|
||
|
||
**User Feedback:**
|
||
- All errors should display user-friendly messages (not stack traces)
|
||
- Error messages should be dismissible
|
||
- Critical errors should prevent further actions until resolved
|
||
- Non-critical errors should allow users to continue working
|
||
|
||
**Recovery Mechanisms:**
|
||
- Failed operations should be retryable
|
||
- Partial failures should not corrupt existing data
|
||
- Auto-save failures should queue changes for retry
|
||
- Network errors should trigger automatic retry with exponential backoff
|
||
- Optimistic UI updates should rollback on server error
|
||
|
||
**Error Boundaries:**
|
||
- React error boundaries should catch component errors
|
||
- Widget errors should not crash entire dashboard
|
||
- Isolated widget failures should show error state in that widget only
|
||
- Global errors should show full-page error with reload option
|
||
|
||
|
||
## Testing Strategy
|
||
|
||
### Dual Testing Approach
|
||
|
||
The application will use both unit testing and property-based testing to ensure comprehensive coverage:
|
||
|
||
**Unit Tests:**
|
||
- Specific examples demonstrating correct behavior
|
||
- Edge cases and boundary conditions
|
||
- Integration points between components
|
||
- Error conditions and failure scenarios
|
||
- UI component rendering with specific props
|
||
- Specific format examples (YAML, Markdown, code)
|
||
- Specific tag operations (add, remove, filter)
|
||
- Group management operations
|
||
|
||
**Property-Based Tests:**
|
||
- Universal properties that hold for all inputs
|
||
- Comprehensive input coverage through randomization
|
||
- Invariants that must be maintained across operations
|
||
- Round-trip properties for serialization and data persistence
|
||
- Concurrent operation safety
|
||
- Tag system correctness across all operations
|
||
- Format preservation across all content types
|
||
- Minimum 100 iterations per property test
|
||
|
||
### Property-Based Testing Configuration
|
||
|
||
**Framework Selection:**
|
||
- **Go**: testing package (built-in) + testify for assertions
|
||
- **Go Property Testing**: gopter or rapid library
|
||
- **Python** (if used): pytest + Hypothesis
|
||
|
||
**Test Configuration:**
|
||
- Each property test must run minimum 100 iterations
|
||
- Each test must reference its design document property
|
||
- Tag format: `Feature: custom-start-page, Property {number}: {property_text}`
|
||
- Each correctness property must be implemented by a single property-based test
|
||
|
||
**Generator Strategy:**
|
||
- Create generators for all domain objects (User, Page, Widget, Bookmark, Note, Tag, Group)
|
||
- Generate valid and invalid inputs to test error handling
|
||
- Use shrinking to find minimal failing examples
|
||
- Ensure generators cover edge cases:
|
||
- Empty lists and collections
|
||
- Special characters and Unicode in text
|
||
- Boundary values (max lengths, large counts)
|
||
- Concurrent operations
|
||
- Multiple tags per item
|
||
- Multiple formats for notes
|
||
- Large datasets (1000+ bookmarks for scale testing)
|
||
|
||
**Tag System Generators:**
|
||
- Generate random tag names (normalized and non-normalized)
|
||
- Generate items with 0 to 10 tags
|
||
- Generate tag filters (single tag, multiple tags, AND/OR logic)
|
||
- Generate concurrent tag operations
|
||
|
||
**Content Format Generators:**
|
||
- Generate plain text with various Unicode characters
|
||
- Generate RTF with formatting markers
|
||
- Generate code in various languages
|
||
- Generate valid and invalid YAML
|
||
- Generate Markdown with various elements
|
||
- Generate content at various sizes (small, medium, large)
|
||
|
||
### Testing Layers
|
||
|
||
**Frontend Testing:**
|
||
- HTML template rendering tests
|
||
- HTMX attribute validation
|
||
- Integration tests for user flows
|
||
- Visual regression tests for UI consistency
|
||
- Accessibility tests (WCAG compliance)
|
||
- Syntax highlighting tests
|
||
- Tag filter UI tests
|
||
- Drag-and-drop tests with Sortable.js
|
||
|
||
**Backend Testing (Go):**
|
||
- HTTP handler unit tests
|
||
- Service layer property tests
|
||
- DynamoDB integration tests
|
||
- OAuth flow integration tests
|
||
- Performance tests for data operations
|
||
- Concurrent update tests
|
||
- Tag query performance tests
|
||
- Batch operation tests
|
||
- Template rendering tests
|
||
|
||
**DynamoDB Testing:**
|
||
- Schema validation tests
|
||
- Access pattern tests (all 10 patterns)
|
||
- GSI query tests
|
||
- Optimistic locking tests
|
||
- Transaction tests
|
||
- Batch operation tests
|
||
- Cost estimation validation
|
||
|
||
**End-to-End Testing:**
|
||
- Critical user journeys:
|
||
- Login → create page → add widgets → add bookmarks with tags
|
||
- Create notes with different formats
|
||
- Filter bookmarks/notes by tags
|
||
- Create groups and organize bookmarks
|
||
- Share bookmarks and notes
|
||
- Concurrent tag updates from multiple sessions
|
||
- Cross-browser compatibility tests
|
||
- Mobile responsiveness tests
|
||
- Session persistence tests
|
||
- Large dataset tests (1000+ bookmarks)
|
||
|
||
### Test Coverage Goals
|
||
|
||
- Minimum 80% code coverage for business logic
|
||
- 100% coverage of error handling paths
|
||
- All correctness properties implemented as property tests
|
||
- All acceptance criteria covered by unit or property tests
|
||
- Integration tests for all HTTP endpoints
|
||
- All DynamoDB access patterns tested
|
||
- All tag operations tested for correctness and concurrency
|
||
- All content formats tested for preservation
|
||
- All HTML templates tested for correct rendering
|
||
|
||
### Performance Testing
|
||
|
||
**Scale Testing:**
|
||
- Test with 10,000+ bookmarks per user
|
||
- Test with 1,000+ notes per user
|
||
- Test with 100+ tags per user
|
||
- Test tag queries with large result sets
|
||
- Test concurrent updates with multiple sessions
|
||
|
||
**Benchmarks:**
|
||
- Page load time with 100 widgets
|
||
- Bookmark list rendering with 1,000 bookmarks
|
||
- Tag filter query time with 10,000 bookmarks
|
||
- Note editor performance with large content
|
||
- DynamoDB query latency for all access patterns
|
||
|
||
**Load Testing:**
|
||
- Concurrent user sessions
|
||
- Rapid tag updates
|
||
- Batch bookmark operations
|
||
- Weather API rate limiting
|
||
- DynamoDB throughput limits
|
||
|
||
### Continuous Integration
|
||
|
||
- Run all tests on every commit
|
||
- Property tests run with 100 iterations in CI
|
||
- Extended property test runs (1000+ iterations) nightly
|
||
- Performance benchmarks tracked over time
|
||
- Test failures block merges to main branch
|
||
- DynamoDB local for integration tests
|
||
- Mock external services (OAuth, Weather API)
|
||
|
||
### Test Data Management
|
||
|
||
**Fixtures:**
|
||
- Sample users with various data sizes
|
||
- Sample bookmarks with tags and groups
|
||
- Sample notes in all formats
|
||
- Sample tag associations
|
||
- Sample shared items
|
||
|
||
**Factories:**
|
||
- User factory with configurable OAuth provider
|
||
- Page factory with configurable widget count
|
||
- Widget factory for each type
|
||
- Bookmark factory with tags and groups
|
||
- Note factory with format selection
|
||
- Tag factory with normalization
|
||
|
||
**Cleanup:**
|
||
- Tear down test data after each test
|
||
- Use DynamoDB local for isolated testing
|
||
- Clear caches between tests
|
||
- Reset mock services
|
||
|