Skip to main content

Backend Architecture

The backend is written in Go using the Wails framework. It handles data storage, clipboard operations, and file system watching.

File Structure

├── main.go                  Entry point, Wails configuration
├── app.go Core application logic, exposed APIs
├── database.go SQLite setup and migrations
├── watcher.go File system watching
├── backup.go ZIP backup and restore
├── plugin_service.go Plugin frontend API (Wails-bound)
├── plugins.go Plugin install/uninstall helpers
├── serve_manager.go Tag serve HTTP server lifecycle and routing
├── serve_json_api.go JSON API handler for served tags (/_api prefix)
├── serve_file_upload.go File upload handler for served tags (/_api/_upload)
├── serve_service.go Tag serve Wails service (start/stop/status)
├── api_manager.go REST API HTTP server and route registration
├── api_service.go REST API Wails service (start/stop/keys)
├── clipboard_service.go Clipboard copy service (Wails-bound)
├── clipboard_darwin.go macOS clipboard implementation
├── clipboard_windows.go Windows clipboard implementation
├── clipboard_other.go Fallback clipboard implementation
├── transfer_service.go Drag-out preparation and native drag initiation
├── transfer_handler.go HTTP handler for transfer file serving
├── transfer_types.go Transfer system type definitions
├── app_transfer_helpers.go Bridge between App and TempClipStore
├── temp_clip_store.go Leased temp file management
├── native_drag_darwin.go macOS native drag implementation
├── native_drag_windows.go Windows native drag implementation
├── native_drag_other.go Fallback native drag stub
├── open_darwin.go macOS open-with-default-app
├── open_windows.go Windows open-with-default-app
├── open_other.go Fallback open-with-default-app stub
├── tag_hierarchy.go Tag tree helpers (parent, root, ancestor, descendant checks)
├── plugin/ Lua plugin system
│ ├── manager.go Plugin lifecycle, event dispatch
│ ├── manifest.go Manifest parsing, validation
│ ├── sandbox.go Sandboxed Lua execution
│ ├── scheduler.go Scheduled/recurring plugin tasks
│ ├── fetch.go Plugin source fetching (URL/file)
│ ├── semver.go Semantic versioning helpers
│ ├── update_checker.go Automatic plugin update checking
│ ├── permission_diff.go Permission change detection across versions
│ └── api_*.go Lua APIs (clips, tags, storage, http, fs, utils, task, toast, image, modal, metadata)
├── go.mod Go module definition
└── go.sum Dependency checksums

Core Components

main.go — Entry Point

Initializes and runs the Wails application:

func main() {
app := NewApp()
pluginService := NewPluginService(app)
clipboardService := NewClipboardService(app)
transferService := NewTransferService(app)
serveService := NewServeService(app)
apiService := NewAPIService(app)

err := wails.Run(&options.App{
Title: "mahpastes",
Width: 1280,
Height: 800,
// ... configuration
Bind: []interface{}{
app, pluginService, clipboardService,
transferService, serveService, apiService,
},
OnStartup: app.startup,
OnShutdown: app.shutdown,
})
}
note

As the source code comment notes, the separate service structs were created to stay under the Wails ~49 method binding limit. In practice the App struct alone has 72+ bound methods and Wails handles this fine, so the limit may not be enforced. Services group related functionality (plugins, clipboard, transfers, tag serving, REST API) into their own files and structs for organizational clarity.

Key configuration:

  • Window size and minimum dimensions
  • Drag and drop enabled
  • Frameless window (macOS)
  • Asset embedding

app.go — Application Logic

The main App struct and all exposed methods:

type App struct {
ctx context.Context
db *sql.DB
tempDir string
tempStore *TempClipStore
transferHandler *TransferFileHandler
mu sync.Mutex
watcherManager *WatcherManager
serveManager *ServeManager
apiManager *APIManager
pluginManager *plugin.Manager
clipboardService *ClipboardService
}

Lifecycle methods:

func (a *App) startup(ctx context.Context)   // Initialize on start
func (a *App) shutdown(ctx context.Context) // Cleanup on exit

Clip operations:

func (a *App) GetClips(archived bool, tagIDs []int64, hiddenTagIDs []int64, sortField string, sortDir string) ([]ClipPreview, error)
func (a *App) GetClipData(id int64) (*ClipData, error)
func (a *App) UploadFiles(files []FileData, expirationMinutes int, autoTagID int64) error
func (a *App) DeleteClip(id int64) error
func (a *App) ToggleArchive(id int64) error

