# MarkEdit - Implementation Plan ## Project Overview 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. ### Core Features (MVP) - GitHub OAuth authentication - Repository and file browsing - Markdown editing with TipTap - Automatic branch management - Commit and Pull Request creation - Docker-based deployment ### Explicitly Out of Scope (MVP) - Image uploads (links only) - Search functionality - Version history UI - Collaborative editing - Merge conflict resolution (create new branch instead) - Preview mode - Keyboard shortcuts - SSR (Astro runs in client-only mode) ## Technical Stack ### Backend - **Language**: Go 1.21+ - **Router**: `chi` (lightweight, composable) - **OAuth**: `goth` (multi-provider support) - **Git Operations**: `go-git` (native Go git implementation) - **Database**: SQLite with `modernc.org/sqlite` (pure Go, no CGO) - **Configuration**: `viper` - **Sessions**: `gorilla/sessions` with encrypted cookies ### Frontend - **Framework**: Astro (client-only, no SSR) - **UI Library**: React 18 - **UI Components**: shadcn/ui (with Tailwind CSS) - **Editor**: TipTap (React) - **Styling**: Tailwind CSS - **State Management**: `@tanstack/react-query` for server state - **HTTP Client**: `axios` or `fetch` - **Build**: Vite (via Astro) ### Deployment - **Container**: Docker (multi-stage build) - **Base Image**: `alpine` or `scratch` for Go binary - **Size Target**: < 50MB ## Architecture ### High-Level Overview ``` ┌─────────────────┐ │ Astro Frontend │ (Static React App) │ (Port 3000) │ └────────┬────────┘ │ HTTP/REST ▼ ┌─────────────────┐ │ Go Backend │ (Single Binary) │ (Port 8080) │ └────────┬────────┘ │ ┌────┴────┬──────────┬──────────┐ ▼ ▼ ▼ ▼ SQLite GitHub go-git Sessions ``` ### Component Responsibilities **Backend (Go):** - OAuth flow handling - GitHub API interactions - Local git operations (clone, branch, commit, push) - Branch lifecycle management - File CRUD operations - Session management **Frontend (Astro/React):** - User interface - TipTap editor integration - File tree rendering - API client - Local state management ### Connector Architecture Future-proof design for multiple storage backends: ```go // pkg/connectors/connector.go type Connector interface { // Authentication Authenticate(ctx context.Context, token string) error // Repository/Storage listing ListRepositories(ctx context.Context) ([]Repository, error) // File operations ListFiles(ctx context.Context, repoID, path string) ([]FileNode, error) ReadFile(ctx context.Context, repoID, path string) (*FileContent, error) WriteFile(ctx context.Context, repoID, path string, content []byte) error // Version control (optional) CreateBranch(ctx context.Context, repoID, branchName string) error CommitChanges(ctx context.Context, repoID, branch, message string, files []string) error PushBranch(ctx context.Context, repoID, branch string) error CreatePullRequest(ctx context.Context, repoID, branch, title, description string) (*PullRequest, error) // Capabilities SupportsVersionControl() bool GetType() string // "github", "gdrive", "dropbox" } // Implementations type GitHubConnector struct { ... } // Later: GoogleDriveConnector, DropboxConnector ``` ## Database Schema ### SQLite Tables ```sql -- Users table (future: when email auth is added) CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, github_id INTEGER UNIQUE, username TEXT NOT NULL, email TEXT, avatar_url TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Sessions/Tokens (encrypted access tokens) CREATE TABLE auth_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, provider TEXT NOT NULL, -- "github", "gdrive", etc. access_token TEXT NOT NULL, -- encrypted refresh_token TEXT, -- encrypted expires_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Branch state tracking CREATE TABLE branch_states ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, repo_full_name TEXT NOT NULL, -- "owner/repo" branch_name TEXT NOT NULL, base_branch TEXT DEFAULT 'main', -- PR target last_push_at DATETIME NOT NULL, has_uncommitted_changes BOOLEAN DEFAULT FALSE, file_paths TEXT, -- JSON array of edited files created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, repo_full_name), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Work-in-progress content (auto-save buffer) CREATE TABLE draft_content ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, repo_full_name TEXT NOT NULL, file_path TEXT NOT NULL, content TEXT NOT NULL, last_saved_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, repo_full_name, file_path), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Indexes CREATE INDEX idx_auth_tokens_user_provider ON auth_tokens(user_id, provider); CREATE INDEX idx_branch_states_user_repo ON branch_states(user_id, repo_full_name); CREATE INDEX idx_draft_content_user_repo_file ON draft_content(user_id, repo_full_name, file_path); ``` ## API Specification ### Authentication Endpoints #### `POST /api/auth/github/login` Start GitHub OAuth flow. **Response:** ```json { "auth_url": "https://github.com/login/oauth/authorize?client_id=..." } ``` #### `GET /api/auth/github/callback` OAuth callback handler (redirects to frontend). **Query Params:** - `code`: OAuth authorization code - `state`: CSRF token **Redirects to:** `http://localhost:3000/dashboard?token={session_token}` #### `GET /api/auth/user` Get current authenticated user. **Headers:** - `Cookie: session=...` **Response:** ```json { "id": 1, "username": "johndoe", "avatar_url": "https://avatars.githubusercontent.com/...", "provider": "github" } ``` #### `POST /api/auth/logout` Logout and clear session. **Response:** ```json { "success": true } ``` ### Repository Endpoints #### `GET /api/repos` List user's repositories. **Query Params:** - `type`: `owner` | `collaborator` | `all` (default: `owner`) - `sort`: `updated` | `created` | `name` (default: `updated`) **Response:** ```json { "repositories": [ { "id": 12345, "full_name": "johndoe/blog", "name": "blog", "owner": "johndoe", "private": false, "default_branch": "main", "updated_at": "2026-01-19T10:30:00Z" } ] } ``` #### `GET /api/repos/:owner/:repo/files` List files in repository (recursive). **Query Params:** - `path`: Directory path (default: root) - `branch`: Branch name (default: default_branch) - `extensions`: Comma-separated list (e.g., `md,mdx`) **Response:** ```json { "files": [ { "path": "posts/hello-world.md", "name": "hello-world.md", "type": "file", "size": 1234, "sha": "abc123..." }, { "path": "posts/drafts", "name": "drafts", "type": "dir", "children": [...] } ], "current_branch": "main" } ``` #### `GET /api/repos/:owner/:repo/files/*path` Get file content. **Query Params:** - `branch`: Branch name (optional, defaults to default_branch) **Response:** ```json { "content": "# Hello World\n\nThis is my post...", "frontmatter": { "title": "Hello World", "date": "2026-01-19", "tags": ["intro", "blog"] }, "path": "posts/hello-world.md", "sha": "abc123...", "branch": "main" } ``` #### `PUT /api/repos/:owner/:repo/files/*path` Update file content (doesn't commit immediately, saves to draft). **Request Body:** ```json { "content": "# Updated content...", "frontmatter": { "title": "Updated Title", "date": "2026-01-19" } } ``` **Response:** ```json { "success": true, "draft_saved": true } ``` ### Branch & Publishing Endpoints #### `GET /api/repos/:owner/:repo/branch/status` Get current branch state for this repo. **Response:** ```json { "branch_name": "markedit-1705659600", "base_branch": "main", "has_changes": true, "last_push_at": "2026-01-19T09:30:00Z", "edited_files": ["posts/hello-world.md"], "hours_since_push": 1.5 } ``` #### `POST /api/repos/:owner/:repo/publish` Commit changes and create PR. **Request Body:** ```json { "commit_message": "Update blog posts", "pr_title": "Blog updates from MarkEdit", "pr_description": "Updated the following files:\n- posts/hello-world.md", "files": ["posts/hello-world.md"] // optional, defaults to all changed files } ``` **Response:** ```json { "success": true, "branch": "markedit-1705659600", "commit_sha": "def456...", "pull_request": { "number": 42, "url": "https://github.com/johndoe/blog/pull/42", "html_url": "https://github.com/johndoe/blog/pull/42" } } ``` ### Health & Info #### `GET /api/health` Health check endpoint. **Response:** ```json { "status": "ok", "version": "0.1.0" } ``` ## Frontend Structure ### File Organization ``` frontend/ ├── src/ │ ├── components/ │ │ ├── editor/ │ │ │ ├── TipTapEditor.tsx # Main editor component │ │ │ ├── MenuBar.tsx # Editor toolbar │ │ │ ├── FrontmatterEditor.tsx # Frontmatter form │ │ │ └── extensions/ # Custom TipTap extensions │ │ ├── files/ │ │ │ ├── FileTree.tsx # Recursive file tree │ │ │ ├── FileNode.tsx # Single file/folder node │ │ │ └── RepoSelector.tsx # Dropdown for repo selection │ │ ├── layout/ │ │ │ ├── Header.tsx │ │ │ ├── Sidebar.tsx │ │ │ └── Layout.tsx │ │ └── ui/ # shadcn/ui components │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── spinner.tsx │ │ └── ... (other shadcn/ui components) │ ├── lib/ │ │ ├── api/ │ │ │ ├── client.ts # Axios instance with interceptors │ │ │ ├── repos.ts # Repository API calls │ │ │ ├── files.ts # File API calls │ │ │ └── auth.ts # Auth API calls │ │ ├── utils/ │ │ │ ├── cn.ts # Tailwind class merging utility (shadcn) │ │ │ ├── markdown.ts # Markdown/frontmatter parsing │ │ │ ├── debounce.ts │ │ │ └── formatters.ts │ │ └── types/ │ │ ├── api.ts # API response types │ │ └── editor.ts # Editor-specific types │ ├── pages/ │ │ ├── index.astro # Landing/login page │ │ ├── dashboard.astro # Main app (mounts React) │ │ └── callback.astro # OAuth callback handler │ ├── stores/ │ │ ├── authStore.ts # Zustand or Context for auth │ │ └── editorStore.ts # Editor state │ └── styles/ │ └── global.css # Tailwind imports ├── public/ │ └── favicon.svg ├── astro.config.mjs ├── tailwind.config.mjs ├── components.json # shadcn/ui configuration ├── tsconfig.json └── package.json ``` ### Key Components #### TipTapEditor.tsx ```typescript interface TipTapEditorProps { initialContent: string; initialFrontmatter: Record; onSave: (content: string, frontmatter: Record) => void; autoSave?: boolean; autoSaveInterval?: number; // milliseconds } ``` #### FileTree.tsx ```typescript interface FileTreeProps { files: FileNode[]; selectedFile: string | null; onFileSelect: (path: string) => void; loading?: boolean; } ``` ### Pages #### `dashboard.astro` Main application shell. Mounts React app client-side. ```astro --- // No SSR, all client-side --- MarkEdit
``` ### State Management Use React Query for server state: ```typescript // hooks/useRepositories.ts export function useRepositories() { return useQuery({ queryKey: ['repositories'], queryFn: () => api.repos.list() }); } // hooks/useFileContent.ts export function useFileContent(owner: string, repo: string, path: string) { return useQuery({ queryKey: ['file', owner, repo, path], queryFn: () => api.files.get(owner, repo, path), enabled: !!path }); } // hooks/useUpdateFile.ts export function useUpdateFile() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (params) => api.files.update(params), onSuccess: () => { queryClient.invalidateQueries(['file']); } }); } ``` ## Branch Management Logic ### Branch Lifecycle ``` ┌─────────────────┐ │ Start Session │ └────────┬────────┘ │ ▼ Check DB for existing branch │ ┌────┴────┐ │ │ Found Not Found │ │ ▼ ▼ Check gap Create new > 4 hours? branch │ ┌───┴───┐ │ │ Yes No │ │ ▼ ▼ Delete Reuse Create branch new │ │ └───┬───┘ │ ▼ ┌─────────────────┐ │ User edits │ │ files │ └────────┬────────┘ │ ▼ Auto-save to draft table │ ▼ User clicks "Publish" │ ▼ ┌─────────────────┐ │ Commit & Push │ │ Create PR │ │ Update DB │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Branch stays │ │ active for │ │ 4 hours │ └─────────────────┘ ``` ### Implementation (Go) ```go // internal/git/branch_manager.go type BranchManager struct { db *sql.DB git GitOperations } func (bm *BranchManager) GetOrCreateBranch(ctx context.Context, userID int, repoFullName string) (string, error) { // 1. Check database for existing branch state, err := bm.getBranchState(ctx, userID, repoFullName) if err != nil && err != sql.ErrNoRows { return "", err } // 2. No existing branch if state == nil { return bm.createNewBranch(ctx, userID, repoFullName) } // 3. Check time gap hoursSincePush := time.Since(state.LastPushAt).Hours() // 4. Gap > 4 hours and no uncommitted changes if hoursSincePush > 4 && !state.HasUncommittedChanges { // Delete old branch locally (not on remote) _ = bm.git.DeleteLocalBranch(repoFullName, state.BranchName) return bm.createNewBranch(ctx, userID, repoFullName) } // 5. Reuse existing branch return state.BranchName, nil } func (bm *BranchManager) createNewBranch(ctx context.Context, userID int, repoFullName string) (string, error) { branchName := fmt.Sprintf("markedit-%d", time.Now().Unix()) // Create branch in git if err := bm.git.CreateBranch(repoFullName, branchName); err != nil { return "", err } // Save to database if err := bm.saveBranchState(ctx, userID, repoFullName, branchName); err != nil { return "", err } return branchName, nil } func (bm *BranchManager) HandleConflict(ctx context.Context, userID int, repoFullName string) (string, error) { // Get current branch state, _ := bm.getBranchState(ctx, userID, repoFullName) // Delete old branch locally if state != nil { _ = bm.git.DeleteLocalBranch(repoFullName, state.BranchName) } // Create new branch return bm.createNewBranch(ctx, userID, repoFullName) } ``` ## Implementation Phases ### Phase 1: Foundation (Week 1-2) ✅ **Goal:** Basic authentication and file browsing **Backend Tasks:** - [x] Project setup (Go modules, directory structure) - [x] SQLite database setup with migrations - [x] GitHub OAuth integration with goth - [x] Session management with encrypted cookies - [x] Basic HTTP server with chi - [x] Health check endpoint - [x] GET /api/auth/github/login - [x] GET /api/auth/github/callback - [x] GET /api/auth/user - [x] GET /api/repos (list repositories) - [x] GET /api/repos/:owner/:repo/files (list files) **Frontend Tasks:** - [x] Astro project setup - [x] Tailwind CSS configuration - [x] shadcn/ui setup and initial components (button, dialog, input, etc.) - [x] Landing page with "Login with GitHub" button - [x] OAuth callback handler - [x] Dashboard shell (empty) - [x] Repository selector dropdown - [x] File tree component (read-only) - [x] API client setup (axios with interceptors) **Testing:** - [x] Manual: Complete OAuth flow - [x] Manual: View list of repositories - [x] Manual: Browse files in a test repository **Deliverable:** User can log in and browse their repositories and files. --- ### Phase 2: Editor Integration (Week 3-4) ✅ **Goal:** Read and edit markdown files **Backend Tasks:** - [x] GET /api/repos/:owner/:repo/files/*path (get file content) - [x] Markdown + YAML frontmatter parser - [x] PUT /api/repos/:owner/:repo/files/*path (save to draft) - [x] Draft content storage in SQLite **Frontend Tasks:** - [x] TipTap editor setup - [x] Markdown extensions for TipTap - [x] Frontmatter editor component (form) - [x] File selection handler - [x] Content display/edit toggle - [x] Auto-save functionality (debounced) - [x] Unsaved changes warning **Testing:** - [x] Manual: Open a markdown file - [x] Manual: Edit content and see auto-save - [x] Manual: Switch files and verify draft persistence **Deliverable:** User can read and edit markdown files with auto-save. --- ### Phase 3: Git Operations (Week 5-6) ✅ **Goal:** Branch management and publishing **Backend Tasks:** - [x] go-git integration - [x] Repository cloning/caching logic - [x] Branch manager implementation - [x] GET /api/repos/:owner/:repo/branch/status - [x] POST /api/repos/:owner/:repo/publish - [x] Commit creation - [x] Push to remote - [x] Pull request creation via GitHub API - [x] Branch state tracking in SQLite **Frontend Tasks:** - [x] Branch status display in UI - [x] "Publish" button with confirmation modal - [x] Commit message input - [x] PR title/description inputs - [x] Success notification with PR link - [x] Error handling for conflicts **Testing:** - [x] Manual: Edit file, publish, verify PR created - [ ] Manual: Edit multiple files, verify single PR - [ ] Manual: Test branch reuse within 4-hour window - [ ] Manual: Test new branch creation after 4 hours - [ ] Manual: Simulate conflict, verify new branch **Deliverable:** User can publish changes via Pull Requests. --- ### Phase 4: Polish & Deployment (Week 7-8) **Goal:** Production-ready Docker deployment **Tasks:** - [ ] Multi-stage Dockerfile - [ ] Docker Compose setup - [ ] Environment variable configuration - [ ] Build scripts - [ ] Error handling improvements - [ ] Loading states - [ ] Empty states (no repos, no files) - [ ] Responsive design testing - [ ] Security audit (token encryption, CORS, CSRF) - [ ] Basic documentation (README, SETUP.md) - [ ] GitHub Actions for builds (optional) **Testing:** - [ ] Manual: Full end-to-end workflow - [ ] Manual: Docker build and run - [ ] Manual: Test on different screen sizes - [ ] Security: Review auth flow, token storage **Deliverable:** Docker image ready for self-hosting. --- ### Phase 5: MVP Refinements (Week 9-10) **Goal:** UX improvements and bug fixes **Tasks:** - [ ] User feedback collection - [ ] Bug fixes from testing - [ ] Performance optimization (large repos) - [ ] Better error messages - [ ] Loading indicators - [ ] Accessibility improvements (keyboard nav, ARIA) - [ ] Dark mode support (optional) - [ ] GitHub Actions for CI/CD - [ ] Deployment guide for common platforms **Deliverable:** Stable MVP ready for open source release. ## Configuration ### Environment Variables ```bash # Backend (Go) # Server PORT=8080 FRONTEND_URL=http://localhost:3000 ALLOWED_ORIGINS=http://localhost:3000 # GitHub OAuth GITHUB_CLIENT_ID=your_github_client_id GITHUB_CLIENT_SECRET=your_github_client_secret GITHUB_REDIRECT_URL=http://localhost:8080/api/auth/github/callback # Session SESSION_SECRET=your-random-session-secret-min-32-chars SESSION_SECURE=false # true in production (HTTPS only) SESSION_MAX_AGE=86400 # 24 hours # Database DATABASE_PATH=./data/markedit.db # Git GIT_CACHE_DIR=./data/repos GIT_AUTHOR_NAME=MarkEdit GIT_AUTHOR_EMAIL=markedit@example.com # Logging LOG_LEVEL=info # debug, info, warn, error ``` ### Development vs Production **Development:** ```bash # .env.development FRONTEND_URL=http://localhost:3000 SESSION_SECURE=false LOG_LEVEL=debug ``` **Production:** ```bash # .env.production FRONTEND_URL=https://markedit.example.com SESSION_SECURE=true LOG_LEVEL=info ALLOWED_ORIGINS=https://markedit.example.com ``` ## File Structure (Complete) ``` markedit/ ├── backend/ │ ├── cmd/ │ │ └── server/ │ │ └── main.go # Entry point │ ├── internal/ │ │ ├── api/ │ │ │ ├── handlers/ │ │ │ │ ├── auth.go # Auth handlers │ │ │ │ ├── repos.go # Repository handlers │ │ │ │ ├── files.go # File handlers │ │ │ │ └── branch.go # Branch/publish handlers │ │ │ ├── middleware/ │ │ │ │ ├── auth.go # Auth middleware │ │ │ │ ├── cors.go # CORS middleware │ │ │ │ └── logger.go # Logging middleware │ │ │ └── router.go # Route setup │ │ ├── auth/ │ │ │ ├── github.go # GitHub OAuth setup │ │ │ └── session.go # Session management │ │ ├── connectors/ │ │ │ ├── connector.go # Interface definition │ │ │ ├── github.go # GitHub implementation │ │ │ └── factory.go # Connector factory │ │ ├── git/ │ │ │ ├── operations.go # Git operations (clone, commit, push) │ │ │ ├── branch_manager.go # Branch lifecycle logic │ │ │ └── cache.go # Repository caching │ │ ├── database/ │ │ │ ├── db.go # Database connection │ │ │ ├── migrations/ │ │ │ │ └── 001_initial.sql │ │ │ ├── queries/ │ │ │ │ ├── users.sql │ │ │ │ ├── auth_tokens.sql │ │ │ │ ├── branch_states.sql │ │ │ │ └── draft_content.sql │ │ │ └── models.go # Data models │ │ ├── markdown/ │ │ │ ├── parser.go # Frontmatter parser │ │ │ └── serializer.go # Frontmatter serializer │ │ └── config/ │ │ └── config.go # Configuration loading │ ├── pkg/ │ │ └── types/ │ │ ├── api.go # API types │ │ └── errors.go # Custom errors │ ├── migrations/ │ │ └── 001_initial.sql │ ├── go.mod │ ├── go.sum │ └── Makefile ├── frontend/ │ ├── src/ │ │ ├── components/ │ │ │ ├── editor/ │ │ │ │ ├── TipTapEditor.tsx │ │ │ │ ├── MenuBar.tsx │ │ │ │ ├── FrontmatterEditor.tsx │ │ │ │ └── extensions/ │ │ │ │ ├── markdown.ts │ │ │ │ └── codeBlock.ts │ │ │ ├── files/ │ │ │ │ ├── FileTree.tsx │ │ │ │ ├── FileNode.tsx │ │ │ │ └── RepoSelector.tsx │ │ │ ├── layout/ │ │ │ │ ├── Header.tsx │ │ │ │ ├── Sidebar.tsx │ │ │ │ └── Layout.tsx │ │ │ ├── ui/ # shadcn/ui components │ │ │ │ ├── button.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── ... (other shadcn/ui components) │ │ │ └── App.tsx # Main React app │ │ ├── lib/ │ │ │ ├── api/ │ │ │ │ ├── client.ts │ │ │ │ ├── auth.ts │ │ │ │ ├── repos.ts │ │ │ │ └── files.ts │ │ │ ├── hooks/ │ │ │ │ ├── useAuth.ts │ │ │ │ ├── useRepositories.ts │ │ │ │ ├── useFiles.ts │ │ │ │ └── useFileContent.ts │ │ │ ├── utils/ │ │ │ │ ├── cn.ts # Tailwind class merging utility │ │ │ │ ├── markdown.ts │ │ │ │ ├── debounce.ts │ │ │ │ └── formatters.ts │ │ │ └── types/ │ │ │ ├── api.ts │ │ │ └── editor.ts │ │ ├── stores/ │ │ │ └── authStore.ts │ │ ├── pages/ │ │ │ ├── index.astro │ │ │ ├── dashboard.astro │ │ │ └── callback.astro │ │ └── styles/ │ │ └── global.css │ ├── public/ │ │ └── favicon.svg │ ├── astro.config.mjs │ ├── tailwind.config.mjs │ ├── components.json # shadcn/ui configuration │ ├── tsconfig.json │ └── package.json ├── docker/ │ ├── Dockerfile # Multi-stage build │ └── docker-compose.yml ├── .github/ │ └── workflows/ │ ├── build.yml # CI build │ └── release.yml # Release automation ├── docs/ │ ├── SETUP.md # Setup instructions │ ├── DEPLOYMENT.md # Deployment guide │ └── ARCHITECTURE.md # Architecture details ├── .env.example ├── .gitignore ├── README.md ├── LICENSE └── IMPLEMENTATION_PLAN.md # This file ``` ## Development Workflow ### Initial Setup ```bash # Clone repository git clone https://github.com/yourusername/markedit.git cd markedit # Backend setup cd backend cp ../.env.example .env # Edit .env with your GitHub OAuth credentials go mod download make migrate-up make run # Frontend setup (new terminal) cd frontend bun install # Initialize shadcn/ui (if not already done) bunx shadcn-ui@latest init bun run dev # Visit http://localhost:3000 ``` ### Docker Development ```bash # Build and run docker-compose up --build # Visit http://localhost:3000 ``` ### GitHub OAuth Setup 1. Go to https://github.com/settings/developers 2. Create new OAuth App 3. Set Authorization callback URL: `http://localhost:8080/api/auth/github/callback` 4. Copy Client ID and Client Secret to `.env` ## Testing Strategy ### Manual Testing Checklist (MVP) **Authentication:** - [ ] Login with GitHub redirects correctly - [ ] Callback handles OAuth code - [ ] Session persists across page refreshes - [ ] Logout clears session - [ ] Unauthorized requests redirect to login **Repository Browsing:** - [ ] List displays all user repositories - [ ] Repository selection loads file tree - [ ] File tree shows directories and markdown files - [ ] File tree filters non-markdown files **File Editing:** - [ ] Selecting file loads content in editor - [ ] Frontmatter displays in separate form - [ ] Editor shows markdown content - [ ] Auto-save triggers after edits - [ ] Switching files saves current file - [ ] Browser refresh preserves unsaved changes **Publishing:** - [ ] Branch status shows correct information - [ ] Publish button enables when changes exist - [ ] Commit message is required - [ ] PR creation succeeds - [ ] PR link is displayed - [ ] Branch reuse works within 4 hours - [ ] New branch created after 4-hour gap - [ ] Conflict creates new branch **Error Handling:** - [ ] Network errors show user-friendly messages - [ ] API errors are logged and displayed - [ ] 401 errors redirect to login - [ ] Invalid file paths show error ### Future Testing (Post-MVP) - Unit tests for Go backend (testify) - Integration tests for API endpoints - E2E tests with Playwright - Performance testing with large repositories ## Security Considerations ### Current Implementation 1. **OAuth Token Storage:** - Encrypted in SQLite using AES-256 - Never exposed to frontend - Backend proxies all GitHub API calls 2. **Session Management:** - HTTP-only cookies - Secure flag in production (HTTPS) - CSRF protection via SameSite=Lax - Encrypted session data 3. **CORS:** - Whitelist specific origins - Credentials included in requests 4. **Input Validation:** - Sanitize file paths - Validate repository ownership - Limit file sizes ### Future Enhancements - Rate limiting - Content Security Policy (CSP) - Subresource Integrity (SRI) - Audit logging - RBAC when multi-user support is added ## Performance Considerations ### Backend - **Repository Caching:** Clone repos to local disk, pull for updates - **Database Indexes:** On user_id, repo_full_name, file_path - **Connection Pooling:** SQLite with WAL mode - **Compression:** gzip middleware for API responses ### Frontend - **Code Splitting:** Lazy load TipTap editor - **Virtualization:** Virtual scrolling for large file trees (react-window) - **Debouncing:** Auto-save with 2-second debounce - **Memoization:** React.memo for FileNode components ### Docker - **Multi-stage Build:** Separate build and runtime images - **Layer Caching:** Optimize Dockerfile for faster rebuilds - **Image Size:** Use Alpine or scratch base ## Open Questions & Future Decisions 1. **Repository Size Limits:** What's the max repo size we'll support? 2. **Concurrent Editing:** How to handle multiple browser tabs? 3. **Image Embedding:** When we add image uploads, where to store (Git LFS, S3)? 4. **MDX Support:** Custom TipTap extensions or separate editor mode? 5. **Mobile Support:** Native app or PWA? 6. **Offline Mode:** Service worker for offline editing? 7. **Collaboration:** Real-time editing with WebSockets/CRDTs? ## Success Metrics (Post-Launch) - **Technical:** - Docker image < 50MB - API response time < 200ms (p95) - File tree render < 500ms for 1000 files - Auto-save latency < 100ms - **User Experience:** - OAuth flow completion rate > 90% - Time to first edit < 30 seconds - PR creation success rate > 95% ## Next Steps 1. **Set up development environment** (Go, Node.js, Docker) 2. **Create GitHub OAuth app** for testing 3. **Start with Phase 1:** Backend authentication 4. **Daily commits** to track progress 5. **Weekly demos** to validate UX --- ## Additional Resources - **TipTap:** https://tiptap.dev/docs - **shadcn/ui:** https://ui.shadcn.com - **Astro with React:** https://docs.astro.build/en/guides/integrations-guide/react/ - **go-git:** https://github.com/go-git/go-git - **goth:** https://github.com/markbates/goth - **Astro:** https://docs.astro.build - **React Query:** https://tanstack.com/query/latest --- **Document Version:** 1.0 **Last Updated:** 2026-01-19 **Status:** Draft - Ready for Implementation