Complete database schema documentation for Harvous, including entity relationships, table definitions, and special patterns.
erDiagram
USERS ||--o{ SPACES : creates
USERS ||--o{ THREADS : creates
USERS ||--o{ NOTES : creates
USERS ||--o{ TAGS : creates
USERS ||--|| USERMETADATA : has
USERS ||--o{ USERXP : earns
USERS ||--o{ USERSEASONALXP : earns
USERS ||--|| USERLIFETIMEXP : has
USERS ||--o{ WEEKLYSTREAKS : has
SPACES ||--o{ THREADS : contains
SPACES ||--o{ NOTES : contains
SPACES ||--o{ MEMBERS : has
SPACES ||--o{ SPACEINVITATIONS : has
THREADS ||--o{ NOTETHREADS : "via junction"
NOTES ||--o{ NOTETHREADS : "via junction (single source of truth)"
NOTES ||--o{ NOTETAGS : has
NOTES ||--o{ COMMENTS : has
NOTES ||--o| SCRIPTUREMETADATA : "may have"
NOTES ||--o| RESOURCEMETADATA : "may have"
NOTES ||--o{ NOTESCRIPTUREREFERENCES : "references"
TAGS ||--o{ NOTETAGS : "via junction"
INBOXITEMS ||--o{ INBOXITEMNOTES : contains
INBOXITEMS ||--o{ USERINBOXITEMS : "user status"
USERS {
text userId PK "Clerk ID"
text firstName
text lastName
text email
}
USERMETADATA {
text id PK
text userId FK
int highestSimpleNoteId
text userColor
text firstName
text lastName
text referralCode
int referralBonusNotes
text lockPinSalt
text lockPinHash
text churchName
text currentSeason
date lastMonthlyVisit
}
SPACES {
text id PK
text title
text description
text userId FK
text color
text backgroundGradient
boolean isPublic
boolean isActive
int order
text shareToken
date shareTokenCreatedAt
}
THREADS {
text id PK
text title
text subtitle
text spaceId FK
text userId FK
text color
boolean isPinned
int order
text shareToken
date shareTokenCreatedAt
}
NOTES {
text id PK
text title
text content
text threadId FK "Legacy (thread_unorganized)"
text spaceId FK
int simpleNoteId "N001, N002..."
text noteType "default|scripture|resource"
text addedBy "user|harvous"
text userId FK
boolean isFeatured
boolean contentEncrypted
int order
text shareToken
date shareTokenCreatedAt
}
NOTETHREADS {
text id PK
text noteId FK
text threadId FK
date createdAt
}
MEMBERS {
text id PK
text userId FK
text spaceId FK
text role "member|admin|owner"
}
SPACEINVITATIONS {
text id PK
text spaceId FK
text inviteToken
text status "pending|accepted|declined|expired|cancelled"
text invitedEmail
text invitedUserId
}
TAGS {
text id PK
text name
text category
text color
text userId FK
boolean isSystem
}
NOTETAGS {
text id PK
text noteId FK
text tagId FK
boolean isAutoGenerated
float confidence "0-1"
}
SCRIPTUREMETADATA {
text id PK
text noteId FK
text reference "John 3:16"
text book
int chapter
int verse
int verseEnd
text translation
text originalText
}
RESOURCEMETADATA {
text id PK
text noteId FK
text sourceUrl
text sourceDomain
text sourceTitle
text sourceImage
}
NOTESCRIPTUREREFERENCES {
text id PK
text noteId FK
text scriptureNoteId FK
}
USERXP {
text id PK
text userId FK
text activityType
int xpAmount
text relatedId
text season
}
USERSEASONALXP {
text id PK
text userId FK
text season
int totalXP
int sessionCount
}
USERLIFETIMEXP {
text id PK
text userId FK
int totalXP
date lastUpdated
}
WEEKLYSTREAKS {
text id PK
text userId FK
date weekStart
int daysWithSessions
int xpAwarded
}
INBOXITEMS {
text id PK
text webflowItemId
text contentType "thread|note"
text title
text targetAudience
}
INBOXITEMNOTES {
text id PK
text inboxItemId FK
text content
int order
}
USERINBOXITEMS {
text id PK
text userId FK
text inboxItemId FK
text status "inbox|archived|added"
}
MONTHLYANALYTICS {
text id PK
text month "YYYY-MM"
text bookName
text tagName
text category "book|tag"
int count
}
| Table | Purpose | Key Fields |
|---|---|---|
| Spaces | Top-level organization containers | title, description, color, backgroundGradient, userId, isPublic, isActive, shareToken |
| Threads | Collections of related notes | title, subtitle, spaceId, color, isPinned, shareToken |
| Notes | Individual content items | title, content, threadId (legacy), simpleNoteId, noteType, addedBy, contentEncrypted, shareToken |
| NoteThreads | Single source of truth: notes ↔ threads (many-to-many) | noteId, threadId |
| Members | Shared space membership (v1 complete) | userId, spaceId, role |
| SpaceInvitations | Invites to join shared spaces (link-based join; email infra exists) | spaceId, inviteToken, status, invitedEmail, invitedUserId |
| Tags | Categorization labels | name, category, color, isSystem |
| NoteTags | Many-to-many: notes ↔ tags | noteId, tagId, isAutoGenerated, confidence |
| UserMetadata | User preferences & cached data | highestSimpleNoteId, userColor, referralBonusNotes, referralCode, lockPinSalt, lockPinHash, church fields, currentSeason |
| UserXP | XP activity log | activityType, xpAmount, relatedId, season |
| UserSeasonalXP | Aggregated XP per season | userId, season, totalXP, sessionCount |
| UserLifetimeXP | Lifetime XP total | userId, totalXP, lastUpdated |
| WeeklyStreaks | Weekly session streaks | userId, weekStart, daysWithSessions, xpAwarded |
| ScriptureMetadata | Bible reference data | reference, book, chapter, verse, verseEnd, translation, originalText |
| ResourceMetadata | Resource note metadata (URLs, OG) | noteId, sourceUrl, sourceDomain, sourceTitle, sourceImage |
| NoteScriptureReferences | Notes referencing scripture notes | noteId, scriptureNoteId |
| Comments | Note comments | noteId, userId, content |
| InboxItems | Curated content (Webflow sync) | webflowItemId, contentType, title, targetAudience |
| InboxItemNotes | Notes within inbox threads | inboxItemId, content, order |
| UserInboxItems | User inbox status | userId, inboxItemId, status (inbox/archived/added) |
| MonthlyAnalytics | Anonymous book/tag usage | month, bookName, tagName, category, count |
- Each user has a
highestSimpleNoteIdin UserMetadata - New notes get next sequential ID: N001, N002, N003...
- Never reused - deleted IDs are skipped for data integrity
Implementation Details:
The system uses a dual ID approach for notes:
- Database ID: Unique timestamp-based ID (e.g.,
note_1756318000001) for internal database operations - User-Friendly ID: Sequential simple ID (e.g.,
N001,N002,N003) for display and user reference
Simple Note ID Logic:
- Sequential Generation: Simple note IDs are generated sequentially (1, 2, 3, etc.)
- No Reuse: Deleted note IDs are never reused to maintain data integrity
- Example: If you have notes N001, N002, N003 and delete N003, the next note will be N004 (not N003)
- Highest Ever Used: The system tracks the highest simpleNoteId ever used per user
- User-Scoped: Each user has their own sequential numbering starting from N001
UserMetadata Approach: The current implementation uses a UserMetadata table to track the highest simpleNoteId ever used per user. This approach was chosen because the requirement is to never reuse deleted note IDs, which requires tracking the highest ID ever assigned, not just the highest currently existing.
How It Works:
- UserMetadata table stores
highestSimpleNoteIdfor each user - Next ID calculation:
highestSimpleNoteId + 1(never reuses deleted IDs) - On note creation: Updates
highestSimpleNoteIdto the new value - Result: Deleted note IDs are never reused, maintaining sequential integrity
Example Flow:
- Create N001 → highestSimpleNoteId = 1
- Create N002 → highestSimpleNoteId = 2
- Create N003 → highestSimpleNoteId = 3
- Delete N003 → highestSimpleNoteId = 3 (unchanged)
- Create new note → highestSimpleNoteId = 3 + 1 = 4 → N004 ✅
Benefits:
- Data Integrity: Ensures deleted note IDs are never reused
- User Experience: Provides predictable sequential numbering
- Performance: Avoids querying all notes on every ID generation
- Reliability: Tracks the highest ID ever used, regardless of current database state
- Every user has a
thread_unorganizedthread (display title My Pile) - In the SPA, this thread is routed at
/thread/mypile;/thread/unorganizedredirects to that path. The database id staysthread_unorganized. - My Pile = notes with NO junction table entries
- Notes automatically land in My Pile when all junction entries are removed
- Appears in navigation when it contains notes (same as regular threads)
- Cannot be deleted or edited (no menu options)
- Primary
threadIdfield is legacy (always set to'thread_unorganized')
Thread Deletion Behavior:
When a thread is deleted, the system preserves all notes by moving them to the "My Pile" thread:
- Primary Thread Notes: Notes that have the deleted thread as their primary
threadIdare moved to the "My Pile" thread - Many-to-Many Relationships: Notes that are in the deleted thread via the
NoteThreadsjunction table have their relationship removed (but remain in other threads) - Note Preservation: No notes are ever deleted when a thread is deleted - they are always preserved and moved to "My Pile"
- My Pile thread: Always exists by default (hidden from dashboard display)
- Protection: The My Pile thread itself cannot be deleted to prevent data loss
- Single source of truth:
NoteThreadsjunction table - Notes belong to threads via junction entries only
- Notes can belong to multiple threads (many-to-many)
- Adding note to thread: creates junction entry → automatically removed from My Pile
- Removing note from thread: deletes junction entry → automatically returns to My Pile if last one
- Primary
threadIdfield is legacy (always'thread_unorganized', never changes) - Navigation falls back to most recently updated thread
Multi-Thread Navigation System:
When a note belongs to multiple threads, the system uses intelligent defaults to determine which thread context to open the note in:
- URL Parameter Override (
?thread=threadId) - explicit user choice - Navigation Context Detection - thread user was viewing when they clicked the note
- Most Recent Thread Activity - fallback to thread with most recent
updatedAttimestamp - My Pile thread fallback - final fallback for notes with no valid thread context
(Note: A NoteThreadAccess table for per-note last-accessed-thread tracking was removed; context is determined by URL and navigation state only.)
The system uses a hybrid approach for note-thread relationships:
- Primary Relationship: Each note has a required
threadIdfield pointing to its primary thread (used primarily for My Pile fallback) - Many-to-Many Support:
NoteThreadsjunction table allows notes to belong to multiple threads - My Pile thread: Special thread with ID
thread_unorganizedserves as default for unassigned notes - Thread Deletion Logic: When a thread is deleted, notes with that thread as primary
threadIdare moved to the My Pile thread - Junction Cleanup: Many-to-many relationships are removed from
NoteThreadstable when threads are deleted - Data Integrity: No notes are ever deleted - they are always preserved and moved to the My Pile thread
The application includes a comprehensive XP (Experience Points) system to gamify user engagement and encourage content creation:
- Activity Tracking: Records all user activities that earn XP
- Activity Types:
thread_created,note_created,note_opened,first_note_daily - XP Amounts: Configurable XP values for different activities
- Related IDs: Links XP records to specific notes/threads
- Metadata: JSON field for additional data (daily caps, etc.)
- Thread Creation: 10 XP per new thread
- Note Creation: 10 XP per new note
- Note Opening: 1 XP per note opened (50 XP daily cap to prevent gaming)
- First Note Daily Bonus: +5 XP bonus for the first note created each day
- Automatic Awarding: XP is automatically awarded when users create content
- Daily Caps: Prevents gaming by limiting note opening XP to 50 per day
- Backfill System: Can retroactively calculate XP for existing users
- Real-time Display: Profile page shows current XP total
- Future Expansion: Designed to support levels, badges, and achievements
- Notes:
note_1756318000001,note_1756318000003 - Threads:
thread_1756318000000,thread_1756318000004 - Spaces:
space_1756318000006
- ARCHITECTURE.md - Overall system architecture
- API.md - API endpoints for database operations
- FEATURES.md - User-facing features that use the database