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.

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:

// 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#

-- 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:

{
  "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:

{
  "id": 1,
  "username": "johndoe",
  "avatar_url": "https://avatars.githubusercontent.com/...",
  "provider": "github"
}

POST /api/auth/logout#

Logout and clear session.

Response:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "content": "# Updated content...",
  "frontmatter": {
    "title": "Updated Title",
    "date": "2026-01-19"
  }
}

Response:

{
  "success": true,
  "draft_saved": true
}

Branch & Publishing Endpoints#

GET /api/repos/:owner/:repo/branch/status#

Get current branch state for this repo.

Response:

{
  "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:

{
  "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:

{
  "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:

{
  "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#

interface TipTapEditorProps {
  initialContent: string;
  initialFrontmatter: Record<string, any>;
  onSave: (content: string, frontmatter: Record<string, any>) => void;
  autoSave?: boolean;
  autoSaveInterval?: number; // milliseconds
}

FileTree.tsx#

interface FileTreeProps {
  files: FileNode[];
  selectedFile: string | null;
  onFileSelect: (path: string) => void;
  loading?: boolean;
}

Pages#

dashboard.astro#

Main application shell. Mounts React app client-side.

---
// No SSR, all client-side
---
<html>
  <head>
    <title>MarkEdit</title>
  </head>
  <body>
    <div id="app"></div>
    <script>
      import App from '../components/App.tsx';
      import { createRoot } from 'react-dom/client';

      const root = createRoot(document.getElementById('app'));
      root.render(<App />);
    </script>
  </body>
</html>

State Management#

Use React Query for server state:

// 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)#

// 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:

  • Project setup (Go modules, directory structure)
  • SQLite database setup with migrations
  • GitHub OAuth integration with goth
  • Session management with encrypted cookies
  • Basic HTTP server with chi
  • Health check endpoint
  • GET /api/auth/github/login
  • GET /api/auth/github/callback
  • GET /api/auth/user
  • GET /api/repos (list repositories)
  • GET /api/repos/:owner/:repo/files (list files)

Frontend Tasks:

  • Astro project setup
  • Tailwind CSS configuration
  • shadcn/ui setup and initial components (button, dialog, input, etc.)
  • Landing page with "Login with GitHub" button
  • OAuth callback handler
  • Dashboard shell (empty)
  • Repository selector dropdown
  • File tree component (read-only)
  • API client setup (axios with interceptors)

Testing:

  • Manual: Complete OAuth flow
  • Manual: View list of repositories
  • 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:

  • GET /api/repos/:owner/:repo/files/*path (get file content)
  • Markdown + YAML frontmatter parser
  • PUT /api/repos/:owner/:repo/files/*path (save to draft)
  • Draft content storage in SQLite

Frontend Tasks:

  • TipTap editor setup
  • Markdown extensions for TipTap
  • Frontmatter editor component (form)
  • File selection handler
  • Content display/edit toggle
  • Auto-save functionality (debounced)
  • Unsaved changes warning

Testing:

  • Manual: Open a markdown file
  • Manual: Edit content and see auto-save
  • 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:

  • go-git integration
  • Repository cloning/caching logic
  • Branch manager implementation
  • GET /api/repos/:owner/:repo/branch/status
  • POST /api/repos/:owner/:repo/publish
  • Commit creation
  • Push to remote
  • Pull request creation via GitHub API
  • Branch state tracking in SQLite

Frontend Tasks:

  • Branch status display in UI
  • "Publish" button with confirmation modal
  • Commit message input
  • PR title/description inputs
  • Success notification with PR link
  • Error handling for conflicts

Testing:

  • 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#

# 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:

# .env.development
FRONTEND_URL=http://localhost:3000
SESSION_SECURE=false
LOG_LEVEL=debug

Production:

# .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#

# 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#

# 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#


Document Version: 1.0 Last Updated: 2026-01-19 Status: Draft - Ready for Implementation