Skip to content

About

Epistola is an open-source product for document creation and automation.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

6 stars

Watchers

1 watching

Forks

Latest commit

 

History

1,268 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Epistola Suite

Version Helm chart Build and Publish Coverage Security Scan License: AGPL-3.0

Note

Epistola Suite 1.0.0 is generally available (31 July 2026). GA REST APIs, catalog formats, configuration, and architecture follow SemVer compatibility. Explicitly labelled alpha and beta features remain experimental and may change in a minor release. Review the changelog before updating and see the Release & Support Policy for supported versions and upgrades. Published security issues and their affected releases are listed in the vulnerability history.

A document suite application built with Spring Boot 4.0.0 and Kotlin, featuring server-side rendered views with Thymeleaf and HTMX.

Project Structure

epistola-suite/
├── apps/
│   └── epistola/          # Main Spring Boot application (Thymeleaf + HTMX)
├── modules/
│   └── editor/            # Rich text editor component (Vite + TypeScript)
├── build.gradle.kts       # Root build configuration
└── settings.gradle.kts    # Module includes

Architecture

The frontend uses a server-side rendering approach:

  • Thymeleaf: Template engine for rendering HTML on the server
  • HTMX: For dynamic interactions without full page reloads
  • Client components: Embedded JavaScript modules (like the editor) for features requiring rich client-side interactivity

Documentation

Using Epistola? User documentation — guides and tutorials for authoring templates and running documents — is published at https://epistola.app/en/learn.

Working on Epistola? The technical documentation lives in docs/ and is indexed in docs/README.md — every page grouped by topic, each labelled with whether it describes shipped behavior, an experimental feature, a design proposal, or a point-in-time record. It also links the architecture decision records and the catalog wire-format reference.

Quick jumps: Project overview · Document generation · Deployment · Authentication · Testing · REST/MCP surfaces

Getting Started

Prerequisites

  1. Install mise version manager

  2. Run the init script to set up your development environment:

./scripts/init.sh

This will:

  • Configure mise in your shell (bash/zsh) if not already done
  • Ensure a UTF-8 locale (see Platform support below)
  • Install the required tool versions (Java Temurin 25, Node.js 24)
  • Install Git hooks (commitlint for conventional commit validation)
  • Configure SSH commit signing (if using SSH remote)

Platform support

Development is actively tested on macOS and Linux. Windows is not tested; if you're on Windows, develop inside WSL2 (treated as Linux).

Your shell must use a UTF-8 locale (LANG/LC_ALL ending in .UTF-8). The JVM derives sun.jnu.encoding — the charset for file paths — from the OS locale, and a non-UTF-8 locale (the POSIX C locale is the default on many WSL and minimal Linux images) breaks Kotlin builds when a source name contains a non-ASCII character:

java.nio.file.InvalidPathException: Malformed input or input contains unmappable characters

./scripts/init.sh configures this automatically. To verify:

locale charmap                          # -> UTF-8
java -XshowSettings:properties -version 2>&1 | grep sun.jnu.encoding
#                                       # -> sun.jnu.encoding = UTF-8

If you change the locale, run gradle --stop so the daemon restarts with it.

Build and Run

Tool versions (Java, Gradle, Node, pnpm and more) come from .mise.toml. The frontend builds first: the editor bundle is a Gradle input, so gradle build needs it in place.

# First time, or after .mise.toml changes
mise install

# Frontend first — also runs the component-registry check
pnpm install
pnpm build

# Build the entire project
gradle build

# Run the application (requires a profile — see Authentication below)
gradle :apps:epistola:bootRun --args='--spring.profiles.active=local'

# Run it with demo data: a tenant, the demo catalog and a well-known API key
gradle :apps:epistola-demo:bootRun --args='--spring.profiles.active=demo,local,localauth'

Tests: gradle unitTest (no Docker), gradle integrationTest and gradle uiTest (both need Docker), or gradle test for unit plus integration. Style: gradle ktlintFormat then gradle ktlintCheck; pnpm format covers everything else.

For live frontend work, run the app with the local profile in one terminal and pnpm --filter @epistola/editor watch in another — the watch rebuilds dist/ on change and the local profile serves it from the filesystem.

Software bill of materials:

gradle :apps:epistola:generateSbom
pnpm --filter @epistola/editor run sbom   # `run` matters: pnpm 11 has its own `sbom` command

While generating one, CycloneDX may print Unknown keyword meta:enum or Unknown keyword deprecated. Those are upstream SPDX schema-vocabulary notices from networknt, not Gradle deprecations — leave the generated SBOM validation enabled.

Authentication

A Spring profile must be set to configure authentication. Without one, the app will fail to start.

Setup Auth Method Use Case
local Form login (in-memory users) Local development
local,keycloak Form login + OAuth2/OIDC with local Keycloak Testing both login methods
localauth Form login with env-var credentials Staging/test fallback
prod + OIDC env OAuth2/OIDC with any compliant provider Production

Local development — uses in-memory users, no external dependencies:

gradle :apps:epistola:bootRun --args='--spring.profiles.active=local'
# Login: admin@local / admin  or  user@local / user

With local services — start PostgreSQL and Keycloak via the unified Docker Compose:

