Skip to content
⚠️ This document is AI-translated. The Chinese version is the authoritative source.

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

LayerTechnologyVersionPurpose
BackendGo1.25Business logic, template management, API
Desktop FrameworkWailsv2Native window, system WebView, Go-JS bridge
Frontend FrameworkSvelteKit5SPA routing, component-based UI
Frontend BuildVite7Dev server, production build
EditorCodeMirror6Markdown editing, syntax highlighting
Typesetting EngineTypst0.14.xTypst source to PDF/SVG

Project Directory Structure

text
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 scripts

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

DimensionDesktop (cmd/presto-desktop)Server (cmd/presto-server)
Frontend Assets//go:embed all:build embedded in binarySTATIC_DIR filesystem path
AuthNone (APIKey empty, auth middleware skipped)Bearer Token (randomly generated or env var)
FontsSystem fontsConfigurable FONT_PATHS (colon-separated)
Typst LookupfindTypstBinary() multi-path searchDirect "typst" (relies on PATH)
Network BindingNone (Wails internal communication)HOST:PORT (default 127.0.0.1:8080)
DeploymentSingle-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:

  • APIKey empty -> authMiddleware skips auth (desktop mode)
  • APIKey non-empty -> All /api/* requests require Bearer Token; HTML pages inject <meta name="api-key"> tag for frontend to read
  • StaticDir empty -> Frontend served by Wails embedded resource server
  • StaticDir non-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:

OperationMethodDescription
ListList()Scans all subdirectories, reads manifest.json + verifies binary exists, auto-deduplicates
GetGet(name)Finds an installed template by name
InstallInstall(owner, repo, opts)Downloads from GitHub + SHA256 verification + writes to disk
UninstallUninstall(name)Safely deletes template directory (path traversal protection + symlink detection)
RenameRename(old, new)Three-step disk rename: binary -> manifest -> directory
ExecutorExecutor(t)Creates an Executor instance for an installed template

Executor (internal/template/executor.go) encapsulates the four invocation modes of the template protocol:

MethodProtocolDescription
Convert(markdown)stdin/stdoutMarkdown to Typst conversion
GetManifest()--manifestRetrieve template metadata
GetExample()--exampleRetrieve 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:

  1. In-memory cache (protected by sync.RWMutex)
  2. Disk cache (JSON file)
  3. CDN fetch (https://presto.c-1o.top/templates/registry.json)
  4. Expired cache fallback (when CDN is unreachable)

Cache TTL is 1 hour, with async refresh on startup. Core methods:

  • VerifySHA256() -- Verify template integrity
  • LookupTrust() -- Query trust level
  • LookupByRepo() -- 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 by topic:presto-template
  • Install() -- 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:

EndpointMethodPurpose
/api/healthGETHealth check
/api/convertPOSTMarkdown to Typst
/api/compilePOSTTypst to PDF
/api/compile-svgPOSTTypst to SVG page array
/api/convert-and-compilePOSTMarkdown to PDF (one step)
/api/templatesGETList installed templates
/api/templates/discoverGETSearch templates on GitHub
/api/templates/{id}/installPOSTInstall template (accepts owner/repo only)
/api/templates/{id}PATCHRename template
/api/templates/{id}DELETEUninstall template
/api/templates/{id}/manifestGETGet template manifest
/api/templates/{id}/exampleGETGet template example
/api/templates/importPOSTImport template from ZIP
/api/batch/import-zipPOSTBatch 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.ConstantTimeCompare to 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:

MethodInputOutputTimeout
CompileString(src, workDir)Typst source stringPDF bytes60s
CompileToSVG(src, workDir)Typst source stringSVG page array60s
Compile(typFile).typ file path.pdf file60s
ListFonts()--Available font list--

Design considerations:

  • Root field restricts Typst's filesystem access scope (SEC-02); desktop sets it to os.TempDir(), not /
  • crypto/rand generates 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:

MethodPurpose
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 NameTrigger Scenario
native-file-dropUser drags and drops files onto the window
url-scheme-open-templatepresto://install/{name} URL scheme
menu:open/export/settings/templatesNative menu click
update:progress / update:statusUpdate 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

RoutePageDescription
/Main EditorCodeMirror editing + SVG live preview + PDF export
/batchBatch ConvertMulti-file drag-in, group by template, concurrent conversion, ZIP packaging
/settingsSettingsTemplate management, community templates toggle, version update
/store-templatesTemplate StoreBrowse registry, install templates, URL scheme deep link
/showcase/*Showcase PagesPre-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:

StoreResponsibility
editor.svelte.tsEditor state: markdown, typstSource, svgPages, selectedTemplate
templates.svelte.tsInstalled template list, loaded from API, listens to templates-changed event
registry.svelte.tsRemote registry data, uses mock in dev, fetches from CDN in production
file-router.svelte.tsUnified file handling: drag/open routing dispatch, ZIP import, toast state
wizard.svelte.tsOnboarding 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_URL environment 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 Lstat before 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

  1. CheckForUpdate() -- Queries GitHub API, compares version numbers, matches download assets by {os}-{arch}
  2. DownloadAndInstallUpdate(url) -- Downloads to temp directory, reports progress via update:progress event
  3. Executes platform-specific installation -> restarts the application

Three-Platform Installation Strategy

PlatformFormatStrategyRationale
macOSDMGMount -> copy replace -> restartRunning binary is in memory, safe to replace the disk file
WindowsZIPExtract -> generate bat script -> delayed replaceWindows locks the running exe, needs external script to wait for exit then replace
Linuxtar.gzExtract -> direct overwrite -> restartSimilar to macOS, running binary is in memory

All platforms include path traversal protection (filepath.Clean + prefix check); Windows additionally guards against zip slip attacks.

Presto — Markdown to PDF