## Bug Fixes ### Note Card Actions - Fix broken size change functionality (missing state declaration) - Implement React 19 useOptimistic for instant UI feedback - Add startTransition for non-blocking updates - Ensure smooth animations without page refresh - All note actions now work: pin, archive, color, size, checklist ### Markdown LaTeX Rendering - Add remark-math and rehype-katex plugins - Support inline equations with dollar sign syntax - Support block equations with double dollar sign syntax - Import KaTeX CSS for proper styling - Equations now render correctly instead of showing raw LaTeX ## Technical Details - Replace undefined currentNote references with optimistic state - Add optimistic updates before server actions for instant feedback - Use router.refresh() in transitions for smart cache invalidation - Install remark-math, rehype-katex, and katex packages ## Testing - Build passes successfully with no TypeScript errors - Dev server hot-reloads changes correctly
11 KiB
Data Models - Memento Project
Overview
Memento uses SQLite as its database with Prisma ORM. The database schema is shared between the web application (keep-notes) and the MCP server.
Database: SQLite (prisma/dev.db)
ORM: Prisma 5.22.0
Adapters:
@prisma/adapter-better-sqlite3(primary)@prisma/adapter-libsql(alternative for Turso)
Database Schema
User
Represents a user account with authentication and profile information.
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
password String? // Hashed password (bcrypt)
role String @default("USER") // "USER" or "ADMIN"
image String? // Profile picture URL
theme String @default("light") // UI theme preference
resetToken String? @unique // Password reset token
resetTokenExpiry DateTime? // Reset token expiration
accounts Account[]
sessions Session[]
notes Note[]
labels Label[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Relationships:
- One-to-many with Account (OAuth providers)
- One-to-many with Session (active sessions)
- One-to-many with Note (user's notes)
- One-to-many with Label (user's labels)
Fields:
id: CUID (Collision-resistant Unique Identifier)email: Unique email addresspassword: Optional (can be OAuth-only users)role: RBAC - "USER" or "ADMIN"theme: UI theme preference ("light" or "dark")resetToken: Password recovery token (unique)resetTokenExpiry: Token validity period
Account
Stores OAuth provider account information (NextAuth.js).
model Account {
userId String
type String
provider String // google, github, etc.
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([provider, providerAccountId])
}
Purpose: Links OAuth provider accounts to local User accounts
Relationships:
- Many-to-one with User (via
userId)
Constraints:
- Composite unique key on
provider+providerAccountId
Session
Stores active user sessions (NextAuth.js).
model Session {
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Purpose: Manages active user sessions for authentication
Relationships:
- Many-to-one with User (via
userId)
Fields:
sessionToken: Unique session identifierexpires: Session expiration timestamp
VerificationToken
Stores email verification tokens (NextAuth.js).
model VerificationToken {
identifier String
token String
expires DateTime
@@id([identifier, token])
}
Purpose: Email verification flow
Constraints:
- Composite unique key on
identifier+token
Label
User-defined labels/tags for organizing notes.
model Label {
id String @id @default(cuid())
name String
color String @default("gray")
userId String? // Made optional for migration
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([name, userId]) // Labels unique per user
@@index([userId])
}
Purpose: Categorization and organization of notes
Relationships:
- Many-to-one with User (via
userId)
Fields:
name: Label display namecolor: Visual color (red, orange, yellow, green, teal, blue, purple, pink, gray)userId: Optional (migration artifact, logic enforces ownership)
Constraints:
- Unique label name per user
- Indexed on
userIdfor fast lookup
Usage:
- Stored as JSON array in
Note.labels - User can have multiple labels with the same name? No, unique constraint
Note
Core data model - represents a note in the system.
model Note {
id String @id @default(cuid())
title String? // Optional title
content String
color String @default("default")
isPinned Boolean @default(false)
isArchived Boolean @default(false)
type String @default("text") // "text" or "checklist"
checkItems String? // JSON array
labels String? // JSON array
images String? // JSON array
links String? // JSON array
reminder DateTime? // Reminder timestamp
isReminderDone Boolean @default(false)
reminderRecurrence String? // "none", "daily", "weekly", "monthly", "custom"
reminderLocation String? // Location-based reminders
isMarkdown Boolean @default(false) // Markdown rendering
size String @default("small") // "small", "medium", "large"
embedding String? // Vector embeddings (JSON)
userId String? // Owner
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
order Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isPinned])
@@index([isArchived])
@@index([order])
@@index([reminder])
@@index([userId])
}
Purpose: Core note-taking entity
Relationships:
- Many-to-one with User (via
userId)
Fields:
Basic:
id: CUIDtitle: Optional heading/titlecontent: Note content (text or markdown)color: Background color (default, red, orange, yellow, green, teal, blue, purple, pink, gray)type: Note type - "text" or "checklist"
State Flags:
isPinned: Shows at top of list- `isArchived**: Removed from main view
isReminderDone: Reminder completed
Rich Features:
checkItems: JSON array of checklist items (if type=checklist)[ {"id": "string", "text": "string", "checked": boolean} ]labels: JSON array of label names["work", "ideas", "urgent"]images: JSON array of image URLs or base64["data:image/png;base64,..."]links: JSON array of link metadatareminder: ISO8601 datetime for reminderreminderRecurrence: Recurrence patternreminderLocation: Location-based reminders (future)isMarkdown: Enable markdown renderingsize: Visual size (small, medium, large)embedding: Vector embeddings for semantic search (JSON)
Ordering:
order: Manual sort order (drag-and-drop)
Indexes:
isPinned: Fast lookup for pinned notesisArchived: Filter archived notesorder: Sort by manual orderingreminder: Reminder queries (cron jobs)userId: Filter by user
SystemConfig
Key-value storage for system-wide configuration.
model SystemConfig {
key String @id
value String
}
Purpose: System configuration and feature flags
Examples:
- Feature toggles
- System-wide settings
- Admin configuration
Relationships Diagram
User (1) ----< (N) Account
|
| (1)
|
+----< (N) Session
|
| (1)
|
+----< (N) Note
|
| (1)
|
+----< (N) Label
VerificationToken (standalone)
SystemConfig (standalone)
JSON Field Structures
checkItems (Note)
Array of checklist items:
[
{
"id": "unique-id",
"text": "Task description",
"checked": false
}
]
labels (Note)
Array of label names (strings):
[
"work",
"ideas",
"urgent"
]
images (Note)
Array of image URLs or base64 strings:
[
"data:image/png;base64,iVBORw0KGgo...",
"/uploads/images/note-image.png"
]
links (Note)
Array of link metadata (structure TBD):
[
{
"url": "https://example.com",
"title": "Example Site",
"description": "..."
}
]
embedding (Note)
Vector embeddings for semantic search (JSON string):
[0.123, -0.456, 0.789, ...]
Database Operations
Common Queries
Get active user's notes:
const notes = await prisma.note.findMany({
where: {
userId: session.user.id,
isArchived: false
},
orderBy: [
{ isPinned: 'desc' },
{ order: 'asc' },
{ updatedAt: 'desc' }
]
})
Search notes:
const notes = await prisma.note.findMany({
where: {
userId: session.user.id,
OR: [
{ title: { contains: query, mode: 'insensitive' } },
{ content: { contains: query, mode: 'insensitive' } }
]
}
})
Get user's labels:
const labels = await prisma.label.findMany({
where: { userId: session.user.id },
orderBy: { name: 'asc' }
})
Data Integrity
Cascade Deletes:
- When User is deleted: delete their Accounts, Sessions, Notes, and Labels
- Maintains referential integrity
Unique Constraints:
- User.email
- Account.provider + Account.providerAccountId
- VerificationToken.identifier + VerificationToken.token
- Label.name + Label.userId (per user)
Indexes:
- All foreign keys indexed
- Frequently queried fields indexed (isPinned, isArchived, order, reminder, userId)
Migration Notes
Current State:
- SQLite database for local development
- Better-sqlite3 adapter for embedded usage
- Optional LibSQL adapter for cloud deployment (Turso)
Schema Evolution:
Label.userIdmade optional for migration (logic enforces ownership)- JSON fields stored as strings (parsed in application layer)
- Future: Consider PostgreSQL for production with proper JSONB support
Database Size Considerations
SQLite Limits:
- Max database size: 281 TB (theoretical)
- Max row size: 1 GB
- Max string/blob size: 1 GB
Practical Considerations:
- Base64 images in Note.images could bloat database
- Consider storing large files in filesystem and storing paths
- Vector embeddings (Note.embedding) will grow with data
Recommendations:
- Use CDN or object storage for images in production
- Implement image compression before storage
- Monitor database size with large note collections