Status: DRAFT β This document is awaiting review and may contain inaccuracies.
This document describes the standard directory layout of a Hoist application repository. Hoist apps follow a consistent structure across projects, combining a Grails 7 server-side backend with a React/TypeScript client-side frontend in a single repository. Understanding this structure is essential for navigating any Hoist codebase β the patterns described here are uniform across XH-built applications.
A Hoist application is a single Git repository containing both server and client code. The server is a Grails 7 application that includes hoist-core as a plugin dependency. The client is a React/TypeScript application that consumes hoist-react as an npm package. Both halves are built, tested, and deployed together, but run as separate processes during development and as separate containers in production.
The server runs on Tomcat (via Grails/Spring Boot), serving a REST API under a configurable path
(typically /api/). The client is a webpack-bundled SPA served by Nginx, which also reverse-proxies
API requests to Tomcat. In local development, webpack-dev-server proxies API calls to the Grails
bootRun process.
Every Hoist app repository has the same top-level shape:
my-app/
βββ grails-app/ # Server-side Grails application code
βββ src/main/groovy/ # Additional server-side source (non-artifact classes)
βββ client-app/ # Client-side React/TypeScript application
β βββ package.json # Dependencies and scripts (@xh/hoist, React, ag-Grid, etc.)
β βββ yarn.lock # Dependency lock file (or package-lock.json if using npm)
β βββ ...
βββ docker/ # Docker build files (Nginx + Tomcat)
β βββ nginx/
β βββ tomcat/
βββ gradle/ # Gradle wrapper distribution
βββ build.gradle # Gradle build configuration
βββ settings.gradle # Project name + optional composite build for inline hoist-core
βββ gradle.properties # App metadata, framework versions, dev flags
βββ .env.template # Required/optional environment variables (template)
βββ .env # Local environment values (git-ignored)
βββ gradlew / gradlew.bat # Gradle wrapper scripts
βββ CHANGELOG.md # Version history
βββ README.md # Project documentation
Some apps may also include:
helm/β Kubernetes Helm charts (for k8s deployments)infra/β Infrastructure-as-code (e.g. AWS CDK)docs/β Additional project documentationbin/β Utility scripts
Defines app identity and framework versions. Every app declares the same set of core properties:
xhAppCode=myApp
xhAppName=My Application
xhAppPackage=com.example.myapp
xhAppVersion=3.0-SNAPSHOT
grailsVersion=7.0.5
hoistCoreVersion=36.1.0
dotEnvGradlePluginVersion=4.0.0
hazelcast.version=5.6.0
runHoistInline=false
enableHotSwap=false
localDevXmx=2G
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024MKey properties:
| Property | Purpose |
|---|---|
xhAppCode |
Short identifier used in instance config env vars, log file names, and framework internals |
xhAppPackage |
Root Java/Groovy package for all server-side code |
hoistCoreVersion |
The published hoist-core version to use (ignored when runHoistInline=true) |
runHoistInline |
When true, uses a sibling ../hoist-core checkout via Gradle composite build |
enableHotSwap |
Enables JVM HotSwap agent for faster server-side dev iteration |
localDevXmx |
JVM max heap for local bootRun |
Minimal β sets the project name and optionally enables composite build for local hoist-core development:
rootProject.name = 'my-app'
if (parseBoolean(runHoistInline)) {
println "${xhAppName}: running with Hoist Core INLINE...."
includeBuild '../hoist-core'
}Configures the Grails web plugin, declares the hoist-core dependency and database drivers, sets up
JVM arguments for bootRun, and extends grails.build.info with Hoist metadata. The structure is
highly consistent across apps β the primary differences are database driver choices and
app-specific dependencies (e.g. JWT libraries, cloud SDKs).
We recommend JDK 17 or JDK 21 for client app builds. The published hoist-core JAR targets Java 17 bytecode and runs on any JDK 17+ runtime, so apps are not forced to track hoist-core's own build JDK.
When building Docker images, use the matching xh-tomcat base image variant for your runtime
JDK: next-tc10-jdk17 or next-tc10-jdk21.
JDK 25 is fully supported for client app builds β it just requires running Gradle itself on a pre-JDK 25 JVM and using a Gradle toolchain to compile against JDK 25. Grails 7 relies on deprecated features that were removed in Gradle 9, so apps are pinned to Gradle 8.x β whose daemon does not support JDK 25 as its runtime JVM. Pointing Gradle directly at JDK 25 results in:
Your build is currently configured to use incompatible Java 25 and Gradle 8.x. The maximum compatible Gradle JVM version is 24.
To build with JDK 25, run Gradle on JDK 24 and target JDK 25 via a toolchain:
- Install BOTH JDK 24 AND JDK 25 locally (IntelliJ: File β Project Structure β SDKs). Set JDK 25 as the project SDK.
- Set the Gradle JVM to JDK 24 (IntelliJ: Settings β Build, Execution, Deployment β Build Tools β Gradle β Gradle JVM).
- Declare the compile toolchain in
build.gradle:java { toolchain { languageVersion = JavaLanguageVersion.of(25) } }
Gradle will provision JDK 25 for compilation while the daemon itself runs on JDK 24. This will become unnecessary once the next Gradle major (compatible with JDK 25 as a daemon JVM) lands in a future Grails release.
Instance configuration is provided via environment variables loaded by the
co.uzzu.dotenv.gradle plugin. The .env.template file
is checked into source control and enumerates all required and optional variables. Developers copy
it to .env (git-ignored) and fill in local values.
Variable names follow the pattern APP_{APPCODE}_{KEY} β for example, APP_MYAPP_DB_HOST. These
are accessible in server code via InstanceConfigUtils.getInstanceConfig('dbHost'), which strips
the prefix and converts to camelCase.
Common variables include database connection details, environment name, OAuth credentials, SMTP settings, and bootstrap admin user credentials.
The server follows standard Grails conventions, with all application code organized under the
app's root package (e.g. com.example.myapp).
grails-app/
βββ conf/
β βββ application.groovy # Grails config β delegates to Hoist defaults
β βββ runtime.groovy # Runtime config β datasource, mail, CORS
β βββ ehcache.xml # Hibernate cache config (if needed)
βββ controllers/{package}/
β βββ BaseController.groovy # App-specific base controller
β βββ UrlMappings.groovy # Custom URL mappings (if needed)
β βββ ... # Feature-specific controllers
βββ domain/{package}/
β βββ ... # GORM domain classes
βββ init/{package}/
β βββ Application.groovy # Spring Boot entry point (boilerplate)
β βββ BootStrap.groovy # Startup initialization
β βββ ClusterConfig.groovy # Hazelcast network configuration
β βββ LogbackConfig.groovy # Logging configuration
βββ services/{package}/
β βββ ... # Grails services (business logic)
βββ i18n/ # Internationalization resources
Every Hoist app provides exactly four files in grails-app/init/:
Application.groovy β Boilerplate Spring Boot entry point. Identical across all apps:
@CompileStatic
class Application extends GrailsAutoConfiguration {
static void main(String[] args) {
GrailsApp.run(Application, args)
}
}BootStrap.groovy β Startup initialization. Implements LogSupport and contains an init
closure that:
- Logs an ASCII art startup banner (app name, version, build, environment)
- Calls
configService.ensureRequiredConfigsCreated()to register all app-specific soft configs - Calls
prefService.ensureRequiredPrefsCreated()to register all app-specific preferences - Optionally creates a bootstrap admin user for local development
- Calls
parallelInit()on app-specific services to initialize them concurrently
ClusterConfig.groovy β Extends io.xh.hoist.ClusterConfig to configure Hazelcast networking.
Typically uses multicast discovery for local development and a cloud-specific strategy (AWS ECS,
etc.) for production. Single-instance apps can leave clustering disabled.
LogbackConfig.groovy β Extends io.xh.hoist.LogbackConfig to inherit Hoist's default
logging configuration. Apps that need custom log formats override methods here; apps that don't
still must include this class (with an empty body) to properly inherit the base configuration.
application.groovy β Delegates to Hoist's default configuration, then adds app-specific
overrides:
import io.xh.hoist.configuration.ApplicationConfig
ApplicationConfig.defaultConfig(this)
hibernate {
show_sql = false
}Apps may also enable features like WebSockets (hoist.enableWebSockets = true) or configure
Hibernate schema names here.
runtime.groovy β Configures the datasource, mail, and other runtime settings. Delegates to
Hoist's RuntimeConfig.defaultConfig(this) for baseline config, then adds the app's database
connection (read from instance configs) and optional SMTP configuration.
Database configuration is often extracted to a separate DBConfig class in src/main/groovy/ for
clarity β this is a common pattern but not required.
Every Hoist app must provide implementations of three abstract services. See the
authentication.md and authorization.md docs for
full details.
| Service | Base Class | Purpose |
|---|---|---|
AuthenticationService |
BaseAuthenticationService |
Defines the authentication scheme (OAuth, SSO, form-based) |
UserService |
BaseUserService |
User lookup, creates HoistUser instances |
RoleService |
BaseRoleService |
Role assignment (or use the built-in DefaultRoleService) |
Apps that use monitors must also provide:
| Service | Base Class | Purpose |
|---|---|---|
MonitorDefinitionService |
BaseMonitorDefinitionService |
Defines application health checks |
Apps define an abstract BaseController extending io.xh.hoist.BaseController. This base is
typically minimal β sometimes empty β but provides a hook for app-wide controller behavior (e.g.
casting getUser() to an app-specific user type).
Feature controllers extend this base and are annotated with access annotations
(@AccessRequiresRole, @AccessAll, etc.). They delegate business logic to services and use
renderJSON() to return responses.
Apps that need custom URL routing beyond the default /$controller/$action?/$id? pattern can
define a UrlMappings.groovy file. Apps using only the standard patterns do not need one β the
default mappings provided by hoist-core are sufficient.
App services extend BaseService (from hoist-core), often through an app-specific intermediate
base class (e.g. BaseMyAppService) that adds shared helpers. Services are organized by feature
area within the app package.
Common service subpackages include:
security/βAuthenticationService,UserService,RoleService, OAuth token services- Feature-specific packages matching the app's domain
GORM domain classes for app-specific persistent data. Hoist's own domain classes (AppConfig, Preference, TrackLog, etc.) are provided by the hoist-core plugin β apps do not need to redeclare them.
Non-Grails-artifact classes: POGOs, enums, utility classes, and helper code that doesn't need to be a Grails service, controller, or domain class. Common examples include:
DBConfig.groovyβ Database configuration helper used byruntime.groovy- Data transfer objects and query result wrappers
- Enum types
- Utility classes
The client app is a React/TypeScript application built with webpack and consuming @xh/hoist
(hoist-react) as its primary dependency.
client-app/
βββ package.json # Dependencies and scripts
βββ webpack.config.js # Webpack build configuration
βββ tsconfig.json # TypeScript compiler configuration
βββ eslint.config.js # ESLint configuration
βββ .prettierrc.json # Prettier code formatting
βββ .stylelintrc # SCSS/CSS linting
βββ .npmrc # npm registry configuration
βββ .nvmrc # Node version specification
βββ yarn.lock # Dependency lock file (or package-lock.json)
βββ public/ # Static assets (favicons, error pages, images)
βββ src/
βββ Bootstrap.ts # Library initialization and service declarations
βββ apps/ # Webpack entry points (one per client app)
β βββ app.ts # Main application entry
β βββ admin.ts # Hoist Admin Console entry
βββ core/ # Shared infrastructure (services, types, columns, icons)
β βββ svc/ # Client-side services
β βββ ...
βββ app/ # Main application UI (matches apps/app.ts entry point)
β βββ AppModel.ts # Root application model
β βββ AppComponent.ts # Root UI component
β βββ App.scss # Global application styles
β βββ ... # Feature-specific view directories
βββ admin/ # Admin app UI (matches apps/admin.ts entry point)
βββ mobile/ # Mobile app UI, if applicable (matches apps/mobile.ts)
package.json β Declares @xh/hoist as the primary dependency along with React, ag-Grid,
Highcharts, and other UI libraries. Common scripts:
| Script | Purpose |
|---|---|
start |
Start webpack-dev-server for local development |
build |
Production webpack build |
startWithHoist |
Dev server using a local sibling hoist-react checkout |
lint |
Run ESLint and Stylelint |
webpack.config.js β Delegates to @xh/hoist-dev-utils/configureWebpack with app-specific
metadata:
const configureWebpack = require('@xh/hoist-dev-utils/configureWebpack');
module.exports = (env = {}) => {
return configureWebpack({
appCode: 'myApp',
appName: 'My Application',
appVersion: '3.0-SNAPSHOT',
favicon: './public/favicon.svg',
devServerOpenPage: 'app/',
sourceMaps: 'devOnly',
...env
});
};tsconfig.json β Targets ES2022 with React JSX support. Includes path aliases for inline
hoist-react development.
Each file in apps/ is a separate webpack entry point β a self-contained client application
sharing the same codebase. Every Hoist app has at minimum:
app.tsβ The main application, callingXH.renderApp()with the app's root component, model, and authentication configuration. By convention, nearly every Hoist app names its primary entry pointapp.ts, with the corresponding UI code in a siblingapp/directory.admin.tsβ The Hoist Admin Console, callingXH.renderAdminApp()with the framework's built-in admin UI. The corresponding admin UI code lives in anadmin/directory.
Some apps define additional entry points (e.g. mobile.ts, clientAdmin.ts), each with a
matching directory for its UI code.
A typical entry point:
import '../Bootstrap';
import {XH} from '@xh/hoist/core';
import {AppContainer} from '@xh/hoist/desktop/appcontainer';
import {AppComponent} from '../app/AppComponent';
import {AppModel} from '../app/AppModel';
XH.renderApp({
clientAppCode: 'app',
componentClass: AppComponent,
modelClass: AppModel,
containerClass: AppContainer,
isMobileApp: false,
enableLogout: true,
checkAccess: 'APP_READER'
});Executed before any app entry point. Responsible for:
- Service declarations β Imports app-specific client-side services and declares them on the
XHApiinterface via TypeScript module augmentation, making them accessible asXH.myService - ag-Grid setup β Registers required ag-Grid modules (community and enterprise) and installs the license key from server-side config
- Highcharts setup β Imports Highcharts modules and installs via
installHighcharts() - HoistUser augmentation β Declares any app-specific user properties
Each entry point in apps/ has a corresponding directory under src/ containing its UI code. By
convention, the directory name matches the entry point filename β e.g. apps/app.ts imports from
app/, apps/admin.ts imports from admin/. The main application directory contains:
AppModel.tsβ ExtendsHoistAppModel. Defines the application's top-level state, tab navigation, ViewManager instances, and overall data loading. This is the model class referenced in the entry point'sXH.renderApp()call.AppComponent.tsβ The root UI component, created withhoistCmp(). Renders the app's chrome (app bar, tab container, etc.) using theAppModel.
Feature-specific views are organized into subdirectories, each typically containing a model file and one or more component files.
Every app includes a docker/ directory with two containers:
docker/
βββ nginx/
β βββ Dockerfile # FROM xhio/xh-nginx:<version>
β βββ app.conf # Nginx site configuration
βββ tomcat/
βββ Dockerfile # FROM xhio/xh-tomcat:next-tc10-jdk17
βββ setenv.sh # JVM options (heap size, instance config path)
Serves the webpack-built client assets and reverse-proxies API requests to Tomcat. The
app.conf configures:
- Security headers β HSTS, CSP, X-Frame-Options, Permissions-Policy, X-Content-Type-Options
- SPA routing β
try_filesfallback toindex.htmlfor each client app entry point - API proxy β
/api/(or similar) proxied tohttp://localhost:8080/(Tomcat) - Cache policy β Static JS/CSS cached indefinitely (cache-busted by webpack hashes); index.html always re-fetched
- Root redirect β
/redirects to/app/(optionally with mobile detection)
Runs the Grails WAR file. The setenv.sh sets JVM heap size (defaulting to a configured amount,
overridable via JAVA_XMX env var) and optionally specifies the instance config file path.
In production, both containers run together within the same host or task (e.g. an AWS ECS Fargate task). Nginx listens on port 80/443 externally; Tomcat listens on port 8080 internally only. This sidecar pattern ensures API requests stay local (no network hop between web server and app server).
./gradlew bootRun -Duser.timezone=Etc/UTCStarts the Grails application on localhost:8080. The .env file provides instance configuration
(database credentials, environment, etc.).
cd client-app && yarn start # or: npm startStarts webpack-dev-server with hot module replacement. API requests are proxied to the Grails backend.
For developing hoist-core or hoist-react alongside the app, check out the framework repos as siblings and enable inline mode:
- hoist-core: Set
runHoistInline=trueingradle.properties(or~/.gradle/gradle.properties) - hoist-react: Run the
startWithHoistscript instead ofstart
| Convention | Pattern | Example |
|---|---|---|
| Server package root | xhAppPackage from gradle.properties |
com.example.myapp |
| Client app name | name field in client-app/package.json |
my-app |
| Instance config env vars | APP_{APPCODE}_{KEY} |
APP_MYAPP_DB_HOST |
| Log file names | {appCode}-{instanceName}-app.log |
myApp-inst1-app.log |
Every Hoist app will have these files (beyond the root-level build files):
| File | Location | Purpose |
|---|---|---|
Application.groovy |
grails-app/init/ |
Spring Boot entry point (boilerplate) |
BootStrap.groovy |
grails-app/init/ |
Config/pref registration, service init |
ClusterConfig.groovy |
grails-app/init/ |
Hazelcast networking |
LogbackConfig.groovy |
grails-app/init/ |
Logging config inheritance |
application.groovy |
grails-app/conf/ |
Grails config (delegates to Hoist) |
runtime.groovy |
grails-app/conf/ |
Datasource, mail, runtime config |
BaseController.groovy |
grails-app/controllers/ |
App-specific controller base |
AuthenticationService.groovy |
grails-app/services/ |
Auth implementation |
UserService.groovy |
grails-app/services/ |
User lookup implementation |
RoleService.groovy |
grails-app/services/ |
Role assignment implementation |
MonitorDefinitionService.groovy |
grails-app/services/ |
Health check definitions |
Bootstrap.ts |
client-app/src/ |
Library init, service declarations |
app.ts |
client-app/src/apps/ |
Main app entry point |
admin.ts |
client-app/src/apps/ |
Admin Console entry point |
AppModel.ts |
client-app/src/app/ |
Root application model |
AppComponent.ts |
client-app/src/app/ |
Root UI component |
The server and client communicate via JSON over HTTP (REST endpoints) and WebSocket. The server provides:
/xh/*β Hoist framework endpoints (auth, config, prefs, tracking, etc.) β served by hoist-core'sXhController/$controller/$actionβ App-specific endpoints defined by app controllers/rest/$controller/$id?β REST CRUD endpoints (forRestControllersubclasses)
The client consumes these via hoist-react's FetchService and app-specific service classes.
See the hoist-react documentation for detailed coverage of the client-side framework, including component architecture, state management with MobX, the grid system, and the admin console.