A deployable markdown editor that connects with your self hosted files and lets you edit in a beautiful interface
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: adds implementation plan

usamasulaiman 6e8635be

+1163
+1163
IMPLEMENTATION_PLAN.md
··· 1 + # MarkEdit - Implementation Plan 2 + 3 + ## Project Overview 4 + 5 + MarkEdit is a markdown editor designed for managing blog posts stored in various locations (initially GitHub, later Google Drive, Dropbox, etc.). It provides a WYSIWYG editing experience using TipTap, with git-based version control for GitHub repositories. 6 + 7 + ### Core Features (MVP) 8 + - GitHub OAuth authentication 9 + - Repository and file browsing 10 + - Markdown editing with TipTap 11 + - Automatic branch management 12 + - Commit and Pull Request creation 13 + - Docker-based deployment 14 + 15 + ### Explicitly Out of Scope (MVP) 16 + - Image uploads (links only) 17 + - Search functionality 18 + - Version history UI 19 + - Collaborative editing 20 + - Merge conflict resolution (create new branch instead) 21 + - Preview mode 22 + - Keyboard shortcuts 23 + - SSR (Astro runs in client-only mode) 24 + 25 + ## Technical Stack 26 + 27 + ### Backend 28 + - **Language**: Go 1.21+ 29 + - **Router**: `chi` (lightweight, composable) 30 + - **OAuth**: `goth` (multi-provider support) 31 + - **Git Operations**: `go-git` (native Go git implementation) 32 + - **Database**: SQLite with `modernc.org/sqlite` (pure Go, no CGO) 33 + - **Configuration**: `viper` 34 + - **Sessions**: `gorilla/sessions` with encrypted cookies 35 + 36 + ### Frontend 37 + - **Framework**: Astro (client-only, no SSR) 38 + - **UI Library**: React 18 39 + - **Editor**: TipTap (React) 40 + - **Styling**: Tailwind CSS 41 + - **State Management**: `@tanstack/react-query` for server state 42 + - **HTTP Client**: `axios` or `fetch` 43 + - **Build**: Vite (via Astro) 44 + 45 + ### Deployment 46 + - **Container**: Docker (multi-stage build) 47 + - **Base Image**: `alpine` or `scratch` for Go binary 48 + - **Size Target**: < 50MB 49 + 50 + ## Architecture 51 + 52 + ### High-Level Overview 53 + 54 + ``` 55 + ┌─────────────────┐ 56 + │ Astro Frontend │ (Static React App) 57 + │ (Port 3000) │ 58 + └────────┬────────┘ 59 + │ HTTP/REST 60 + 61 + ┌─────────────────┐ 62 + │ Go Backend │ (Single Binary) 63 + │ (Port 8080) │ 64 + └────────┬────────┘ 65 + 66 + ┌────┴────┬──────────┬──────────┐ 67 + ▼ ▼ ▼ ▼ 68 + SQLite GitHub go-git Sessions 69 + ``` 70 + 71 + ### Component Responsibilities 72 + 73 + **Backend (Go):** 74 + - OAuth flow handling 75 + - GitHub API interactions 76 + - Local git operations (clone, branch, commit, push) 77 + - Branch lifecycle management 78 + - File CRUD operations 79 + - Session management 80 + 81 + **Frontend (Astro/React):** 82 + - User interface 83 + - TipTap editor integration 84 + - File tree rendering 85 + - API client 86 + - Local state management 87 + 88 + ### Connector Architecture 89 + 90 + Future-proof design for multiple storage backends: 91 + 92 + ```go 93 + // pkg/connectors/connector.go 94 + type Connector interface { 95 + // Authentication 96 + Authenticate(ctx context.Context, token string) error 97 + 98 + // Repository/Storage listing 99 + ListRepositories(ctx context.Context) ([]Repository, error) 100 + 101 + // File operations 102 + ListFiles(ctx context.Context, repoID, path string) ([]FileNode, error) 103 + ReadFile(ctx context.Context, repoID, path string) (*FileContent, error) 104 + WriteFile(ctx context.Context, repoID, path string, content []byte) error 105 + 106 + // Version control (optional) 107 + CreateBranch(ctx context.Context, repoID, branchName string) error 108 + CommitChanges(ctx context.Context, repoID, branch, message string, files []string) error 109 + PushBranch(ctx context.Context, repoID, branch string) error 110 + CreatePullRequest(ctx context.Context, repoID, branch, title, description string) (*PullRequest, error) 111 + 112 + // Capabilities 113 + SupportsVersionControl() bool 114 + GetType() string // "github", "gdrive", "dropbox" 115 + } 116 + 117 + // Implementations 118 + type GitHubConnector struct { ... } 119 + // Later: GoogleDriveConnector, DropboxConnector 120 + ``` 121 + 122 + ## Database Schema 123 + 124 + ### SQLite Tables 125 + 126 + ```sql 127 + -- Users table (future: when email auth is added) 128 + CREATE TABLE users ( 129 + id INTEGER PRIMARY KEY AUTOINCREMENT, 130 + github_id INTEGER UNIQUE, 131 + username TEXT NOT NULL, 132 + email TEXT, 133 + avatar_url TEXT, 134 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 135 + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 136 + ); 137 + 138 + -- Sessions/Tokens (encrypted access tokens) 139 + CREATE TABLE auth_tokens ( 140 + id INTEGER PRIMARY KEY AUTOINCREMENT, 141 + user_id INTEGER NOT NULL, 142 + provider TEXT NOT NULL, -- "github", "gdrive", etc. 143 + access_token TEXT NOT NULL, -- encrypted 144 + refresh_token TEXT, -- encrypted 145 + expires_at DATETIME, 146 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 147 + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE 148 + ); 149 + 150 + -- Branch state tracking 151 + CREATE TABLE branch_states ( 152 + id INTEGER PRIMARY KEY AUTOINCREMENT, 153 + user_id INTEGER NOT NULL, 154 + repo_full_name TEXT NOT NULL, -- "owner/repo" 155 + branch_name TEXT NOT NULL, 156 + base_branch TEXT DEFAULT 'main', -- PR target 157 + last_push_at DATETIME NOT NULL, 158 + has_uncommitted_changes BOOLEAN DEFAULT FALSE, 159 + file_paths TEXT, -- JSON array of edited files 160 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 161 + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, 162 + UNIQUE(user_id, repo_full_name), 163 + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE 164 + ); 165 + 166 + -- Work-in-progress content (auto-save buffer) 167 + CREATE TABLE draft_content ( 168 + id INTEGER PRIMARY KEY AUTOINCREMENT, 169 + user_id INTEGER NOT NULL, 170 + repo_full_name TEXT NOT NULL, 171 + file_path TEXT NOT NULL, 172 + content TEXT NOT NULL, 173 + last_saved_at DATETIME DEFAULT CURRENT_TIMESTAMP, 174 + UNIQUE(user_id, repo_full_name, file_path), 175 + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE 176 + ); 177 + 178 + -- Indexes 179 + CREATE INDEX idx_auth_tokens_user_provider ON auth_tokens(user_id, provider); 180 + CREATE INDEX idx_branch_states_user_repo ON branch_states(user_id, repo_full_name); 181 + CREATE INDEX idx_draft_content_user_repo_file ON draft_content(user_id, repo_full_name, file_path); 182 + ``` 183 + 184 + ## API Specification 185 + 186 + ### Authentication Endpoints 187 + 188 + #### `POST /api/auth/github/login` 189 + Start GitHub OAuth flow. 190 + 191 + **Response:** 192 + ```json 193 + { 194 + "auth_url": "https://github.com/login/oauth/authorize?client_id=..." 195 + } 196 + ``` 197 + 198 + #### `GET /api/auth/github/callback` 199 + OAuth callback handler (redirects to frontend). 200 + 201 + **Query Params:** 202 + - `code`: OAuth authorization code 203 + - `state`: CSRF token 204 + 205 + **Redirects to:** `http://localhost:3000/dashboard?token={session_token}` 206 + 207 + #### `GET /api/auth/user` 208 + Get current authenticated user. 209 + 210 + **Headers:** 211 + - `Cookie: session=...` 212 + 213 + **Response:** 214 + ```json 215 + { 216 + "id": 1, 217 + "username": "johndoe", 218 + "avatar_url": "https://avatars.githubusercontent.com/...", 219 + "provider": "github" 220 + } 221 + ``` 222 + 223 + #### `POST /api/auth/logout` 224 + Logout and clear session. 225 + 226 + **Response:** 227 + ```json 228 + { 229 + "success": true 230 + } 231 + ``` 232 + 233 + ### Repository Endpoints 234 + 235 + #### `GET /api/repos` 236 + List user's repositories. 237 + 238 + **Query Params:** 239 + - `type`: `owner` | `collaborator` | `all` (default: `owner`) 240 + - `sort`: `updated` | `created` | `name` (default: `updated`) 241 + 242 + **Response:** 243 + ```json 244 + { 245 + "repositories": [ 246 + { 247 + "id": 12345, 248 + "full_name": "johndoe/blog", 249 + "name": "blog", 250 + "owner": "johndoe", 251 + "private": false, 252 + "default_branch": "main", 253 + "updated_at": "2026-01-19T10:30:00Z" 254 + } 255 + ] 256 + } 257 + ``` 258 + 259 + #### `GET /api/repos/:owner/:repo/files` 260 + List files in repository (recursive). 261 + 262 + **Query Params:** 263 + - `path`: Directory path (default: root) 264 + - `branch`: Branch name (default: default_branch) 265 + - `extensions`: Comma-separated list (e.g., `md,mdx`) 266 + 267 + **Response:** 268 + ```json 269 + { 270 + "files": [ 271 + { 272 + "path": "posts/hello-world.md", 273 + "name": "hello-world.md", 274 + "type": "file", 275 + "size": 1234, 276 + "sha": "abc123..." 277 + }, 278 + { 279 + "path": "posts/drafts", 280 + "name": "drafts", 281 + "type": "dir", 282 + "children": [...] 283 + } 284 + ], 285 + "current_branch": "main" 286 + } 287 + ``` 288 + 289 + #### `GET /api/repos/:owner/:repo/files/*path` 290 + Get file content. 291 + 292 + **Query Params:** 293 + - `branch`: Branch name (optional, defaults to default_branch) 294 + 295 + **Response:** 296 + ```json 297 + { 298 + "content": "# Hello World\n\nThis is my post...", 299 + "frontmatter": { 300 + "title": "Hello World", 301 + "date": "2026-01-19", 302 + "tags": ["intro", "blog"] 303 + }, 304 + "path": "posts/hello-world.md", 305 + "sha": "abc123...", 306 + "branch": "main" 307 + } 308 + ``` 309 + 310 + #### `PUT /api/repos/:owner/:repo/files/*path` 311 + Update file content (doesn't commit immediately, saves to draft). 312 + 313 + **Request Body:** 314 + ```json 315 + { 316 + "content": "# Updated content...", 317 + "frontmatter": { 318 + "title": "Updated Title", 319 + "date": "2026-01-19" 320 + } 321 + } 322 + ``` 323 + 324 + **Response:** 325 + ```json 326 + { 327 + "success": true, 328 + "draft_saved": true 329 + } 330 + ``` 331 + 332 + ### Branch & Publishing Endpoints 333 + 334 + #### `GET /api/repos/:owner/:repo/branch/status` 335 + Get current branch state for this repo. 336 + 337 + **Response:** 338 + ```json 339 + { 340 + "branch_name": "markedit-1705659600", 341 + "base_branch": "main", 342 + "has_changes": true, 343 + "last_push_at": "2026-01-19T09:30:00Z", 344 + "edited_files": ["posts/hello-world.md"], 345 + "hours_since_push": 1.5 346 + } 347 + ``` 348 + 349 + #### `POST /api/repos/:owner/:repo/publish` 350 + Commit changes and create PR. 351 + 352 + **Request Body:** 353 + ```json 354 + { 355 + "commit_message": "Update blog posts", 356 + "pr_title": "Blog updates from MarkEdit", 357 + "pr_description": "Updated the following files:\n- posts/hello-world.md", 358 + "files": ["posts/hello-world.md"] // optional, defaults to all changed files 359 + } 360 + ``` 361 + 362 + **Response:** 363 + ```json 364 + { 365 + "success": true, 366 + "branch": "markedit-1705659600", 367 + "commit_sha": "def456...", 368 + "pull_request": { 369 + "number": 42, 370 + "url": "https://github.com/johndoe/blog/pull/42", 371 + "html_url": "https://github.com/johndoe/blog/pull/42" 372 + } 373 + } 374 + ``` 375 + 376 + ### Health & Info 377 + 378 + #### `GET /api/health` 379 + Health check endpoint. 380 + 381 + **Response:** 382 + ```json 383 + { 384 + "status": "ok", 385 + "version": "0.1.0" 386 + } 387 + ``` 388 + 389 + ## Frontend Structure 390 + 391 + ### File Organization 392 + 393 + ``` 394 + frontend/ 395 + ├── src/ 396 + │ ├── components/ 397 + │ │ ├── editor/ 398 + │ │ │ ├── TipTapEditor.tsx # Main editor component 399 + │ │ │ ├── MenuBar.tsx # Editor toolbar 400 + │ │ │ ├── FrontmatterEditor.tsx # Frontmatter form 401 + │ │ │ └── extensions/ # Custom TipTap extensions 402 + │ │ ├── files/ 403 + │ │ │ ├── FileTree.tsx # Recursive file tree 404 + │ │ │ ├── FileNode.tsx # Single file/folder node 405 + │ │ │ └── RepoSelector.tsx # Dropdown for repo selection 406 + │ │ ├── layout/ 407 + │ │ │ ├── Header.tsx 408 + │ │ │ ├── Sidebar.tsx 409 + │ │ │ └── Layout.tsx 410 + │ │ └── ui/ 411 + │ │ ├── Button.tsx 412 + │ │ ├── Modal.tsx 413 + │ │ └── ... (shadcn/ui components or custom) 414 + │ ├── lib/ 415 + │ │ ├── api/ 416 + │ │ │ ├── client.ts # Axios instance with interceptors 417 + │ │ │ ├── repos.ts # Repository API calls 418 + │ │ │ ├── files.ts # File API calls 419 + │ │ │ └── auth.ts # Auth API calls 420 + │ │ ├── utils/ 421 + │ │ │ ├── markdown.ts # Markdown/frontmatter parsing 422 + │ │ │ ├── debounce.ts 423 + │ │ │ └── formatters.ts 424 + │ │ └── types/ 425 + │ │ ├── api.ts # API response types 426 + │ │ └── editor.ts # Editor-specific types 427 + │ ├── pages/ 428 + │ │ ├── index.astro # Landing/login page 429 + │ │ ├── dashboard.astro # Main app (mounts React) 430 + │ │ └── callback.astro # OAuth callback handler 431 + │ ├── stores/ 432 + │ │ ├── authStore.ts # Zustand or Context for auth 433 + │ │ └── editorStore.ts # Editor state 434 + │ └── styles/ 435 + │ └── global.css # Tailwind imports 436 + ├── public/ 437 + │ └── favicon.svg 438 + ├── astro.config.mjs 439 + ├── tailwind.config.mjs 440 + ├── tsconfig.json 441 + └── package.json 442 + ``` 443 + 444 + ### Key Components 445 + 446 + #### TipTapEditor.tsx 447 + ```typescript 448 + interface TipTapEditorProps { 449 + initialContent: string; 450 + initialFrontmatter: Record<string, any>; 451 + onSave: (content: string, frontmatter: Record<string, any>) => void; 452 + autoSave?: boolean; 453 + autoSaveInterval?: number; // milliseconds 454 + } 455 + ``` 456 + 457 + #### FileTree.tsx 458 + ```typescript 459 + interface FileTreeProps { 460 + files: FileNode[]; 461 + selectedFile: string | null; 462 + onFileSelect: (path: string) => void; 463 + loading?: boolean; 464 + } 465 + ``` 466 + 467 + ### Pages 468 + 469 + #### `dashboard.astro` 470 + Main application shell. Mounts React app client-side. 471 + 472 + ```astro 473 + --- 474 + // No SSR, all client-side 475 + --- 476 + <html> 477 + <head> 478 + <title>MarkEdit</title> 479 + </head> 480 + <body> 481 + <div id="app"></div> 482 + <script> 483 + import App from '../components/App.tsx'; 484 + import { createRoot } from 'react-dom/client'; 485 + 486 + const root = createRoot(document.getElementById('app')); 487 + root.render(<App />); 488 + </script> 489 + </body> 490 + </html> 491 + ``` 492 + 493 + ### State Management 494 + 495 + Use React Query for server state: 496 + 497 + ```typescript 498 + // hooks/useRepositories.ts 499 + export function useRepositories() { 500 + return useQuery({ 501 + queryKey: ['repositories'], 502 + queryFn: () => api.repos.list() 503 + }); 504 + } 505 + 506 + // hooks/useFileContent.ts 507 + export function useFileContent(owner: string, repo: string, path: string) { 508 + return useQuery({ 509 + queryKey: ['file', owner, repo, path], 510 + queryFn: () => api.files.get(owner, repo, path), 511 + enabled: !!path 512 + }); 513 + } 514 + 515 + // hooks/useUpdateFile.ts 516 + export function useUpdateFile() { 517 + const queryClient = useQueryClient(); 518 + 519 + return useMutation({ 520 + mutationFn: (params) => api.files.update(params), 521 + onSuccess: () => { 522 + queryClient.invalidateQueries(['file']); 523 + } 524 + }); 525 + } 526 + ``` 527 + 528 + ## Branch Management Logic 529 + 530 + ### Branch Lifecycle 531 + 532 + ``` 533 + ┌─────────────────┐ 534 + │ Start Session │ 535 + └────────┬────────┘ 536 + 537 + 538 + Check DB for 539 + existing branch 540 + 541 + ┌────┴────┐ 542 + │ │ 543 + Found Not Found 544 + │ │ 545 + ▼ ▼ 546 + Check gap Create new 547 + > 4 hours? branch 548 + 549 + ┌───┴───┐ 550 + │ │ 551 + Yes No 552 + │ │ 553 + ▼ ▼ 554 + Delete Reuse 555 + Create branch 556 + new 557 + │ │ 558 + └───┬───┘ 559 + 560 + 561 + ┌─────────────────┐ 562 + │ User edits │ 563 + │ files │ 564 + └────────┬────────┘ 565 + 566 + 567 + Auto-save to 568 + draft table 569 + 570 + 571 + User clicks 572 + "Publish" 573 + 574 + 575 + ┌─────────────────┐ 576 + │ Commit & Push │ 577 + │ Create PR │ 578 + │ Update DB │ 579 + └────────┬────────┘ 580 + 581 + 582 + ┌─────────────────┐ 583 + │ Branch stays │ 584 + │ active for │ 585 + │ 4 hours │ 586 + └─────────────────┘ 587 + ``` 588 + 589 + ### Implementation (Go) 590 + 591 + ```go 592 + // internal/git/branch_manager.go 593 + 594 + type BranchManager struct { 595 + db *sql.DB 596 + git GitOperations 597 + } 598 + 599 + func (bm *BranchManager) GetOrCreateBranch(ctx context.Context, userID int, repoFullName string) (string, error) { 600 + // 1. Check database for existing branch 601 + state, err := bm.getBranchState(ctx, userID, repoFullName) 602 + if err != nil && err != sql.ErrNoRows { 603 + return "", err 604 + } 605 + 606 + // 2. No existing branch 607 + if state == nil { 608 + return bm.createNewBranch(ctx, userID, repoFullName) 609 + } 610 + 611 + // 3. Check time gap 612 + hoursSincePush := time.Since(state.LastPushAt).Hours() 613 + 614 + // 4. Gap > 4 hours and no uncommitted changes 615 + if hoursSincePush > 4 && !state.HasUncommittedChanges { 616 + // Delete old branch locally (not on remote) 617 + _ = bm.git.DeleteLocalBranch(repoFullName, state.BranchName) 618 + return bm.createNewBranch(ctx, userID, repoFullName) 619 + } 620 + 621 + // 5. Reuse existing branch 622 + return state.BranchName, nil 623 + } 624 + 625 + func (bm *BranchManager) createNewBranch(ctx context.Context, userID int, repoFullName string) (string, error) { 626 + branchName := fmt.Sprintf("markedit-%d", time.Now().Unix()) 627 + 628 + // Create branch in git 629 + if err := bm.git.CreateBranch(repoFullName, branchName); err != nil { 630 + return "", err 631 + } 632 + 633 + // Save to database 634 + if err := bm.saveBranchState(ctx, userID, repoFullName, branchName); err != nil { 635 + return "", err 636 + } 637 + 638 + return branchName, nil 639 + } 640 + 641 + func (bm *BranchManager) HandleConflict(ctx context.Context, userID int, repoFullName string) (string, error) { 642 + // Get current branch 643 + state, _ := bm.getBranchState(ctx, userID, repoFullName) 644 + 645 + // Delete old branch locally 646 + if state != nil { 647 + _ = bm.git.DeleteLocalBranch(repoFullName, state.BranchName) 648 + } 649 + 650 + // Create new branch 651 + return bm.createNewBranch(ctx, userID, repoFullName) 652 + } 653 + ``` 654 + 655 + ## Implementation Phases 656 + 657 + ### Phase 1: Foundation (Week 1-2) 658 + **Goal:** Basic authentication and file browsing 659 + 660 + **Backend Tasks:** 661 + - [ ] Project setup (Go modules, directory structure) 662 + - [ ] SQLite database setup with migrations 663 + - [ ] GitHub OAuth integration with goth 664 + - [ ] Session management with encrypted cookies 665 + - [ ] Basic HTTP server with chi 666 + - [ ] Health check endpoint 667 + - [ ] GET /api/auth/github/login 668 + - [ ] GET /api/auth/github/callback 669 + - [ ] GET /api/auth/user 670 + - [ ] GET /api/repos (list repositories) 671 + - [ ] GET /api/repos/:owner/:repo/files (list files) 672 + 673 + **Frontend Tasks:** 674 + - [ ] Astro project setup 675 + - [ ] Tailwind CSS configuration 676 + - [ ] Landing page with "Login with GitHub" button 677 + - [ ] OAuth callback handler 678 + - [ ] Dashboard shell (empty) 679 + - [ ] Repository selector dropdown 680 + - [ ] File tree component (read-only) 681 + - [ ] API client setup (axios with interceptors) 682 + 683 + **Testing:** 684 + - [ ] Manual: Complete OAuth flow 685 + - [ ] Manual: View list of repositories 686 + - [ ] Manual: Browse files in a test repository 687 + 688 + **Deliverable:** User can log in and browse their repositories and files. 689 + 690 + --- 691 + 692 + ### Phase 2: Editor Integration (Week 3-4) 693 + **Goal:** Read and edit markdown files 694 + 695 + **Backend Tasks:** 696 + - [ ] GET /api/repos/:owner/:repo/files/*path (get file content) 697 + - [ ] Markdown + YAML frontmatter parser 698 + - [ ] PUT /api/repos/:owner/:repo/files/*path (save to draft) 699 + - [ ] Draft content storage in SQLite 700 + 701 + **Frontend Tasks:** 702 + - [ ] TipTap editor setup 703 + - [ ] Markdown extensions for TipTap 704 + - [ ] Frontmatter editor component (form) 705 + - [ ] File selection handler 706 + - [ ] Content display/edit toggle 707 + - [ ] Auto-save functionality (debounced) 708 + - [ ] Unsaved changes warning 709 + 710 + **Testing:** 711 + - [ ] Manual: Open a markdown file 712 + - [ ] Manual: Edit content and see auto-save 713 + - [ ] Manual: Switch files and verify draft persistence 714 + 715 + **Deliverable:** User can read and edit markdown files with auto-save. 716 + 717 + --- 718 + 719 + ### Phase 3: Git Operations (Week 5-6) 720 + **Goal:** Branch management and publishing 721 + 722 + **Backend Tasks:** 723 + - [ ] go-git integration 724 + - [ ] Repository cloning/caching logic 725 + - [ ] Branch manager implementation 726 + - [ ] GET /api/repos/:owner/:repo/branch/status 727 + - [ ] POST /api/repos/:owner/:repo/publish 728 + - [ ] Commit creation 729 + - [ ] Push to remote 730 + - [ ] Pull request creation via GitHub API 731 + - [ ] Branch state tracking in SQLite 732 + 733 + **Frontend Tasks:** 734 + - [ ] Branch status display in UI 735 + - [ ] "Publish" button with confirmation modal 736 + - [ ] Commit message input 737 + - [ ] PR title/description inputs 738 + - [ ] Success notification with PR link 739 + - [ ] Error handling for conflicts 740 + 741 + **Testing:** 742 + - [ ] Manual: Edit file, publish, verify PR created 743 + - [ ] Manual: Edit multiple files, verify single PR 744 + - [ ] Manual: Test branch reuse within 4-hour window 745 + - [ ] Manual: Test new branch creation after 4 hours 746 + - [ ] Manual: Simulate conflict, verify new branch 747 + 748 + **Deliverable:** User can publish changes via Pull Requests. 749 + 750 + --- 751 + 752 + ### Phase 4: Polish & Deployment (Week 7-8) 753 + **Goal:** Production-ready Docker deployment 754 + 755 + **Tasks:** 756 + - [ ] Multi-stage Dockerfile 757 + - [ ] Docker Compose setup 758 + - [ ] Environment variable configuration 759 + - [ ] Build scripts 760 + - [ ] Error handling improvements 761 + - [ ] Loading states 762 + - [ ] Empty states (no repos, no files) 763 + - [ ] Responsive design testing 764 + - [ ] Security audit (token encryption, CORS, CSRF) 765 + - [ ] Basic documentation (README, SETUP.md) 766 + - [ ] GitHub Actions for builds (optional) 767 + 768 + **Testing:** 769 + - [ ] Manual: Full end-to-end workflow 770 + - [ ] Manual: Docker build and run 771 + - [ ] Manual: Test on different screen sizes 772 + - [ ] Security: Review auth flow, token storage 773 + 774 + **Deliverable:** Docker image ready for self-hosting. 775 + 776 + --- 777 + 778 + ### Phase 5: MVP Refinements (Week 9-10) 779 + **Goal:** UX improvements and bug fixes 780 + 781 + **Tasks:** 782 + - [ ] User feedback collection 783 + - [ ] Bug fixes from testing 784 + - [ ] Performance optimization (large repos) 785 + - [ ] Better error messages 786 + - [ ] Loading indicators 787 + - [ ] Accessibility improvements (keyboard nav, ARIA) 788 + - [ ] Dark mode support (optional) 789 + - [ ] GitHub Actions for CI/CD 790 + - [ ] Deployment guide for common platforms 791 + 792 + **Deliverable:** Stable MVP ready for open source release. 793 + 794 + ## Configuration 795 + 796 + ### Environment Variables 797 + 798 + ```bash 799 + # Backend (Go) 800 + # Server 801 + PORT=8080 802 + FRONTEND_URL=http://localhost:3000 803 + ALLOWED_ORIGINS=http://localhost:3000 804 + 805 + # GitHub OAuth 806 + GITHUB_CLIENT_ID=your_github_client_id 807 + GITHUB_CLIENT_SECRET=your_github_client_secret 808 + GITHUB_REDIRECT_URL=http://localhost:8080/api/auth/github/callback 809 + 810 + # Session 811 + SESSION_SECRET=your-random-session-secret-min-32-chars 812 + SESSION_SECURE=false # true in production (HTTPS only) 813 + SESSION_MAX_AGE=86400 # 24 hours 814 + 815 + # Database 816 + DATABASE_PATH=./data/markedit.db 817 + 818 + # Git 819 + GIT_CACHE_DIR=./data/repos 820 + GIT_AUTHOR_NAME=MarkEdit 821 + GIT_AUTHOR_EMAIL=markedit@example.com 822 + 823 + # Logging 824 + LOG_LEVEL=info # debug, info, warn, error 825 + ``` 826 + 827 + ### Development vs Production 828 + 829 + **Development:** 830 + ```bash 831 + # .env.development 832 + FRONTEND_URL=http://localhost:3000 833 + SESSION_SECURE=false 834 + LOG_LEVEL=debug 835 + ``` 836 + 837 + **Production:** 838 + ```bash 839 + # .env.production 840 + FRONTEND_URL=https://markedit.example.com 841 + SESSION_SECURE=true 842 + LOG_LEVEL=info 843 + ALLOWED_ORIGINS=https://markedit.example.com 844 + ``` 845 + 846 + ## File Structure (Complete) 847 + 848 + ``` 849 + markedit/ 850 + ├── backend/ 851 + │ ├── cmd/ 852 + │ │ └── server/ 853 + │ │ └── main.go # Entry point 854 + │ ├── internal/ 855 + │ │ ├── api/ 856 + │ │ │ ├── handlers/ 857 + │ │ │ │ ├── auth.go # Auth handlers 858 + │ │ │ │ ├── repos.go # Repository handlers 859 + │ │ │ │ ├── files.go # File handlers 860 + │ │ │ │ └── branch.go # Branch/publish handlers 861 + │ │ │ ├── middleware/ 862 + │ │ │ │ ├── auth.go # Auth middleware 863 + │ │ │ │ ├── cors.go # CORS middleware 864 + │ │ │ │ └── logger.go # Logging middleware 865 + │ │ │ └── router.go # Route setup 866 + │ │ ├── auth/ 867 + │ │ │ ├── github.go # GitHub OAuth setup 868 + │ │ │ └── session.go # Session management 869 + │ │ ├── connectors/ 870 + │ │ │ ├── connector.go # Interface definition 871 + │ │ │ ├── github.go # GitHub implementation 872 + │ │ │ └── factory.go # Connector factory 873 + │ │ ├── git/ 874 + │ │ │ ├── operations.go # Git operations (clone, commit, push) 875 + │ │ │ ├── branch_manager.go # Branch lifecycle logic 876 + │ │ │ └── cache.go # Repository caching 877 + │ │ ├── database/ 878 + │ │ │ ├── db.go # Database connection 879 + │ │ │ ├── migrations/ 880 + │ │ │ │ └── 001_initial.sql 881 + │ │ │ ├── queries/ 882 + │ │ │ │ ├── users.sql 883 + │ │ │ │ ├── auth_tokens.sql 884 + │ │ │ │ ├── branch_states.sql 885 + │ │ │ │ └── draft_content.sql 886 + │ │ │ └── models.go # Data models 887 + │ │ ├── markdown/ 888 + │ │ │ ├── parser.go # Frontmatter parser 889 + │ │ │ └── serializer.go # Frontmatter serializer 890 + │ │ └── config/ 891 + │ │ └── config.go # Configuration loading 892 + │ ├── pkg/ 893 + │ │ └── types/ 894 + │ │ ├── api.go # API types 895 + │ │ └── errors.go # Custom errors 896 + │ ├── migrations/ 897 + │ │ └── 001_initial.sql 898 + │ ├── go.mod 899 + │ ├── go.sum 900 + │ └── Makefile 901 + ├── frontend/ 902 + │ ├── src/ 903 + │ │ ├── components/ 904 + │ │ │ ├── editor/ 905 + │ │ │ │ ├── TipTapEditor.tsx 906 + │ │ │ │ ├── MenuBar.tsx 907 + │ │ │ │ ├── FrontmatterEditor.tsx 908 + │ │ │ │ └── extensions/ 909 + │ │ │ │ ├── markdown.ts 910 + │ │ │ │ └── codeBlock.ts 911 + │ │ │ ├── files/ 912 + │ │ │ │ ├── FileTree.tsx 913 + │ │ │ │ ├── FileNode.tsx 914 + │ │ │ │ └── RepoSelector.tsx 915 + │ │ │ ├── layout/ 916 + │ │ │ │ ├── Header.tsx 917 + │ │ │ │ ├── Sidebar.tsx 918 + │ │ │ │ └── Layout.tsx 919 + │ │ │ ├── ui/ 920 + │ │ │ │ ├── Button.tsx 921 + │ │ │ │ ├── Modal.tsx 922 + │ │ │ │ ├── Spinner.tsx 923 + │ │ │ │ └── Alert.tsx 924 + │ │ │ └── App.tsx # Main React app 925 + │ │ ├── lib/ 926 + │ │ │ ├── api/ 927 + │ │ │ │ ├── client.ts 928 + │ │ │ │ ├── auth.ts 929 + │ │ │ │ ├── repos.ts 930 + │ │ │ │ └── files.ts 931 + │ │ │ ├── hooks/ 932 + │ │ │ │ ├── useAuth.ts 933 + │ │ │ │ ├── useRepositories.ts 934 + │ │ │ │ ├── useFiles.ts 935 + │ │ │ │ └── useFileContent.ts 936 + │ │ │ ├── utils/ 937 + │ │ │ │ ├── markdown.ts 938 + │ │ │ │ ├── debounce.ts 939 + │ │ │ │ └── formatters.ts 940 + │ │ │ └── types/ 941 + │ │ │ ├── api.ts 942 + │ │ │ └── editor.ts 943 + │ │ ├── stores/ 944 + │ │ │ └── authStore.ts 945 + │ │ ├── pages/ 946 + │ │ │ ├── index.astro 947 + │ │ │ ├── dashboard.astro 948 + │ │ │ └── callback.astro 949 + │ │ └── styles/ 950 + │ │ └── global.css 951 + │ ├── public/ 952 + │ │ └── favicon.svg 953 + │ ├── astro.config.mjs 954 + │ ├── tailwind.config.mjs 955 + │ ├── tsconfig.json 956 + │ └── package.json 957 + ├── docker/ 958 + │ ├── Dockerfile # Multi-stage build 959 + │ └── docker-compose.yml 960 + ├── .github/ 961 + │ └── workflows/ 962 + │ ├── build.yml # CI build 963 + │ └── release.yml # Release automation 964 + ├── docs/ 965 + │ ├── SETUP.md # Setup instructions 966 + │ ├── DEPLOYMENT.md # Deployment guide 967 + │ └── ARCHITECTURE.md # Architecture details 968 + ├── .env.example 969 + ├── .gitignore 970 + ├── README.md 971 + ├── LICENSE 972 + └── IMPLEMENTATION_PLAN.md # This file 973 + ``` 974 + 975 + ## Development Workflow 976 + 977 + ### Initial Setup 978 + 979 + ```bash 980 + # Clone repository 981 + git clone https://github.com/yourusername/markedit.git 982 + cd markedit 983 + 984 + # Backend setup 985 + cd backend 986 + cp ../.env.example .env 987 + # Edit .env with your GitHub OAuth credentials 988 + go mod download 989 + make migrate-up 990 + make run 991 + 992 + # Frontend setup (new terminal) 993 + cd frontend 994 + npm install 995 + npm run dev 996 + 997 + # Visit http://localhost:3000 998 + ``` 999 + 1000 + ### Docker Development 1001 + 1002 + ```bash 1003 + # Build and run 1004 + docker-compose up --build 1005 + 1006 + # Visit http://localhost:3000 1007 + ``` 1008 + 1009 + ### GitHub OAuth Setup 1010 + 1011 + 1. Go to https://github.com/settings/developers 1012 + 2. Create new OAuth App 1013 + 3. Set Authorization callback URL: `http://localhost:8080/api/auth/github/callback` 1014 + 4. Copy Client ID and Client Secret to `.env` 1015 + 1016 + ## Testing Strategy 1017 + 1018 + ### Manual Testing Checklist (MVP) 1019 + 1020 + **Authentication:** 1021 + - [ ] Login with GitHub redirects correctly 1022 + - [ ] Callback handles OAuth code 1023 + - [ ] Session persists across page refreshes 1024 + - [ ] Logout clears session 1025 + - [ ] Unauthorized requests redirect to login 1026 + 1027 + **Repository Browsing:** 1028 + - [ ] List displays all user repositories 1029 + - [ ] Repository selection loads file tree 1030 + - [ ] File tree shows directories and markdown files 1031 + - [ ] File tree filters non-markdown files 1032 + 1033 + **File Editing:** 1034 + - [ ] Selecting file loads content in editor 1035 + - [ ] Frontmatter displays in separate form 1036 + - [ ] Editor shows markdown content 1037 + - [ ] Auto-save triggers after edits 1038 + - [ ] Switching files saves current file 1039 + - [ ] Browser refresh preserves unsaved changes 1040 + 1041 + **Publishing:** 1042 + - [ ] Branch status shows correct information 1043 + - [ ] Publish button enables when changes exist 1044 + - [ ] Commit message is required 1045 + - [ ] PR creation succeeds 1046 + - [ ] PR link is displayed 1047 + - [ ] Branch reuse works within 4 hours 1048 + - [ ] New branch created after 4-hour gap 1049 + - [ ] Conflict creates new branch 1050 + 1051 + **Error Handling:** 1052 + - [ ] Network errors show user-friendly messages 1053 + - [ ] API errors are logged and displayed 1054 + - [ ] 401 errors redirect to login 1055 + - [ ] Invalid file paths show error 1056 + 1057 + ### Future Testing (Post-MVP) 1058 + 1059 + - Unit tests for Go backend (testify) 1060 + - Integration tests for API endpoints 1061 + - E2E tests with Playwright 1062 + - Performance testing with large repositories 1063 + 1064 + ## Security Considerations 1065 + 1066 + ### Current Implementation 1067 + 1068 + 1. **OAuth Token Storage:** 1069 + - Encrypted in SQLite using AES-256 1070 + - Never exposed to frontend 1071 + - Backend proxies all GitHub API calls 1072 + 1073 + 2. **Session Management:** 1074 + - HTTP-only cookies 1075 + - Secure flag in production (HTTPS) 1076 + - CSRF protection via SameSite=Lax 1077 + - Encrypted session data 1078 + 1079 + 3. **CORS:** 1080 + - Whitelist specific origins 1081 + - Credentials included in requests 1082 + 1083 + 4. **Input Validation:** 1084 + - Sanitize file paths 1085 + - Validate repository ownership 1086 + - Limit file sizes 1087 + 1088 + ### Future Enhancements 1089 + 1090 + - Rate limiting 1091 + - Content Security Policy (CSP) 1092 + - Subresource Integrity (SRI) 1093 + - Audit logging 1094 + - RBAC when multi-user support is added 1095 + 1096 + ## Performance Considerations 1097 + 1098 + ### Backend 1099 + 1100 + - **Repository Caching:** Clone repos to local disk, pull for updates 1101 + - **Database Indexes:** On user_id, repo_full_name, file_path 1102 + - **Connection Pooling:** SQLite with WAL mode 1103 + - **Compression:** gzip middleware for API responses 1104 + 1105 + ### Frontend 1106 + 1107 + - **Code Splitting:** Lazy load TipTap editor 1108 + - **Virtualization:** Virtual scrolling for large file trees (react-window) 1109 + - **Debouncing:** Auto-save with 2-second debounce 1110 + - **Memoization:** React.memo for FileNode components 1111 + 1112 + ### Docker 1113 + 1114 + - **Multi-stage Build:** Separate build and runtime images 1115 + - **Layer Caching:** Optimize Dockerfile for faster rebuilds 1116 + - **Image Size:** Use Alpine or scratch base 1117 + 1118 + ## Open Questions & Future Decisions 1119 + 1120 + 1. **Repository Size Limits:** What's the max repo size we'll support? 1121 + 2. **Concurrent Editing:** How to handle multiple browser tabs? 1122 + 3. **Image Embedding:** When we add image uploads, where to store (Git LFS, S3)? 1123 + 4. **MDX Support:** Custom TipTap extensions or separate editor mode? 1124 + 5. **Mobile Support:** Native app or PWA? 1125 + 6. **Offline Mode:** Service worker for offline editing? 1126 + 7. **Collaboration:** Real-time editing with WebSockets/CRDTs? 1127 + 1128 + ## Success Metrics (Post-Launch) 1129 + 1130 + - **Technical:** 1131 + - Docker image < 50MB 1132 + - API response time < 200ms (p95) 1133 + - File tree render < 500ms for 1000 files 1134 + - Auto-save latency < 100ms 1135 + 1136 + - **User Experience:** 1137 + - OAuth flow completion rate > 90% 1138 + - Time to first edit < 30 seconds 1139 + - PR creation success rate > 95% 1140 + 1141 + ## Next Steps 1142 + 1143 + 1. **Set up development environment** (Go, Node.js, Docker) 1144 + 2. **Create GitHub OAuth app** for testing 1145 + 3. **Start with Phase 1:** Backend authentication 1146 + 4. **Daily commits** to track progress 1147 + 5. **Weekly demos** to validate UX 1148 + 1149 + --- 1150 + 1151 + ## Additional Resources 1152 + 1153 + - **TipTap:** https://tiptap.dev/docs 1154 + - **go-git:** https://github.com/go-git/go-git 1155 + - **goth:** https://github.com/markbates/goth 1156 + - **Astro:** https://docs.astro.build 1157 + - **React Query:** https://tanstack.com/query/latest 1158 + 1159 + --- 1160 + 1161 + **Document Version:** 1.0 1162 + **Last Updated:** 2026-01-19 1163 + **Status:** Draft - Ready for Implementation