Skip to content

Commit 487bb34

Browse files
docs: finalize product spec, architecture, and design for MVP buildout (#18)
- Rewrite docs/product-spec.md with all decisions locked in: Lexical editor, personal + team workspaces (3 limit), email/password auth, realtime deferred, members in scope - Add data model (5 tables), acceptance criteria, URL routing, explicit dependency chains for Feature Planner issue decomposition - Update .agents/architecture.md: Lexical implementation plan (packages, plugins, custom nodes), corrected data model, planned route structure - Fix .agents/design.md: remove 'color' from toolbar items to match product spec acceptance criteria Co-authored-by: Ona <no-reply@ona.com>
1 parent b136ec4 commit 487bb34

3 files changed

Lines changed: 371 additions & 61 deletions

File tree

.agents/architecture.md

Lines changed: 133 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,25 @@
22

33
## Overview
44

5-
Memo is a Notion-style workspace app: block-based editor, nested pages, workspaces,
6-
real-time collaboration. Built with Next.js 16 on Vercel, Supabase for data and auth,
7-
Sentry for error tracking.
5+
Memo is a Notion-style workspace app: Lexical block editor, nested pages, personal +
6+
team workspaces, member invitations. Built with Next.js 16 on Vercel, Supabase for
7+
data and auth, Sentry for error tracking. Realtime collaboration is deferred to post-MVP.
88

99
## System Diagram
1010

1111
```
1212
Browser
1313
├── Server Components → Supabase server client (read data, cookie-based auth)
14-
├── Client Components → Supabase browser client (mutations, realtime subscriptions)
14+
├── Client Components → Supabase browser client (mutations, Lexical editor)
1515
├── API Routes (/api/*) → server-side logic, health checks
1616
└── Proxy (src/proxy.ts) → Supabase session refresh, route protection
1717
1818
Supabase
19-
├── PostgreSQL → workspaces, pages, blocks, members
20-
├── Auth → GitHub/Google OAuth, email/password
21-
├── Realtime → live collaboration (page edits, presence)
22-
└── RLS → row-level security per workspace
19+
├── PostgreSQL → profiles, workspaces, members, workspace_invites, pages
20+
├── Auth → email/password (OAuth deferred — buttons rendered with "coming soon")
21+
├── Storage → image uploads for editor
22+
├── DB Triggers → handle_new_user (profile + personal workspace + owner membership)
23+
└── RLS → row-level security per workspace membership
2324
2425
Sentry → error tracking, source maps, performance monitoring, session replay
2526
Vercel → hosting, preview deploys per PR, production deploys on merge
@@ -29,45 +30,123 @@ Vercel → hosting, preview deploys per PR, production deploys on merge
2930

3031
No database migrations exist yet. The schema below is the planned target model.
3132
Create migrations in `supabase/migrations/` as features are implemented.
33+
See `docs/product-spec.md` → Data Model for the full column-level schema.
3234

3335
```
34-
workspace
35-
├── has many: members (user_id + role)
36+
profiles (1:1 with auth.users)
37+
└── created automatically on sign-up via handle_new_user trigger
38+
39+
workspaces
40+
├── is_personal: boolean (personal workspace = non-deletable, always listed first)
41+
├── created_by → profiles.id
42+
├── Constraint: max 3 workspaces per user (created_by count, enforced via DB trigger)
43+
├── Constraint: one personal workspace per user (partial unique index)
44+
├── has many: members (user_id + role: owner | admin | member)
45+
├── has many: workspace_invites (email + role + token)
3646
└── has many: pages
37-
├── belongs to: workspace
38-
├── has one: parent_page (nullable → enables nesting)
39-
└── has many: blocks (ordered by position)
40-
├── type: text | heading_1 | heading_2 | heading_3 | bullet_list |
41-
│ numbered_list | todo | code | image | divider | callout | toggle
42-
└── content: JSON (structure varies by type)
47+
├── parent_id → pages.id (nullable, enables nesting)
48+
├── content: jsonb (Lexical editor state — NOT a separate blocks table)
49+
├── position: integer (ordering among siblings)
50+
└── created_by → profiles.id
51+
52+
Sign-up flow (atomic, via DB trigger):
53+
1. auth.users row created by Supabase Auth
54+
2. handle_new_user trigger fires → creates:
55+
a. profiles row
56+
b. workspaces row (is_personal = true, name = "{display_name}'s Workspace")
57+
c. members row (role = owner)
4358
```
4459

4560
## Key Technical Decisions
4661

4762
| Decision | Choice | Rationale |
4863
|---|---|---|
49-
| Editor library | Tiptap or BlockNote (evaluate before building) | Do NOT build a custom editor — this is months of work |
50-
| Content storage | JSON block tree in PostgreSQL | Flexible, queryable, no HTML parsing needed |
51-
| Auth | Supabase Auth with RLS | Row-level security at the DB layer, no app-level auth checks needed per query |
52-
| Realtime | Supabase Realtime subscriptions | Built into the client, no extra infrastructure |
64+
| Editor library | **Lexical** (Meta, MIT) | Full control, MIT license, Meta-backed. Build from lexical-playground reference, adapt to Tailwind + shadcn/ui. |
65+
| Content storage | Lexical JSON in PostgreSQL `jsonb` | `editorState.toJSON()` stored in `pages.content`. No separate blocks table. |
66+
| Auth | Supabase Auth — email/password | OAuth (GitHub, Google) deferred to post-MVP. Buttons rendered with "coming soon" tooltip. |
67+
| Workspace model | Personal + team workspaces | Auto-created personal workspace on sign-up (non-deletable). Max 3 created workspaces per user. Unlimited joined via invite. |
68+
| Realtime | Deferred to post-MVP | Yjs + Supabase Realtime adds complexity. Ship single-user editing first. |
5369
| Styling | Tailwind v4 + shadcn/ui | No custom CSS, consistent design system |
5470
| Package manager | pnpm | Strict dependency resolution, faster installs |
5571
| Session management | Next.js 16 proxy (not middleware) | `src/proxy.ts` with `updateSession` — Next.js 16 convention replacing middleware |
72+
| Floating UI | `@floating-ui/react` | Positioning for slash command menu, floating toolbar, link editor (same as Lexical playground) |
73+
| Image storage | Supabase Storage | Bucket for uploaded images, public URL stored in ImageNode |
74+
| Full-text search | PostgreSQL `tsvector` + `tsquery` | Generated column or trigger on page title + extracted content text |
75+
76+
## Lexical Editor — Implementation Plan
77+
78+
The Lexical playground (`facebook/lexical/packages/lexical-playground`) is the reference.
79+
We do NOT fork it. We build a Memo-specific editor using official Lexical packages,
80+
referencing the playground for patterns. All UI rebuilt with Tailwind + shadcn/ui.
81+
82+
### Official Lexical packages (install from npm)
83+
84+
| Package | Purpose |
85+
|---|---|
86+
| `lexical` | Core editor engine |
87+
| `@lexical/react` | React bindings (LexicalComposer, plugins, hooks) |
88+
| `@lexical/rich-text` | Headings, quotes, rich text support |
89+
| `@lexical/list` | Bullet, numbered, and check lists |
90+
| `@lexical/code` | Code blocks |
91+
| `@lexical/link` | Link nodes and auto-linking |
92+
| `@lexical/selection` | Selection utilities |
93+
| `@lexical/utils` | Shared utilities |
94+
| `@lexical/markdown` | Markdown import/export transforms |
95+
| `@lexical/clipboard` | Copy/paste handling |
96+
97+
Pin to a specific version to avoid breaking changes.
98+
99+
### Custom plugins (referencing playground)
100+
101+
| Plugin | Playground reference | Complexity |
102+
|---|---|---|
103+
| ComponentPickerPlugin (slash commands) | `plugins/ComponentPickerPlugin` | Medium |
104+
| DraggableBlockPlugin (drag-and-drop) | `plugins/DraggableBlockPlugin` | High |
105+
| FloatingTextFormatToolbarPlugin | `plugins/FloatingTextFormatToolbarPlugin` | High |
106+
| FloatingLinkEditorPlugin | `plugins/FloatingLinkEditorPlugin` | Medium |
107+
| ImagesExtension | `plugins/ImagesExtension` | High |
108+
| CodeHighlightExtension | `plugins/CodeHighlightExtension` | Medium |
109+
| CollapsibleExtension (toggle blocks) | `plugins/CollapsibleExtension` | Medium |
110+
| ToolbarPlugin (top toolbar) | `plugins/ToolbarPlugin` | High |
111+
112+
### Custom nodes
113+
114+
| Node | Type | Purpose |
115+
|---|---|---|
116+
| ImageNode | DecoratorNode | Image display with caption, resize handles |
117+
| CalloutNode | ElementNode | Callout/alert block with emoji + text |
118+
| DividerNode | HorizontalRuleNode (`@lexical/extension`) | Horizontal divider |
119+
120+
### Skipped plugins (not needed for MVP)
121+
122+
ExcalidrawPlugin, EquationsPlugin, PollPlugin, FigmaExtension, MentionsPlugin,
123+
SpeechToTextPlugin, AutocompletePlugin, CommentPlugin, TablePlugin, LayoutPlugin,
124+
DateTimeExtension, VersionsPlugin.
125+
126+
### Content storage flow
127+
128+
```
129+
Save: editor.getEditorState().toJSON() → Supabase pages.content (jsonb)
130+
Load: editor.setEditorState(editor.parseEditorState(json))
131+
Auto-save: debounce 500ms on editor change → write to Supabase
132+
```
56133

57134
## Request Flow
58135

59136
1. User visits a page → proxy (`src/proxy.ts`) refreshes Supabase session via `updateSession`
60137
2. Server component renders with data from Supabase server client (`@/lib/supabase/server`)
61-
3. Client component hydrates, subscribes to Realtime for live updates
62-
4. User edits a block → client component writes to Supabase → Realtime broadcasts to other users
138+
3. Client component hydrates, initializes Lexical editor with saved content from `pages.content`
139+
4. User edits content → Lexical editor state changes → debounced auto-save writes `editorState.toJSON()` to Supabase
63140
5. Errors captured by Sentry (client via `instrumentation-client.ts`, server via `src/instrumentation.ts`)
64141

65142
## Component Map
66143

144+
Current state (infrastructure only — no product features yet):
145+
67146
```
68147
src/
69148
├── app/ # Next.js App Router
70-
│ ├── layout.tsx # Root layout (Geist fonts, global styles)
149+
│ ├── layout.tsx # Root layout (currently Geist fonts — will switch to JetBrains Mono)
71150
│ ├── page.tsx # Landing page
72151
│ ├── manifest.ts # PWA manifest (name, icons, display mode)
73152
│ ├── global-error.tsx # Sentry error boundary
@@ -88,6 +167,37 @@ Root config files:
88167
└── sentry.client.config.ts # Sentry client SDK config
89168
```
90169

170+
Planned structure (added as features are built):
171+
172+
```
173+
src/
174+
├── app/
175+
│ ├── (auth)/ # Unauthenticated routes
176+
│ │ ├── sign-in/page.tsx # /sign-in
177+
│ │ ├── sign-up/page.tsx # /sign-up
178+
│ │ └── invite/[token]/page.tsx # /invite/[token]
179+
│ ├── (app)/ # Authenticated routes
180+
│ │ ├── layout.tsx # App shell (sidebar + main content)
181+
│ │ └── [workspaceSlug]/
182+
│ │ ├── page.tsx # /[workspaceSlug] (workspace home)
183+
│ │ ├── [pageId]/page.tsx # /[workspaceSlug]/[pageId] (editor)
184+
│ │ └── settings/
185+
│ │ ├── page.tsx # /[workspaceSlug]/settings
186+
│ │ └── members/page.tsx # /[workspaceSlug]/settings/members
187+
│ └── api/
188+
│ ├── health/ # Existing
189+
│ └── ... # Additional API routes as needed
190+
├── components/
191+
│ ├── ui/ # shadcn/ui components
192+
│ ├── editor/ # Lexical editor + plugins
193+
│ ├── sidebar/ # Sidebar, page tree, workspace switcher
194+
│ └── ... # Feature-specific components
195+
├── lib/
196+
│ ├── supabase/ # Existing
197+
│ └── types.ts # Shared TypeScript types
198+
└── ...
199+
```
200+
91201
## Observability
92202

93203
- **Sentry client**: session replay (10% normal, 100% on error), route transition tracking

.agents/design.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ Each block is a full-width element with consistent vertical spacing.
229229
### Toolbar
230230

231231
- Floating toolbar appears on text selection.
232-
- Items: bold, italic, underline, strikethrough, code, link, color.
232+
- Items: bold, italic, underline, strikethrough, code, link.
233233
- Icon-only buttons, 28px square, `gap-0.5`.
234234
- `bg-popover border border-white/[0.06] shadow-md p-1`. Sharp corners.
235235

0 commit comments

Comments
 (0)