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/sessionswith 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-queryfor server state - HTTP Client:
axiosorfetch - Build: Vite (via Astro)
Deployment#
- Container: Docker (multi-stage build)
- Base Image:
alpineorscratchfor 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 codestate: 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#
- Go to https://github.com/settings/developers
- Create new OAuth App
- Set Authorization callback URL:
http://localhost:8080/api/auth/github/callback - 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#
-
OAuth Token Storage:
- Encrypted in SQLite using AES-256
- Never exposed to frontend
- Backend proxies all GitHub API calls
-
Session Management:
- HTTP-only cookies
- Secure flag in production (HTTPS)
- CSRF protection via SameSite=Lax
- Encrypted session data
-
CORS:
- Whitelist specific origins
- Credentials included in requests
-
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#
- Repository Size Limits: What's the max repo size we'll support?
- Concurrent Editing: How to handle multiple browser tabs?
- Image Embedding: When we add image uploads, where to store (Git LFS, S3)?
- MDX Support: Custom TipTap extensions or separate editor mode?
- Mobile Support: Native app or PWA?
- Offline Mode: Service worker for offline editing?
- 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#
- Set up development environment (Go, Node.js, Docker)
- Create GitHub OAuth app for testing
- Start with Phase 1: Backend authentication
- Daily commits to track progress
- 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