78 KiB
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:
- Plain Text: Simple text with line breaks and Unicode support
- Rich Text (RTF): Basic formatting (bold, italic, lists, headings) using contenteditable or TinyMCE
- Code: Syntax-highlighted code blocks with language selection (JavaScript, Python, Go, etc.)
- YAML: Structured data with indentation preservation
- Markdown: Markdown syntax with live preview
Storage Strategy:
- Content stored as string in DynamoDB Notes table
- Format metadata stored in
formatfield (enum: plain, rtf, code, yaml, markdown) - Language metadata stored in
languagefield (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_locationfield 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:
-
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
-
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
-
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
-
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
-
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:
-
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
-
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
-
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
-
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
-
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:
-
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
-
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
-
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
-
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:
- Get all pages for a user
- Get all widgets for a page
- Get all bookmarks for a widget
- Get all notes for a widget
- Get all bookmarks/notes with a specific tag
- Get all tags for a user
- Get all items in a group
- Get shared item by share ID
- Update tags on a bookmark/note (concurrent-safe)
- 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:
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:
<button hx-get="/pages/{pageId}"
hx-target="#widget-grid"
hx-swap="innerHTML">
Page Name
</button>
Add Bookmark:
<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:
<button hx-delete="/widgets/{widgetId}"
hx-target="closest .widget"
hx-swap="outerHTML swap:1s">
Delete
</button>
Tag Filter:
<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:
<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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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 pageGET /auth/oauth/:provider- Initiate OAuth flowGET /auth/callback/:provider- OAuth callback handlerPOST /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 HTMLPOST /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 widgetPOST /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 widgetPOST /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 widgetPOST /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
User {
id: string; // UUID
email: string;
oauth_provider: string;
oauth_id: string;
created_at: number; // Unix timestamp
updated_at: number;
}
Page Model
Page {
id: string; // UUID
user_id: string;
name: string;
order: number;
created_at: number;
updated_at: number;
}
Widget Model
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
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
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
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
TagInfo {
name: string;
count: number; // Number of items with this tag
item_types: ItemType[]; // Which types use this tag
}
Group Model
Group {
id: string; // UUID
widget_id: string;
user_id: string;
name: string;
order: number;
created_at: number;
updated_at: number;
}
Share Model
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
Preferences {
user_id: string;
search_provider: string; // 'google' | 'duckduckgo' | 'bing'
theme?: string; // Optional theme preference
updated_at: number;
}
Widget Config Types
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