Architecture
PDF Server is a Go service that renders HTML templates to PDF by driving a
headless browser over the Chrome DevTools Protocol (CDP). Module
gitlab.com/c0va23/pdf-server. Web framework: Echo. CLI: urfave/cli v2. DI:
Google Wire.
Process structure (cmd/pdf-server)
main.go builds a CLI app with two subcommands:
server— the long-running HTTP server. Its action calls the Wire injectorInitServer, then blocks onSIGINT/SIGTERM.validate— a one-shot that renders every example of every template and exits non-zero on failure (used in CI and while authoring). Calls the second injectorBuildValidateService.
Dependency injection is wired with Google Wire. wire.go declares the two
injectors; wire_bindings.go holds the single wireSet (all providers +
interface bindings); wire_gen.go is generated. Reading InitServer in
wire_gen.go top-to-bottom is the clearest single view of the object graph and
its reverse-ordered cleanup chain.
Flags mutate the config structs in place (cmd/pdf-server/config.go) before Wire
runs, so runtime configuration reaches the graph.
Layered packages (app/)
| Package | Responsibility |
|---|---|
app/server | Echo router, handlers, middleware (request-ID logging, panic recovery, OTel), static asset embedding, Swagger generation. |
app/services | Application services between handlers and the domain: PDFRenderService, ValidateService, StatsService, SpecService, and the domain error taxonomy. |
app/templates | Domain core: the Template/CompositeTemplate interfaces, the Store, the futures-based async render abstraction, and the validator. |
app/templates/fileloader | Loads templates from disk: engines, schema/params/assets/examples/partials loaders, plus composition and remote sub-packages. |
app/pdfrender | The PDFRenderer interface, the Config/Document types, and lifecycle-event definitions. |
app/pdfrender/cdp | The CDP implementation: browser pool, factory (runner/page), runner (process management), page, render. |
app/pdfrender/server | The loopback-only internal HTTP server the browser calls back into. |
app/config | GlobalConfig aggregating each package's sub-config. |
app/log | logrus wrapper; context-carried loggers. |
app/utils/trace_utils | OpenTelemetry providers wired through middleware and spans. |
Key seams
- Rendering is asynchronous via a small
futurespackage. The HTML→PDF path runs on aPoolSchedulerwhose worker count equals the browser pool size. - The template store is hot-reloadable via a
StoreProvider/StoreConsumerLazyStore(see Template store & hot reload). - Errors are typed in
app/serviceswithIs/Unwrap; the handler'sbuildErroris the single place they map to HTTP status codes. - Tracing and logging are pervasive: components take a
TracerProviderand open spans; loggers are pulled from context withlog.LoggerFromContext.
Deeper dives
- Render pipeline — the end-to-end request flow.
- CDP browser pool — pooling, factory modes, process management.
- Internal callback server — how HTML reaches the browser and the callback event.
- Template store & hot reload.
- Extension points — add a provider, engine, helper, or loader.
- Codebase map — package-by-package with references.
The design/ directory contains PlantUML diagrams (main_flow.puml,
services.puml, templates.puml).