Tag operations (AddTagToClip and BulkAddTag enforce tree exclusivity -- adding a tag removes other tags from the same root tree):

func (a *App) CreateTag(name string) (*Tag, error)
func (a *App) UpdateTag(id int64, name, color string) error
func (a *App) DeleteTag(id int64) error
func (a *App) GetTags() ([]Tag, error)
func (a *App) AddTagToClip(clipID, tagID int64) error // enforces tree exclusivity
func (a *App) RemoveTagFromClip(clipID, tagID int64) error
func (a *App) BulkAddTag(clipIDs []int64, tagID int64) error // enforces tree exclusivity
func (a *App) BulkRemoveTag(clipIDs []int64, tagID int64) error
func (a *App) GetClipTags(clipID int64) ([]Tag, error)
func (a *App) GetHiddenTags() ([]int64, error)
func (a *App) SetHiddenTags(ids []int64) error

Watch folder operations:

func (a *App) GetWatchedFolders() ([]WatchedFolder, error)
func (a *App) AddWatchedFolder(config WatchedFolderConfig) (*WatchedFolder, error)
func (a *App) RemoveWatchedFolder(id int64) error

Backup operations:

func (a *App) ShowCreateBackupDialog() (string, error)
func (a *App) ShowRestoreBackupDialog() (*BackupManifest, string, error)
func (a *App) ConfirmRestoreBackup(backupPath string) error

database.go — SQLite Management

Database initialization and migrations:

func initDB() (*sql.DB, error) {
// Get platform-specific data directory
dataDir, err := getDataDir()

// Open SQLite with pragmas in DSN for connection-pool safety
dsn := dbPath +
"?_pragma=busy_timeout%3D5000" +
"&_pragma=journal_mode%3Dwal" +
"&_pragma=foreign_keys%3Don"
db, err := sql.Open("sqlite", dsn)

// Create/migrate tables
db.Exec(createTableSQL)

return db, nil
}

Pragmas are set via the DSN string (not db.Exec) so they apply to all pooled connections.

Cleanup job:

func startCleanupJob(db *sql.DB) {
ticker := time.NewTicker(1 * time.Minute)
go func() {
for range ticker.C {
db.Exec("DELETE FROM clips WHERE expires_at IS NOT NULL AND expires_at <= CURRENT_TIMESTAMP")
}
}()
}

watcher.go — File System Watching

Manages folder watching using fsnotify:

type WatcherManager struct {
watcher *fsnotify.Watcher
app *App
activeWatches map[int64]string // folderID -> path
debounceMap map[string]*time.Timer
mu sync.RWMutex
running bool
}

Event handling:

func (w *WatcherManager) handleEvents() {
for {
select {
case event := <-w.watcher.Events:
if event.Op&(fsnotify.Create|fsnotify.Write) != 0 {
w.debounceFile(event.Name)
}
case err := <-w.watcher.Errors:
log.Printf("Watcher error: %v", err)
}
}
}

Data Types

ClipPreview

Lightweight clip data for gallery display:

