hoangsonww / hoangsonww/CollabNote-Fullstack-App
Backlinks & Graph View (WikiLinks, Bidirectional Mentions, Network Map)
- Dominant language
- TypeScript
- Stars
- 27
- Forks
- 11
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
Add first-class **backlinks** and a **graph view** so notes can reference each other using `[[WikiLinks]]` and `@note` mentions. Auto-extract links on save, show **Backlinks** and **Linked Mentions** sidebars, and visualize relationships in an interactive **Graph** (force-directed) across a notebook or filtered subset. Ships with GraphQL and REST, Supabase Realtime updates, and Postgres migrations.
## Why
* Improve knowledge discovery and non-linear navigation (Roam/Obsidian-style).
* Make collaboration clearer: see who referenced a note and why.
* Reduce duplication by surfacing related notes automatically.
**Success metrics (30 days post-launch):**
* ≥40% of active users create ≥1 link.
* ≥25% of note opens involve following a backlink/graph edge.
* Search exits via link follow increase ≥15%.
---
## User Stories
* As a user, I can type `[[Note Title]]` to create a link; if it doesn’t exist, I’m prompted to create it.
* As a user, I see a **Backlinks** panel on a note listing all inbound links with preview snippets.
* As a user, I can open a **Graph** that shows notes as nodes and links as edges; I can filter by tag, text query, or date.
* As a collaborator, I see **Linked Mentions** when other users reference the note in shared workspaces.
* As a user, I get realtime updates when someone links to my open note.
---
## UX
### Editor
* Inline autocompletion when typing `[[...]]` or `@...` (debounced search by title).
* Hover on an inline link reveals a mini-card (title, first 120 chars, last modified).
### Sidebars
* **Backlinks:** grouped by source note, with snippet of surrounding text; click scrolls to the referencing context.
* **Linked Mentions:** shows mentions from shared notes (requires share permission).
### Graph View
* Route: `/graph` and `/graph?noteId=`
* Controls: search, tag filter, neighbor depth (1–3), pin/expand, focus on current note.
* Tooling: D3 force simulation; Material UI for controls.
---
## Data Model (PostgreSQL)
```sql
-- notes (existing) assumed: id (uuid), user_id, title, content, tags, ...
-- new table: normalized, many-to-many between notes
CREATE TABLE note_links (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
target_note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (source_note_id, target_note_id)
);
-- optional helper for fast lookups
CREATE INDEX idx_note_links_source ON note_links(source_note_id);
CREATE INDEX idx_note_links_target ON note_links(target_note_id);
-- optional: store match context for backlink snippets
CREATE TABLE note_link_contexts (
link_id UUID REFERENCES note_links(id) ON DELETE CASCADE,
excerpt TEXT,
offset_start INT,
offset_end INT
);
```
**Parsing rules**
* `[[Note Title]]` resolves by **exact title match**; if multiple, prefer most recently modified; if none, prompt create.
* `@Note Title` acts the same but styled as a mention.
* Future-proof: detect `[[#heading]]` anchors (v2).
---
## API Design
### GraphQL (NestJS)
```graphql
type NoteLink {
id: ID!
sourceNoteId: ID!
targetNoteId: ID!
createdBy: ID!
createdAt: DateTime!
excerpt: String
}
extend type Query {
backlinks(noteId: ID!): [NoteLink!]!
outgoingLinks(noteId: ID!): [NoteLink!]!
graph(noteId: ID, depth: Int = 1, filter: String, tags: [String!]): [Note!]!
}
extend type Mutation {
upsertLinksFromContent(noteId: ID!, content: String!): Boolean!
createNoteFromTitle(title: String!): Note! # used by "create on link"
}
```
### REST (Swagger)
```
GET /links/backlinks/:noteId
GET /links/outgoing/:noteId
POST /links/parse-upsert { noteId, content }
GET /graph?noteId=&depth=&filter=&tags=
```
### Realtime
* Supabase channel `links:note:{noteId}` publishes:
* `link_created`, `link_deleted`, `links_refreshed`
* Editor subscribes for live backlink panel updates.
---
## Backend Implementation (NestJS)
* `LinksModule` with:
* `LinksService.extract(content: string)` → `{ matches: { title, offsetStart, offsetEnd }[] }` (simple lexer; no heavy Markdown parse required)
* `LinksService.resolveOrCreate(title)` → `noteId`
* `LinksService.upsert(noteId, content)` → create/delete rows in `note_links` and `note_link_contexts` within a txn
* Guards: ensure `source_note_id` is owned or shared with editor rights.
* Multi-tenant: sharing rules respected in backlinks/graph queries.
---
## Frontend (Next.js + Vite + MUI)
* Editor enhancements:
* Autocomplete popover on `[[` and `@` with type-ahead (title, recent first).
* On save, call `POST /links/parse-upsert`.
* Sidebars:
* `BacklinksPanel` + `MentionsPanel` (virtualized lists for large sets).
* Graph:
* `/graph`: D3 component with zoom/pan, node drag, focus, and neighbor highlight.
* Deep-link from a note: “Open in Graph”.
---
## Permissions
* Backlinks list only includes sources the viewer can access.
* Graph query filters out private targets/sources not shared with viewer.
* When creating a note via link in a shared space, new note inherits **workspace** context (if applicable) or remains private (prompt user).
---
## Migrations & Seeds
* SQL above as `V2025_10_04__note_links.sql`.
* Backfill job: scan all notes to populate initial links; run idempotently.
---
## Telemetry & Monitoring
* Events: `link_created`, `backlink_opened`, `graph_opened`, `graph_follow_edge`.
* Prometheus: counter for `links_upsert_total`, histogram for parse latency.
* Log parse failure rate <1%.
---
## Tests
* Parser unit tests (edge cases: nested brackets, punctuation, accents).
* Service integration: upsert creates/deletes rows correctly.
* GraphQL e2e: backlinks visibility with/without sharing.
* Frontend RTL: autocomplete, sidebar rendering, graph node click.
---
## Rollout
1. Feature flag `FEATURE_BACKLINKS=true`.
2. Migrate DB; backfill links offline.
3. Enable for internal, then 20% of users; monitor performance.
4. Full rollout + “What’s new” tooltip.
---
## Acceptance Criteria
* Typing `[[Some Note]]` links, saving creates an edge; opening **Some Note** shows a backlink with an excerpt.
* Autocomplete lists existing titles; creating a new one works and opens the new note.
* Graph view loads ≤1s for ≤500 nodes in local tests; interactive filters work.
* Realtime updates: if user A links to B, user B’s open Backlinks panel updates without refresh.
* Permissions enforced: no leakage of private note titles/content.
---
## Tasks
### Backend
* [ ] Create migrations and indices.
* [ ] Implement parser + link upsert service.
* [ ] REST + GraphQL resolvers.
* [ ] Supabase Realtime triggers/publishers.
* [ ] E2E tests (Jest).
### Frontend
* [ ] Editor autocomplete + hover preview.
* [ ] Backlinks & Mentions panels.
* [ ] Graph page + D3 component.
* [ ] Realtime subscriptions.
* [ ] RTL + Cypress coverage.
### DevOps / Docs
* [ ] Env flags: `FEATURE_BACKLINKS`, `REALTIME_ENABLED`.
* [ ] Swagger & README updates.
* [ ] Dashboards/alerts for parse errors.
---
## Estimated Effort (parallelizable)
* Backend: 3–5 days
* Frontend: 5–7 days
* QA/Docs/DevOps: 2 days
---
## Risks & Mitigations
* **Large graphs slow UI:** limit depth by default, virtualize lists, lazy load neighbors.
* **Ambiguous titles:** show note ID in autocomplete tooltip; consider unique slugs (v2).
* **Permission edge cases:** centralize visibility checks in service layer; add e2e coverage.
---
Contributor guide
Assessment
This issue has not been assessed yet.