This file provides repository guidance for coding agents and automated assistants working with this codebase.
- NEVER use
slog.Info()→ alwaysslog.InfoContext(ctx, ...) - NEVER use
logpackage →depguardenforceslog/slogonly - ALWAYS run
make swaggerafter API changes (swagger.yaml+openapi.yaml) - ALWAYS read files before suggesting changes
- NEVER modify DB schema SQL without understanding migration ordering
- ALWAYS include multi-tenant headers (
X-Tenant-ID,X-Workspace-ID) in API calls - NEVER change module path in
go.mod— must staygithub.com/rendis/pdf-forgeeven in forks
pdf-forge: Forkeable multi-tenant PDF template engine powered by Typst.
Fork → customize core/extensions/ → docker compose up → done.
Stack: Go 1.25 + PostgreSQL 16 + Typst + React 19 (embedded SPA via go:embed)
Key terms: Tenant → Workspace → Template → Version (DRAFT→[STAGING]→PUBLISHED→ARCHIVED). See core/docs/glossary.md.
Non-Goals: NOT a WYSIWYG editor, Word generator, CMS, signature platform, or reporting tool.
# Root Makefile (orchestrator — delegates to core/ and app/)
make build # Build frontend + embed + Go binary (single binary)
make build-core # Build Go backend only (uses existing embedded assets)
make build-app # Build React frontend only (outputs to app/dist/)
make embed-app # Build frontend and copy to Go embed location
make run # Run API server
make migrate # Apply database migrations
make dev # Hot reload backend (air)
make dev-app # Start Vite dev server
make test # Unit tests
make lint # golangci-lint
make swagger # Regenerate Swagger + OpenAPI specs
make docker-up # Start all services with Docker Compose
make clean # Remove all build artifacts
# Fork workflow
make init-fork # Set up upstream remote + merge drivers
make doctor # Check system dependencies and build health
make check-upgrade VERSION=v1.2.0 # Verify if upgrade is safe before merging
make sync-upstream VERSION=v1.2.0 # Merge upstream release into current branch
# Single test (from root)
go test -run TestFunctionName ./core/internal/core/service/...
# Format Go code
make -C core fmtgo.mod ← Module root (github.com/rendis/pdf-forge)
core/ ← Backend Go
sdk/ ← PUBLIC API for external consumers (type aliases)
cmd/api/
main.go ← Entrypoint (run server / migrate)
bootstrap/
engine.go ← Engine: config, extensions, lifecycle
initializer.go ← Manual DI wiring (no Wire)
preflight.go ← Startup checks (Typst, DB, auth)
extensions/ ← USER CUSTOMIZATION POINT
register.go ← Registers all extensions with Engine
injectors/ ← Custom injector implementations
mapper.go, init.go, ← RequestMapper, InitFunc, Provider,
provider.go, middleware.go ← Middleware stubs
internal/
core/entity/ ← Domain types
core/port/ ← Interfaces (Injector, RequestMapper, etc.)
core/service/ ← Business logic
adapters/primary/http/ ← Gin controllers, DTOs
adapters/secondary/database/ ← PostgreSQL repositories
extensions/injectors/ ← Built-in injectors (date_now, time_now, etc.)
frontend/ ← Embedded React SPA (go:embed)
embed.go ← //go:embed all:dist + Assets()
dist/ ← Built SPA assets (generated by make embed-app)
infra/ ← Config, logging, server, registry
migrations/sql/ ← Embedded SQL migrations
core/settings/app.yaml ← Default configuration
docs/ ← Architecture, auth matrix, extensibility
app/ ← Frontend React SPA source code
src/ ← React 19 + TypeScript + TanStack Router
Dockerfile ← Unified build: frontend + backend → single binary
docker-compose.yaml ← Full stack: postgres + api (with embedded frontend)
End users create new projects via scaffold command — no fork needed:
go run github.com/rendis/pdf-forge/cmd/init@latest my-project --module github.com/myorg/my-project
cd my-project && go mod tidyGenerates: main.go, extensions/, settings/app.yaml, Makefile, Dockerfile, docker-compose.yaml.
- API-only via
go run .(no frontend embedded locally) - Full stack (frontend + API) via
docker compose up --build(Dockerfile clones and builds SPA) - Generated
main.gouses//go:embed all:frontend-dist+engine.SetFrontendFS()for optional local embedding
Scaffold source: cmd/init/ (templates in cmd/init/templates/)
Fork model: All user code goes in core/extensions/. Entry point: core/extensions/register.go. Called from core/cmd/api/main.go. Users never modify core/internal/ or core/cmd/api/bootstrap/.
SDK model (scaffold): External projects import only github.com/rendis/pdf-forge/core/sdk. Same extension pattern, all types come from sdk package.
Types imported from:
github.com/rendis/pdf-forge/core/sdk— public API (Engine, interfaces, entity types, constructors) — use this for external consumersgithub.com/rendis/pdf-forge/core/internal/core/port— interfaces (internal, fork-only)github.com/rendis/pdf-forge/core/internal/core/entity— domain types (internal, fork-only)github.com/rendis/pdf-forge/core/cmd/api/bootstrap— Engine type (internal, fork-only)
Interfaces in core/internal/core/port/:
| Interface | File | Purpose |
|---|---|---|
Injector |
injector.go |
Custom injectable resolution |
InitFunc |
injector.go |
Shared setup before resolution |
RequestMapper |
mapper.go |
Transform render request data |
WorkspaceInjectableProvider |
workspace_injectable_provider.go |
Dynamic workspace-specific injectables |
RenderAuthenticator |
render_authenticator.go |
Custom auth for render endpoints |
Middleware: engine.UseMiddleware() (global) / engine.UseAPIMiddleware() (API only)
Lifecycle: engine.OnStart() / engine.OnShutdown() (shutdown runs LIFO)
See core/docs/extensibility-guide.md for full documentation.
- Hexagonal: core (entities, ports, services) + adapters (HTTP, DB)
- Runtime DI:
core/cmd/api/bootstrap/initializer.go(no Wire — manual wiring) - Preflight checks: Typst binary, DB connectivity, schema version, auth validation at startup
- Concurrency: Semaphore limits concurrent renders (
typst.max_concurrent)
POST /api/v1/workspace/document-types/{code}/render
→ Acquire semaphore → Run InitFuncs → Resolve injectables (topological order)
→ Build Typst source → Resolve images (cached) → Typst CLI → PDF bytes
Two separate auth flows in core/internal/infra/server/http.go:
- Panel routes (
/api/v1/*): Full OIDC → identity lookup → role/membership check - Render routes (
/api/v1/workspace/*/render): Priority chain: dummy auth → customRenderAuthenticator→ OIDC render provider. No workspace membership check.
Recovery → Logger → CORS → Global middleware → Auth → Identity → Roles → API middleware → Controller
| Route | Auth |
|---|---|
/api/v1/* (except render) |
Panel OIDC + Identity |
/api/v1/workspace/document-types/*/render |
Render providers (NO membership check) |
/api/v1/workspace/templates/versions/*/render |
Render providers (render by version ID) |
/swagger/*, /health, /ready |
None |
/* (non-API paths) |
None (embedded SPA) |
Frontend embedded in Go binary via go:embed. Served from same port as API.
Uses mcp-openapi-proxy with repo-local config:
- Claude Code:
.mcp.json - Codex:
.codex/config.toml - Canonical MCP spec:
core/docs/openapi.yaml(generated bymake swagger) - Server name:
pdf-forge - Tool prefix:
pf
The proxy registers exactly 3 MCP tools:
pf_list_endpointspf_describe_endpointpf_call_endpoint
Correct workflow:
pf_list_endpointspf_describe_endpointpf_call_endpoint
Do not assume one MCP tool per endpoint. Endpoint-level identifiers are passed as toolName values to pf_describe_endpoint and pf_call_endpoint.
When the task involves editing template documents or contentStructure, read these first:
skills/pdf-forge/SKILL.mdskills/pdf-forge/editor-capability-matrix.mdskills/pdf-forge/portable-document-contract.mdskills/pdf-forge/typst-rendering-boundaries.mdskills/pdf-forge/mcp-editor-workflows.md
Always separate:
- current UI support
- PortableDoc / schema support
- Typst renderer support
- documented agent-safe support
Never assume that TipTap support alone makes a feature safe to introduce via MCP.
Headers (Panel): X-Tenant-ID, X-Workspace-ID, Authorization (Bearer JWT)
Headers (Render): X-Tenant-Code, X-Workspace-Code, X-Environment (required, dev or prod), Authorization (Bearer JWT)
System: SUPERADMIN · Tenant: OWNER, ADMIN · Workspace: OWNER, ADMIN, EDITOR, OPERATOR, VIEWER
See core/docs/authorization-matrix.md.
YAML (core/settings/app.yaml) + env vars (DOC_ENGINE_* prefix).
Key settings: typst.bin_path, typst.max_concurrent (default: 20), server.port (default: 8080)
Auth: auth.panel (OIDC for panel) + auth.render_providers[] (additional OIDC for render). Omit auth for dummy mode.
CORS: server.cors.allowed_origins (default: ["*"]), server.cors.allowed_headers (default: [], extra headers)
See core/docs/configuration.md.
PostgreSQL 16, migrations in core/internal/migrations/sql/ (golang-migrate, embedded)
Run: make migrate (from root or core/)
Schemas: tenancy, identity, content, organizer
Config: .golangci.yml (project root). Key enforced limits:
funlen: 60 lines / 40 statementsgocognit: 15,gocyclo: 15,nestif: 4depguard: stdliblogpackage forbidden, uselog/sloggosec: enabled (excludes G104, G115)
React 19 + TypeScript SPA. Embedded in Go binary via go:embed (single binary deployment). Vite bundler.
pnpm --dir app dev # Vite on port 3000
pnpm --dir app build # Production build → app/dist/
pnpm --dir app lint # ESLint- TanStack Router — file-based in
app/src/routes/, auto-generatedrouteTree.gen.ts - Zustand stores:
auth-store.ts(JWT + roles),app-context-store.ts(tenant/workspace),theme-store.ts
- OIDC config fetched at runtime from backend
{VITE_BASE_PATH}/api/v1/config—src/lib/auth-config.ts - Mock auth:
VITE_USE_MOCK_AUTH=true - RBAC:
usePermission()hook +<PermissionGuard>component — always check core/docs/authorization-matrix.md before implementing permissions
- Axios client (
src/lib/api-client.ts) auto-attachesAuthorization,X-Tenant-ID,X-Workspace-ID - Base URL:
${VITE_BASE_PATH}/api/v1 - Always check OpenAPI spec before implementing API calls: MCP
pdf-forgetools orcore/docs/openapi.yaml - Multi-tenant headers can be sent per request in
pf_call_endpoint.headersor globally viaMCP_EXTRA_HEADERS
app/src/features/{name}/ → api/, components/, hooks/, types/
Current: auth, tenants, workspaces, documents, editor
- Tailwind CSS + shadcn/ui CSS variables — dark mode via
classstrategy - HSL CSS variables in
index.css - Design system: app/docs/design_system.md — always check before creating/modifying UI
- TipTap editor with StarterKit in
src/features/editor/, prose styling via@tailwindcss/typography - Body and header are different editing surfaces; do not treat header edits as generic body edits
- Before changing template version
contentStructure, verify the feature in the agent docs matrix/contract docs above - Distinguish what the UI exposes from what PortableDoc and Typst support internally
- i18next —
public/locales/{lng}/translation.json, supportsen,es
VITE_BASE_PATH # URL prefix for all routes (empty = root, e.g. /pdf-forge)
VITE_USE_MOCK_AUTH # "true" to skip OIDC (dev only)
Env files: .env.development, .env.production, .env.local (not committed)
Path alias: @/ → ./src/ (vite.config.ts)
Read the relevant doc BEFORE working on that area.
| Area | Doc |
|---|---|
| Domain concepts | core/docs/glossary.md |
| Config & deploy | core/docs/configuration.md, core/docs/deployment.md |
| Extending | core/docs/extensibility-guide.md |
| Auth & RBAC | core/docs/authorization-matrix.md |
| Architecture | core/docs/architecture.md, core/docs/decisions.md |
| Frontend | app/README.md |
| Design system | app/docs/design_system.md |
| Fork workflow | FORKING.md |
make build && make test && make lintmake swaggerif API changed- Update README.md if public API or config changed