# Start PostgreSQL + Keycloak (admin console at http://localhost:4002, admin/admin)
docker compose -f apps/epistola/docker/docker-compose.yaml up -d

# Or start only PostgreSQL (sufficient for local profile)
docker compose -f apps/epistola/docker/docker-compose.yaml up -d postgres

# Run with form login and OAuth2
gradle :apps:epistola:bootRun --args='--spring.profiles.active=local,keycloak'

Production — configure OIDC via environment variables or the Helm chart's oidc.* values. Do not use the keycloak profile outside local development.

export SPRING_PROFILES_ACTIVE=prod
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KEYCLOAK_CLIENTID=epistola-suite
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KEYCLOAK_CLIENTSECRET=<your-secret>
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KEYCLOAK_SCOPE=openid,profile,email
export SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KEYCLOAK_ISSUERURI=https://sso.example.com/realms/epistola
export SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI=https://sso.example.com/realms/epistola

See docs/auth.md for full details on OIDC setup, auto-provisioning, and safety guards.

Docker

Build a Docker image:

gradle :apps:epistola:bootBuildImage

Run the container:

docker run --rm -p 8080:8080 epistola:0.0.1-SNAPSHOT

Helm Chart

The application can be deployed to Kubernetes using the Helm chart published to the OCI registry.

Install from the OCI registry:

helm install epistola oci://ghcr.io/epistola-app/charts/epistola --version <version>

Upgrade an existing release:

helm upgrade epistola oci://ghcr.io/epistola-app/charts/epistola --version <version>

Customize the deployment with values:

helm install epistola oci://ghcr.io/epistola-app/charts/epistola \
  --version <version> \
  --set image.tag=1.0.0 \
  --set ingress.enabled=true \
  --set ingress.hosts[0].host=epistola.example.com

See charts/epistola/values.yaml for all available configuration options.

Modules

apps/epistola

The main Spring Boot application that serves the document suite. Uses Thymeleaf for server-side HTML rendering and HTMX for dynamic interactions.

modules/editor

A Vite-based TypeScript editor component that builds as a library and is embedded in the main application for rich text editing functionality. The built assets are served from /editor/.

To develop the editor standalone:

cd modules/editor
npm install
npm run dev

Development

Code Style

This project uses ktlint for Kotlin code style. Run the linter:

# Check code style
gradle ktlintCheck

# Auto-format code
gradle ktlintFormat

Testing

gradle test

Tests use Testcontainers which requires Docker to be running.

MCP servers

.mcp.json registers MCP servers for coding agents: GitHub (issues, pull requests, projects) and the community forum. Each lives in .agents/mcp/ with a README covering its one-time setup; no secrets are committed.

CI/CD

The project uses GitHub Actions for CI/CD:

  • Build and test runs on all PRs and main branch pushes
  • Docker images are published to ghcr.io:
    • On main: tagged with semantic version (based on conventional commits), SHA, and latest
    • On PRs with publish label: tagged with {version}-pr-{number}-{run}
  • Helm charts are published to ghcr.io OCI registry:
    • Triggered when changes are detected in the charts/ directory
    • Tagged with chart-X.Y.Z (separate versioning from the app)
    • Published to oci://ghcr.io/epistola-app/charts

Conventional Commits

Version bumps are determined automatically from commit messages:

  • feat: - minor version bump
  • fix: - patch version bump
  • feat!: or fix!: with BREAKING CHANGE - major version bump

Both the app and Helm chart follow this convention. The Helm chart version is only bumped when changes are made to the charts/ directory.

Support and upgrades

Epistola Nederland B.V. publishes releases when ready and expects installations to stay on a supported version. Upgrades are designed to be direct (no intermediate releases required), with automated migrations and documented manual steps when needed. Downgrades after a successful upgrade are not supported; rollback of a failed upgrade is supported via backup restore.

Full details: SUPPORT_POLICY.md. Currently supported versions are published separately (website / release metadata), not hardcoded in the policy. See also docs/version-check.md and docs/migrations.md.

Shared with Epistola by default. A Suite installation shares its installation id and version with Epistola Nederland B.V. by default (and, once introduced, anonymous usage counters), so it can show whether the installation is supported and see which versions are in use. No hostnames, tenant, user or document data is ever shared. The daily release and security-advisory check sends nothing that identifies the installation. Turn sharing off with epistola.sharing.* (Helm: sharing), and the release check with epistola.version-check.enabled=false; with everything off the installation sends no data to Epistola. See Data shared with Epistola.

License

Copyright in the first-party portions of Epistola Suite is held by Epistola Nederland B.V. Identified third-party materials and separately attributed contributions remain subject to their respective notices.

Epistola Suite is distributed under the GNU Affero General Public License v3. The software is provided "as is", without warranty of any kind. Epistola Nederland B.V. accepts no responsibility for the correctness of output or for any damages incurred through use of the software. Correctness assurances, defect remediation, support, and contractual liability coverage are available only under a separate commercial Service Level Agreement (SLA) with Epistola Nederland B.V. See DISCLAIMER.md for the full statement.

About

Epistola is an open-source product for document creation and automation.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages