Skip to main content

Architecture Overview

mahpastes is built using Wails, a framework for building desktop applications with Go backends and web frontends.

High-Level Architecture

┌─────────────────────────────────────────────────────────────┐
│ Desktop App (Wails) │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ Frontend │ │ Backend │ │
│ │ (HTML/CSS/JS) │◄──►│ (Go) │ │
│ │ │ │ │ │
│ │ • Vanilla JS │ │ • Wails v2 │ │
│ │ • Tailwind CSS │ │ • SQLite │ │
│ │ • No framework │ │ • Clipboard integration │ │
│ └─────────────────────┘ │ • File system watcher │ │
│ │ • REST API server │ │
│ │ • Tag HTTP servers │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌────────────────┐ ┌──────────────────┐
│ SQLite DB │ │ HTTP listeners │
│ clips.db │ │ (REST API, Tag │
└────────────────┘ │ serve servers) │
└──────────────────┘

Technology Stack

Backend (Go)

ComponentTechnologyPurpose
FrameworkWails v2Desktop app framework
DatabaseSQLite 3Local data storage
Clipboardgolang.design/x/clipboardCross-platform clipboard
File WatchfsnotifyFilesystem events
Pluginsgopher-luaLua scripting sandbox
CLIcobramp CLI command framework
EXIFgoexifImage metadata extraction
COM (Windows)go-oleOLE drag-and-drop interop

Frontend (Web)

ComponentTechnologyPurpose
MarkupHTML5Structure
StylingTailwind CSSUtility-first CSS
LogicVanilla JavaScriptNo framework overhead
FontIBM Plex MonoConsistent typography

Component Overview

Backend Components

main.go                  Entry point, Wails setup
app.go Core application logic, API methods
database.go SQLite setup, schema, migrations
watcher.go Folder watching, file import
backup.go ZIP backup and restore
clipboard_service.go Clipboard copy service (Wails-bound)
clipboard_darwin.go macOS clipboard via NSPasteboard (CGo)
clipboard_windows.go Windows clipboard via PowerShell
clipboard_other.go Unsupported platform stub
transfer_service.go Drag-out preparation and native drag
transfer_handler.go HTTP handler for transfer file serving (DownloadURL)
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 via CGo
native_drag_windows.go Windows native drag via COM OLE DoDragDrop
native_drag_other.go Unsupported platform stub
open_darwin.go Open file with default app (macOS)
open_windows.go Open file with default app (Windows)
open_other.go Unsupported platform stub
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, route registration, key management
api_service.go REST API Wails service (start/stop/keys)
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 fetching and downloading
├── semver.go Semantic versioning helpers
├── update_checker.go Plugin update checking
├── permission_diff.go Permission diff logic for plugin updates
├── fonts/ Bundled fonts for image overlay
└── api_*.go Lua APIs (clips, tags, storage, http, fs, utils, task, toast, image, modal, metadata)

Frontend Components

frontend/
├── index.html Main UI structure
├── js/
│ ├── app.js Core application logic
│ ├── ui.js Card rendering, gallery management
│ ├── editor/ Modular image editor
│ │ ├── editor-core.js Core editor canvas logic
│ │ ├── tool-anonymize.js Anonymize/blur tool
│ │ ├── tool-arrow.js Arrow drawing tool
│ │ ├── tool-brush.js Brush/freehand tool
│ │ ├── tool-crop.js Crop tool
│ │ ├── tool-eyedropper.js Color picker tool
│ │ ├── tool-select.js Selection tool
│ │ ├── tool-shapes.js Shape drawing tool
│ │ ├── tool-text.js Text overlay tool
│ │ ├── tool-transform.js Transform/resize tool
│ │ └── tool-zoom.js Zoom/pan tool
│ ├── modals.js All modal/lightbox/editor logic
│ ├── tags.js Tag management UI
│ ├── settings.js Settings modal
│ ├── plugins.js Plugin management UI
│ ├── plugin-icons.js Plugin icon rendering
│ ├── task-queue.js Plugin task progress UI
│ ├── watch.js Watch folders UI
│ ├── transfer.js Drag-out transfer state management
│ ├── transfer-strategies.js Platform-specific drag data adapters
│ ├── modal-renderer.js Plugin result modal rendering
│ ├── metadata.js Clip metadata UI
│ ├── sort.js Gallery sorting controls
│ ├── tooltips.js Tooltip management
│ ├── shortcuts.js Keyboard shortcut registration and context handling
│ ├── plugin-review.js Plugin permission review UI
│ ├── context-menu.js Shared context menu module
│ ├── folder-drag.js Folder drag-and-drop for moving clips between folders
│ ├── roving-tabindex.js Reusable roving tabindex keyboard navigation
│ ├── utils.js Shared utilities
│ ├── wails-api.js Wails bindings wrapper
│ ├── serve.js Tag serve view UI
│ └── api-settings.js REST API settings modal
└── css/
├── main.css Global styles, scrollbars, form styling
└── modals.css Modal-specific styles

Wails-Bound Services

The backend exposes multiple Go structs to the frontend via Wails bindings. As the source code comment in main.go notes, services were originally separate structs to stay under the Wails ~49 method binding limit. In practice the App struct has 72+ exported methods and all bind correctly, so the limit may not be enforced. The separate services remain as an organizational boundary, grouping related functionality into dedicated files and structs.

ServiceFileFrontend accessPurpose
Appapp.gowindow.go.main.App.*Core clip, tag, watch, backup, and settings operations
PluginServiceplugin_service.gowindow.go.main.PluginService.*Plugin lifecycle, UI actions, storage, updates
ClipboardServiceclipboard_service.gowindow.go.main.ClipboardService.*Copy clips as files or raw content to system clipboard
TransferServicetransfer_service.gowindow.go.main.TransferService.*Drag-out preparation and native OS drag
ServeServiceserve_service.gowindow.go.main.ServeService.*Start/stop tag HTTP servers, status queries
APIServiceapi_service.gowindow.go.main.APIService.*REST API server lifecycle, API key management

All six structs are bound in main.go via the Bind option.

REST API Server

The api_manager.go file manages a standalone HTTP server that exposes clip, tag, watch, plugin, and backup operations over a REST interface (/api/v1/*). The mp CLI and other external tools authenticate with API keys (stored in the api_keys table).

Key details:

  • The server listens on a user-configured port (default off).
  • Authentication uses Bearer tokens (Authorization: Bearer mp_...).
  • API keys have a name, role, and optional tag scope.
  • api_service.go wraps the manager as a Wails-bound struct so the frontend can start/stop the server and manage keys from the Settings UI.

Data Flow

Adding a Clip

User Action (paste/drop)


Frontend (app.js)
- Capture event
- Convert to FileData
- Call backend API


Backend (app.go)
- UploadFiles()
- Detect content type
- Insert into SQLite


Frontend
- Refresh gallery
- Display new clip

Retrieving a Clip

User clicks Copy


Frontend
- GetClipData(id)


Backend
- Query SQLite
- Return base64 data


Frontend
- Decode if needed
- Write to clipboard

Watch Folder Import

File created in watched folder


fsnotify event


WatcherManager
- Debounce (500ms)
- Check filter
- Read file


Backend
- Import as clip
- Delete original
- Emit event


Frontend
- Receive event
- Refresh gallery
- Show toast

Key Design Decisions

Why Wails?

  • Single binary distribution
  • Native performance (Go backend)
  • Web technologies for UI (familiar, flexible)
  • Cross-platform support
  • Good developer experience

Why SQLite?

  • Zero configuration
  • Single file database
  • Excellent performance for local data
  • WAL mode for concurrent access
  • No external dependencies

Why Vanilla JavaScript?

  • Small codebase doesn't need framework complexity
  • Fast load times
  • No build step for development
  • Easy to understand and modify
  • Reduced bundle size

Why Base64 for Binary Data?

Wails communication between Go and JavaScript uses JSON. Binary data must be serialized:

  • Text content: Passed as-is (UTF-8 strings)
  • Binary content: Base64 encoded for transport
  • Frontend decodes when displaying

State Management

Frontend State

// Global state variables in app.js
let isViewingArchive = false; // Current view mode
let selectedIds = new Set(); // Multi-select state
let imageClips = []; // Lightbox navigation
let currentLightboxIndex = -1; // Current lightbox position

Backend State

// App struct in app.go
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
}

Communication Protocol

Frontend → Backend

JavaScript calls Go functions via Wails bindings:

// Generated bindings in wailsjs/go/main/App.js
import { GetClips, UploadFiles } from '../wailsjs/go/main/App';

// GetClips takes archived flag, tag filter IDs, hidden tag IDs, sort field, and sort direction
const clips = await GetClips(false, [], [], 'date', 'desc');

Backend → Frontend

Go emits events that JavaScript listens for:

// Backend emits
runtime.EventsEmit(a.ctx, "watch:import", clip)

// Frontend listens
window.runtime.EventsOn("watch:import", (clip) => {
showToast(`Imported: ${clip.filename}`);
// Update gallery if clip matches current view filters
});

Error Handling

Backend Errors

Go functions return errors that are propagated to JavaScript:

func (a *App) GetClipData(id int64) (*ClipData, error) {
// ...
if err != nil {
return nil, fmt.Errorf("failed to get clip: %w", err)
}
return clip, nil
}

Frontend Errors

JavaScript handles errors with try/catch:

try {
const clip = await GetClipData(id);
// Use clip
} catch (error) {
showToast('Failed to load clip');
console.error(error);
}

Security Considerations

Local First

  • All data stays on the user's machine
  • No cloud sync or external APIs
  • Optional HTTP servers: REST API (authenticated with Bearer tokens) and tag serve servers (cookie-based auth) can listen on configurable ports
  • Plugins may make network requests with user permission

File System Access

  • Reads files user explicitly provides
  • Watch folders require user configuration
  • Temp files in app-specific directory
  • Cleaned up on exit

Database

  • SQLite file in user data directory
  • Standard file permissions
  • No encryption (local data)