type ClipPreview struct {
ID int64 `json:"id"`
ContentType string `json:"content_type"`
Filename string `json:"filename"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt *time.Time `json:"expires_at"`
Preview string `json:"preview"` // First 500 chars for text
IsArchived bool `json:"is_archived"`
Tags []Tag `json:"tags"`
Size int64 `json:"size"` // Clip size in bytes
DuplicateCount int `json:"duplicate_count"`
}

ClipData

Full clip data for retrieval:

type ClipData struct {
ID int64 `json:"id"`
ContentType string `json:"content_type"`
Data string `json:"data"` // Base64 for binary, raw for text
Filename string `json:"filename"`
}

FileData

Upload data format:

type FileData struct {
Name string `json:"name"`
ContentType string `json:"content_type"`
Data string `json:"data"` // Base64 encoded
}

WatchedFolder

Watch folder configuration:

type WatchedFolder struct {
ID int64 `json:"id"`
Path string `json:"path"`
FilterMode string `json:"filter_mode"`
FilterPresets []string `json:"filter_presets"`
FilterRegex string `json:"filter_regex"`
ProcessExisting bool `json:"process_existing"`
AutoArchive bool `json:"auto_archive"`
AutoTagID *int64 `json:"auto_tag_id"`
IsPaused bool `json:"is_paused"`
CreatedAt time.Time `json:"created_at"`
Exists bool `json:"exists"`
}

API Patterns

Error Handling

All exposed methods return errors that Wails propagates to JavaScript:

func (a *App) GetClipData(id int64) (*ClipData, error) {
row := a.db.QueryRow("SELECT ... WHERE id = ?", id)
if err := row.Scan(...); err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("clip not found")
}
return nil, fmt.Errorf("failed to get clip: %w", err)
}
return clip, nil
}

Binary Data Handling

Binary content is base64 encoded for JSON transport:

// Encoding for response — text and JSON are returned raw, everything else is base64
if strings.HasPrefix(contentType, "text/") || contentType == "application/json" {
clip.Data = string(data)
} else {
clip.Data = base64.StdEncoding.EncodeToString(data)
}

// Decoding from request
data, err := base64.StdEncoding.DecodeString(file.Data)

Content Type Detection

Automatic detection for text content:

func (a *App) UploadFiles(files []FileData, expirationMinutes int, autoTagID int64) error {
for _, file := range files {
contentType := file.ContentType

if contentType == "text/plain" || contentType == "" {
trimmedText := strings.TrimSpace(string(data))
if strings.HasPrefix(trimmedText, "<!DOCTYPE html") {
contentType = "text/html"
} else if isJSON(trimmedText) {
contentType = "application/json"
}
}
// Insert with detected type
}
}

Concurrency

Mutex Usage

Protect shared state with mutexes. For example, CreateTempFile delegates to TempClipStore which manages leased temporary files with its own synchronization:

func (a *App) CreateTempFile(id int64) (string, error) {
item, err := a.prepareClipTransferItem(id, "legacy_create_temp")
if err != nil {
return "", err
}
return item.AbsPath, nil
}

Watcher Concurrency

The watcher uses RWMutex for reads vs writes:

func (w *WatcherManager) processFile(filePath string) {
w.mu.RLock() // Read lock for map access
var folderID int64
for id, path := range w.activeWatches {
if path == dir {
folderID = id
break
}
}
w.mu.RUnlock()
// ...
}

Event Emission

Send events from Go to JavaScript:

// Emit import notification (sends full ClipPreview for UI refresh)
runtime.EventsEmit(a.ctx, "watch:import", clip)

// Emit error
runtime.EventsEmit(a.ctx, "watch:error", map[string]string{
"file": filepath.Base(filePath),
"error": errMsg,
})

Platform-Specific Code

Data Directory

func getDataDir() (string, error) {
switch runtime.GOOS {
case "darwin":
return filepath.Join(homeDir, "Library", "Application Support", "mahpastes"), nil
case "windows":
return filepath.Join(os.Getenv("APPDATA"), "mahpastes"), nil
default: // Linux
return filepath.Join(homeDir, ".config", "mahpastes"), nil
}
}

Dependencies

require (
github.com/fsnotify/fsnotify v1.9.0
github.com/go-ole/go-ole v1.3.0 // Windows COM/OLE for native drag
github.com/rwcarlsen/goexif v0.0.0-... // EXIF metadata reading
github.com/spf13/cobra v1.10.2 // CLI framework for mp
github.com/wailsapp/wails/v2 v2.11.0
github.com/yuin/gopher-lua v1.1.1
golang.design/x/clipboard v0.7.0
golang.org/x/image v0.35.0
modernc.org/sqlite v1.45.0
)
PackagePurpose
fsnotifyFilesystem event watching for watch folders
go-oleWindows COM interop for OLE drag-and-drop
goexifEXIF metadata extraction from images
cobraCLI framework for the mp binary (cmd/mp/)
wails/v2Desktop application framework (Go backend + web frontend)
gopher-luaLua VM for the plugin sandbox
x/clipboardCross-platform clipboard read/write
x/imageExtended image format support (WebP)
sqlitePure-Go SQLite driver (no CGo dependency for the database)

Building

Development

wails dev

Hot-reloads frontend, rebuilds backend on changes.

Production

# Current platform
wails build

# Cross-platform
wails build -platform darwin/universal
wails build -platform windows/amd64
wails build -platform linux/amd64

Testing

Go Unit Tests

The backend includes Go unit tests alongside the Playwright e2e suite. Run them with:

go test ./...

Test files cover the transfer system, temp clip store, tag hierarchy, serve security, file uploads, and the plugin subsystem (manifest parsing, fetch, semver, permission diffing, update checking).

End-to-End Tests (Playwright)

cd e2e && npm test              # Run all tests
cd e2e && npm run test:headed # Run with browser visible
cd e2e && npm run test:debug # Debug mode with Playwright inspector

Tests are organized by feature in e2e/tests/ covering clips, tags, search, images, plugins, watch folders, backup, bulk operations, and edge cases.