Skip to content

Latest commit

 

History

History
339 lines (238 loc) · 15.3 KB

File metadata and controls

339 lines (238 loc) · 15.3 KB

Agent Instructions

This file provides repository guidance for coding agents and automated assistants working with this codebase.

Critical Rules

  • NEVER use slog.Info() → always slog.InfoContext(ctx, ...)
  • NEVER use log package → depguard enforces log/slog only
  • ALWAYS run make swagger after 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 stay github.com/rendis/pdf-forge even in forks

Project Overview

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.

Commands

# 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 fmt

Repository Structure

go.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)

Scaffold (External Consumers)

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 tidy

Generates: 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.go uses //go:embed all:frontend-dist + engine.SetFrontendFS() for optional local embedding

Scaffold source: cmd/init/ (templates in cmd/init/templates/)

User Customization (core/extensions/)

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/sdkpublic API (Engine, interfaces, entity types, constructors) — use this for external consumers
  • github.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)

Extension Points

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.

Architecture

  • 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)

Render Flow

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

Dual Auth System

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 → custom RenderAuthenticator → OIDC render provider. No workspace membership check.

Middleware Stack Order

Recovery → Logger → CORS → Global middleware → Auth → Identity → Roles → API middleware → Controller

HTTP Routes

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.

MCP (OpenAPI Proxy)

Uses mcp-openapi-proxy with repo-local config:

  • Claude Code: .mcp.json
  • Codex: .codex/config.toml
  • Canonical MCP spec: core/docs/openapi.yaml (generated by make swagger)
  • Server name: pdf-forge
  • Tool prefix: pf

The proxy registers exactly 3 MCP tools:

  • pf_list_endpoints
  • pf_describe_endpoint
  • pf_call_endpoint

Correct workflow:

  1. pf_list_endpoints
  2. pf_describe_endpoint
  3. pf_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.md
  • skills/pdf-forge/editor-capability-matrix.md
  • skills/pdf-forge/portable-document-contract.md
  • skills/pdf-forge/typst-rendering-boundaries.md
  • skills/pdf-forge/mcp-editor-workflows.md

Always separate:

  1. current UI support
  2. PortableDoc / schema support
  3. Typst renderer support
  4. 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)

RBAC

System: SUPERADMIN · Tenant: OWNER, ADMIN · Workspace: OWNER, ADMIN, EDITOR, OPERATOR, VIEWER

See core/docs/authorization-matrix.md.

Configuration

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.

Database

PostgreSQL 16, migrations in core/internal/migrations/sql/ (golang-migrate, embedded)

Run: make migrate (from root or core/)

Schemas: tenancy, identity, content, organizer

Linter Constraints

Config: .golangci.yml (project root). Key enforced limits:

  • funlen: 60 lines / 40 statements
  • gocognit: 15, gocyclo: 15, nestif: 4
  • depguard: stdlib log package forbidden, use log/slog
  • gosec: enabled (excludes G104, G115)

Frontend

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

Routing & State

  • TanStack Router — file-based in app/src/routes/, auto-generated routeTree.gen.ts
  • Zustand stores: auth-store.ts (JWT + roles), app-context-store.ts (tenant/workspace), theme-store.ts

Auth & RBAC

  • OIDC config fetched at runtime from backend {VITE_BASE_PATH}/api/v1/configsrc/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

API Layer

  • Axios client (src/lib/api-client.ts) auto-attaches Authorization, X-Tenant-ID, X-Workspace-ID
  • Base URL: ${VITE_BASE_PATH}/api/v1
  • Always check OpenAPI spec before implementing API calls: MCP pdf-forge tools or core/docs/openapi.yaml
  • Multi-tenant headers can be sent per request in pf_call_endpoint.headers or globally via MCP_EXTRA_HEADERS

Feature Structure

app/src/features/{name}/api/, components/, hooks/, types/

Current: auth, tenants, workspaces, documents, editor

Styling

  • Tailwind CSS + shadcn/ui CSS variables — dark mode via class strategy
  • HSL CSS variables in index.css
  • Design system: app/docs/design_system.mdalways check before creating/modifying UI

Rich Text & i18n

  • 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
  • i18nextpublic/locales/{lng}/translation.json, supports en, es

Frontend Environment

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)

Documentation

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

PR Guidelines

  1. make build && make test && make lint
  2. make swagger if API changed
  3. Update README.md if public API or config changed