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.
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
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
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
-
Install mise version manager
-
Run the init script to set up your development environment:
./scripts/init.shThis 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)
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-8If you change the locale, run gradle --stop so the daemon restarts with it.
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` commandWhile 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.
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 / userWith 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/epistolaSee docs/auth.md for full details on OIDC setup, auto-provisioning, and safety guards.
Build a Docker image:
gradle :apps:epistola:bootBuildImageRun the container:
docker run --rm -p 8080:8080 epistola:0.0.1-SNAPSHOTThe 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.comSee charts/epistola/values.yaml for all available configuration options.
The main Spring Boot application that serves the document suite. Uses Thymeleaf for server-side HTML rendering and HTMX for dynamic interactions.
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 devThis project uses ktlint for Kotlin code style. Run the linter:
# Check code style
gradle ktlintCheck
# Auto-format code
gradle ktlintFormatgradle testTests use Testcontainers which requires Docker to be running.
.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.
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
publishlabel: tagged with{version}-pr-{number}-{run}
- On main: tagged with semantic version (based on conventional commits), SHA, and
- 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
- Triggered when changes are detected in the
Version bumps are determined automatically from commit messages:
feat:- minor version bumpfix:- patch version bumpfeat!:orfix!:withBREAKING 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.
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.
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.