Presto Software Architecture
Presto is a Markdown to Typst to PDF document conversion platform that achieves extensible typesetting capabilities through binary templates while providing both desktop and web usage modes.
System Overview
Core Tech Stack
| Layer | Technology | Version | Purpose |
|---|---|---|---|
| Backend | Go | 1.25 | Business logic, template management, API |
| Desktop Framework | Wails | v2 | Native window, system WebView, Go-JS bridge |
| Frontend Framework | SvelteKit | 5 | SPA routing, component-based UI |
| Frontend Build | Vite | 7 | Dev server, production build |
| Editor | CodeMirror | 6 | Markdown editing, syntax highlighting |
| Typesetting Engine | Typst | 0.14.x | Typst source to PDF/SVG |
Project Directory Structure
Presto/
├── cmd/
│ ├── presto-desktop/ # Wails desktop entry point
│ │ ├── main.go # App struct + Wails config + main()
│ │ ├── updater.go # Cross-platform auto-updater
│ │ └── build/ # Embedded frontend build artifacts
│ └── presto-server/
│ └── main.go # HTTP server entry point
├── internal/
│ ├── api/ # HTTP API layer (shared by both entry points)
│ ├── template/ # Template management core
│ └── typst/ # Typst compiler wrapper
├── frontend/ # SvelteKit 5 frontend
├── packaging/ # Platform packaging scripts
├── Dockerfile # Server Docker image
└── Makefile # Build scriptsDual-Entry Architecture
Presto uses a "shared core + dual shell" architecture: two entry programs (cmd/presto-desktop and cmd/presto-server) share all business logic from the internal/ package, adapting to their respective runtime environments through different configuration parameters.
Why Share internal Instead of Splitting into Microservices
Presto's core operations (template execution, Typst compilation) are all local CPU-intensive tasks with no remote service calls involved. Splitting into microservices would only add inter-process communication overhead and deployment complexity with no practical benefit. Sharing the internal/ package keeps the behavior of both entry points consistent -- one change takes effect in both places.
Desktop vs Server Comparison
| Dimension | Desktop (cmd/presto-desktop) | Server (cmd/presto-server) |
|---|---|---|
| Frontend Assets | //go:embed all:build embedded in binary | STATIC_DIR filesystem path |
| Auth | None (APIKey empty, auth middleware skipped) | Bearer Token (randomly generated or env var) |
| Fonts | System fonts | Configurable FONT_PATHS (colon-separated) |
| Typst Lookup | findTypstBinary() multi-path search | Direct "typst" (relies on PATH) |
| Network Binding | None (Wails internal communication) | HOST:PORT (default 127.0.0.1:8080) |
| Deployment | Single-file app (DMG/ZIP/tar.gz) | Docker image or direct execution |
Mode Switching Mechanism
Both entry points call api.NewServer(ServerOptions{...}), achieving behavior switching through ServerOptions field differences:
APIKeyempty ->authMiddlewareskips auth (desktop mode)APIKeynon-empty -> All/api/*requests require Bearer Token; HTML pages inject<meta name="api-key">tag for frontend to readStaticDirempty -> Frontend served by Wails embedded resource serverStaticDirnon-empty -> Static files served from filesystem
Go Backend Core Packages
There are three core packages under internal/, with clearly separated responsibilities:
template/ -- Template System Core
The template system manages the complete lifecycle of templates: discovery, installation, execution, and uninstallation.
Manager (internal/template/manager.go) is the coordinator of the template system. All operations revolve around the ~/.presto/templates/{name}/ directory:
| Operation | Method | Description |
|---|---|---|
| List | List() | Scans all subdirectories, reads manifest.json + verifies binary exists, auto-deduplicates |
| Get | Get(name) | Finds an installed template by name |
| Install | Install(owner, repo, opts) | Downloads from GitHub + SHA256 verification + writes to disk |
| Uninstall | Uninstall(name) | Safely deletes template directory (path traversal protection + symlink detection) |
| Rename | Rename(old, new) | Three-step disk rename: binary -> manifest -> directory |
| Executor | Executor(t) | Creates an Executor instance for an installed template |
Executor (internal/template/executor.go) encapsulates the four invocation modes of the template protocol:
| Method | Protocol | Description |
|---|---|---|
Convert(markdown) | stdin/stdout | Markdown to Typst conversion |
GetManifest() | --manifest | Retrieve template metadata |
GetExample() | --example | Retrieve example document |
All invocations share the internal run(args, stdin) method, with unified 30-second timeout (SEC-12) and minimized environment variables PATH=/usr/local/bin:/usr/bin:/bin (SEC-10).
RegistryCache (internal/template/registry.go) is the local cache layer for the registry CDN, with a three-tier fallback strategy:
- In-memory cache (protected by
sync.RWMutex) - Disk cache (JSON file)
- CDN fetch (
https://presto.c-1o.top/templates/registry.json) - Expired cache fallback (when CDN is unreachable)
Cache TTL is 1 hour, with async refresh on startup. Core methods:
VerifySHA256()-- Verify template integrityLookupTrust()-- Query trust levelLookupByRepo()-- Server-side secure lookup, does not trust client URL (SEC-39)
GitHub Interaction (internal/template/github.go) handles template discovery and download:
DiscoverTemplates()-- GitHub Search API, searches bytopic:presto-templateInstall()-- Complete installation flow: name validation -> download URL retrieval (registry preferred) -> domain whitelist verification -> download (100MB limit) -> SHA256 verification -> write to disk- Security: 6 GitHub domain whitelist (SEC-07), official/verified templates must have SHA256 (SEC-01), restrictive file permissions 0700/0600 (SEC-28/45)
api/ -- HTTP API Layer
api.Server is the core shared component of both entry points, providing 15 REST endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
/api/health | GET | Health check |
/api/convert | POST | Markdown to Typst |
/api/compile | POST | Typst to PDF |
/api/compile-svg | POST | Typst to SVG page array |
/api/convert-and-compile | POST | Markdown to PDF (one step) |
/api/templates | GET | List installed templates |
/api/templates/discover | GET | Search templates on GitHub |
/api/templates/{id}/install | POST | Install template (accepts owner/repo only) |
/api/templates/{id} | PATCH | Rename template |
/api/templates/{id} | DELETE | Uninstall template |
/api/templates/{id}/manifest | GET | Get template manifest |
/api/templates/{id}/example | GET | Get template example |
/api/templates/import | POST | Import template from ZIP |
/api/batch/import-zip | POST | Batch ZIP import |
Middleware chain (outer to inner):
- CORS -- 6 whitelisted Origins (localhost:8080/5173, 127.0.0.1, wails://wails, wails.localhost)
- Security headers --
X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: strict-origin-when-cross-origin - Auth -- Bearer Token +
subtle.ConstantTimeCompareto prevent timing attacks (NEW-04), skipped in desktop mode - Rate limiting -- Token bucket algorithm, 10 req/s, burst 30 (SEC-19)
typst/ -- Compiler Wrapper
typst.Compiler (internal/typst/compiler.go) wraps the Typst CLI, providing two output formats:
| Method | Input | Output | Timeout |
|---|---|---|---|
CompileString(src, workDir) | Typst source string | PDF bytes | 60s |
CompileToSVG(src, workDir) | Typst source string | SVG page array | 60s |
Compile(typFile) | .typ file path | .pdf file | 60s |
ListFonts() | -- | Available font list | -- |
Design considerations:
Rootfield restricts Typst's filesystem access scope (SEC-02); desktop sets it toos.TempDir(), not/crypto/randgenerates random filename suffixes to prevent concurrent compilation conflicts (SEC-25)- Multi-page SVG output sorted by page number, single page auto-fallback
Wails Communication Mechanism
There are three communication methods between the frontend and Go backend in the desktop app, each suited for different scenarios:
Method 1: Go Binding (Frontend Calls Go)
The frontend directly calls Go methods via window.go.main.App.*, with Wails handling serialization automatically:
| Method | Purpose |
|---|---|
OpenFile() / OpenFiles() | Native file open dialog |
SavePDF(markdown, templateId, workDir) | Convert + native save dialog |
SaveFile(b64Data, filename) | Base64 data to native save |
CompileSVG(typstSource, workDir) | Typst to SVG compilation |
ImportBatchZip(filePath) | ZIP batch import |
DeleteTemplate(name) | Uninstall template |
GetVersion() | Get version number |
CheckForUpdate() | Check for updates |
DownloadAndInstallUpdate(url) | Download and install update |
Method 2: Wails Events (Go Pushes to Frontend)
The Go backend proactively pushes events to the frontend via runtime.EventsEmit():
| Event Name | Trigger Scenario |
|---|---|
native-file-drop | User drags and drops files onto the window |
url-scheme-open-template | presto://install/{name} URL scheme |
menu:open/export/settings/templates | Native menu click |
update:progress / update:status | Update download progress and status |
Method 3: HTTP API (Frontend Fetch)
The frontend calls /api/* endpoints through authFetch(), sharing the same API as server mode.
Why Some Operations Bypass HTTP and Use Wails Binding
Wails' WebView has known limitations:
- Multipart Content-Type header is stripped: FormData uploads (e.g., ZIP import) don't work properly
- DELETE method may not be supported: Some WebView implementations are incomplete
Therefore, operations like CompileSVG, ImportBatchZip, and DeleteTemplate use Go Binding direct calls, bypassing the WebView's HTTP layer. Operations requiring native UI (file dialogs, save dialogs) are naturally suited for Binding.
Frontend Architecture
The frontend is built with SvelteKit 5 + adapter-static as a pure SPA, embedded in the desktop binary via //go:embed or independently deployed as the web version.
Route Structure
| Route | Page | Description |
|---|---|---|
/ | Main Editor | CodeMirror editing + SVG live preview + PDF export |
/batch | Batch Convert | Multi-file drag-in, group by template, concurrent conversion, ZIP packaging |
/settings | Settings | Template management, community templates toggle, version update |
/store-templates | Template Store | Browse registry, install templates, URL scheme deep link |
/showcase/* | Showcase Pages | Pre-rendered demo pages, embedded in website |
State Management: Svelte 5 Runes
Fully adopts Svelte 5 runes pattern ($state, $derived, $effect), without using traditional writable stores. Store files use the .svelte.ts extension:
| Store | Responsibility |
|---|---|
editor.svelte.ts | Editor state: markdown, typstSource, svgPages, selectedTemplate |
templates.svelte.ts | Installed template list, loaded from API, listens to templates-changed event |
registry.svelte.ts | Remote registry data, uses mock in dev, fetches from CDN in production |
file-router.svelte.ts | Unified file handling: drag/open routing dispatch, ZIP import, toast state |
wizard.svelte.ts | Onboarding wizard: localStorage persistence, multiple trigger mechanisms |
API Client
authFetch() (src/lib/api/client.ts) provides unified HTTP request handling:
- Reads API Key from
<meta name="api-key">(injected in server mode) - Automatically adds
Authorization: Bearer {key}header when Key is present - Desktop mode has no Key, fetches directly (auth middleware skipped)
- API base URL configured via
VITE_API_URLenvironment variable; empty for desktop (same origin)
Data Flow (End-to-End)
The complete data flow from when a user opens the app to exporting a PDF:
Desktop preview (CompileSVG) uses Wails Binding direct calls instead of the HTTP layer, reducing one serialization overhead. When exporting PDF, the desktop calls the SavePDF() Binding, which automatically opens the native save dialog.
Security Design
Presto uses a SEC-XX annotation system in the code to mark security measures, covering the following key areas:
Path Security
- Path traversal protection (SEC-05/06/30): All file operations validate absolute path prefixes, reject paths containing
.., template names validated with regex^[a-zA-Z0-9][a-zA-Z0-9._-]*$ - Symlink detection (SEC-38): Uses
Lstatbefore uninstall to prevent TOCTOU attacks - Hidden file filtering (SEC-27): Static file serving blocks access to dotfiles
Network Security
- Domain whitelist (SEC-07/46): Download URLs restricted to 6 GitHub domains; CDN redirects also verified
- CORS whitelist (SEC-08): Only known Origins allowed
- Constant-time comparison (NEW-04): API Key auth uses
subtle.ConstantTimeCompare - Token bucket rate limiting (SEC-19): 10 req/s, burst 30
Template Sandbox
Template binaries run in a strictly restricted environment (see binary protocol reference):
- 30-second execution timeout (SEC-12)
- Minimized environment variables (SEC-10)
- No network access, no file writes
- SHA256 verification chain: registry record -> download verification -> installation validation (SEC-01)
- official/verified templates must have SHA256, otherwise installation is rejected
Data Security
- Request body limits (SEC-11/13/29): API 10MB, ZIP 100MB, registry 10MB
- Secure response headers (SEC-36): nosniff, DENY, strict-origin-when-cross-origin
- Error message isolation (SEC-15/16/35): Clients only see generic error messages; details logged on the server side
- File permissions (SEC-28/45): Directories 0700, files 0600
Cross-Platform Auto-Update
The desktop app has a built-in auto-update mechanism that checks the latest version from GitHub Releases and installs it per platform.
Update Flow
CheckForUpdate()-- Queries GitHub API, compares version numbers, matches download assets by{os}-{arch}DownloadAndInstallUpdate(url)-- Downloads to temp directory, reports progress viaupdate:progressevent- Executes platform-specific installation -> restarts the application
Three-Platform Installation Strategy
| Platform | Format | Strategy | Rationale |
|---|---|---|---|
| macOS | DMG | Mount -> copy replace -> restart | Running binary is in memory, safe to replace the disk file |
| Windows | ZIP | Extract -> generate bat script -> delayed replace | Windows locks the running exe, needs external script to wait for exit then replace |
| Linux | tar.gz | Extract -> direct overwrite -> restart | Similar to macOS, running binary is in memory |
All platforms include path traversal protection (filepath.Clean + prefix check); Windows additionally guards against zip slip attacks.
