# Halo Admin Guide

This guide is the operational and architectural reference for **Halo**, the open-source website-building platform. It is written for administrators, DevOps engineers, and developers who deploy, operate, secure, extend, or contribute to Halo. It covers the system architecture, the complete configuration surface, deployment patterns, security hardening, day-to-day operations, the REST API, and the internals needed to build themes and plugins or to reason about the codebase.

> [!NOTE]
> Halo is a single Spring Boot application written in **Java 21** using the **reactive (WebFlux)** stack, fronted by a **Vue 3 + TypeScript** single-page console. It uses a **Kubernetes-style extension/custom-resource model** for all of its data. Understanding that model (Section 3) is the key to understanding everything else.

---

## 1. Executive Summary

| Attribute | Value |
|-----------|-------|
| **What it is** | Self-hosted CMS / website builder (blog, knowledge base, docs, company site). |
| **Backend** | Java 21, Spring Boot 3, Spring WebFlux (reactive, non-blocking, Netty). |
| **Frontend** | Vue 3 + TypeScript SPA (Console + User Center), built with Vite. |
| **Persistence** | Reactive R2DBC. H2 (default), PostgreSQL, MySQL, MariaDB. |
| **Data model** | Kubernetes-inspired Extensions (custom resources) with schemes, reconcilers, watchers, and indexers. |
| **Extensibility** | PF4J-based plugin system; Thymeleaf-based theme system; extension points. |
| **Search** | Apache Lucene full-text indexing. |
| **Cache** | Caffeine in-memory cache. |
| **Security** | Spring Security reactive: OAuth2/JWT, TOTP 2FA, remember-me, personal access tokens, RBAC, rate limiting. |
| **Default port** | `8090` (HTTP). |
| **Working directory** | `~/.halo2` (host) / `/root/.halo2` (container). |
| **License** | GPL-3.0. |
| **Distribution** | Docker image (`halohub/halo`, `ghcr.io/halo-dev/halo`) and executable Spring Boot JAR. |
| **Analyzed commit** | `7bf96ee` (version line `2.24.x`). |

Halo is designed so that a single operator can run a production website with minimal moving parts: one application process, one database, and a single working directory that holds all mutable state. Horizontal complexity (themes, plugins, storage backends, auth providers) is added at runtime through the extension and plugin systems rather than by changing core code.

---

## 2. System Architecture

### 2.1 High-level component diagram

```text
                          ┌──────────────────────────────────────────────┐
   Browser (visitors)     │                   Halo JVM                    │
        │                 │            (Spring Boot 3 / WebFlux)          │
        ▼                 │                                               │
  ┌───────────┐  HTTP     │  ┌────────────────────────────────────────┐  │
  │  Reverse  │──────────▶│  │ Netty reactive web server  :8090       │  │
  │   proxy   │  :8090    │  └───────────────┬────────────────────────┘  │
  │ (nginx/   │           │                  │  Spring Security filter    │
  │  caddy/   │◀──────────│                  │  chain (reactive)          │
  │  1Panel)  │           │     ┌────────────┼────────────┬───────────┐  │
  └───────────┘           │     ▼            ▼            ▼           ▼  │
        ▲                 │  Theme       /apis REST    /console     /uc  │
        │                 │  router      endpoints     (SPA)       (SPA) │
  Browser (admins)        │  (Thymeleaf) (console/uc/   static      static│
                          │              public/ext)   assets      assets│
                          │     │            │                           │
                          │     ▼            ▼                           │
                          │  ┌──────────────────────────────────────┐   │
                          │  │  Extension (custom-resource) layer    │   │
                          │  │  Scheme · ReactiveExtensionClient ·   │   │
                          │  │  Reconcilers (controllers) · Watchers │   │
                          │  │  · Lucene indexers                    │   │
                          │  └───────┬───────────────────┬──────────┘   │
                          │          ▼                   ▼               │
                          │   ┌────────────┐      ┌─────────────┐        │
                          │   │ R2DBC repo │      │  PF4J plugin │        │
                          │   │  (DB store)│      │   manager    │        │
                          │   └─────┬──────┘      └──────┬──────┘        │
                          └─────────┼────────────────────┼───────────────┘
                                    ▼                     ▼
                          ┌──────────────────┐   ┌──────────────────┐
                          │  Database         │   │ Working directory │
                          │ H2/PG/MySQL/Maria │   │  ~/.halo2         │
                          └──────────────────┘   │  db/ attachments/ │
                                                  │  themes/ plugins/ │
                                                  │  logs/ backups/   │
                                                  └──────────────────┘
```

### 2.2 Runtime stack

| Layer | Technology | Notes |
|-------|-----------|-------|
| Language / runtime | Java 21 | Compiled with `options.release = 21`; toolchain pinned to JDK 21. |
| Application framework | Spring Boot 3 | `@SpringBootApplication(scanBasePackages = "run.halo.app")`, `@EnableScheduling`, `@ConfigurationPropertiesScan`. |
| Web | Spring WebFlux | Fully reactive, non-blocking; Netty server. Virtual threads enabled (`spring.threads.virtual.enabled: true`). |
| Security | Spring Security (reactive) | OAuth2 jose/client/resource-server, custom auth managers. |
| Persistence | R2DBC | Reactive DB access; `spring-boot-starter-data-r2dbc`. |
| Templating | Thymeleaf (reactive) | Server-side rendering of the public theme. |
| Plugin runtime | PF4J | Isolated classloaders per plugin. |
| Cache | Caffeine | Via `spring-boot-starter-cache`. |
| Search | Apache Lucene | `lucene-core`, `lucene-queryparser`, `lucene-highlighter`, `lucene-analysis-common`. |
| Resilience | Resilience4j | Rate limiters for auth/signup/email/comment flows. |
| Email | Spring Boot Mail | Notification delivery. |
| API docs | springdoc-openapi (WebFlux) | Generates the OpenAPI spec consumed by the JS client. |
| Misc | Guava, Jsoup, Apache Tika, Thumbnailator, java-diff-utils, json-patch, BouncyCastle, ua-parser | Utility libraries (HTML parsing, MIME detection, image thumbnails, diffs/patches for revisions, crypto, user-agent parsing). |

> [!NOTE]
> The application runs on **virtual threads** (Project Loom) and a reactive web stack. Blocking calls inside reactive pipelines are an anti-pattern here; plugin authors must respect the reactive contract (return `Mono`/`Flux`, never block the event loop).

### 2.3 Gradle multi-module layout

Halo is a Gradle multi-project build. The modules:

| Module | Purpose |
|--------|---------|
| `:api` | The **public plugin API** (`run.halo.app.*`). Published as a library so plugins compile against a stable surface. Depends on Spring Boot starters (webflux, security, data-r2dbc, actuator, mail, thymeleaf, cache), Lucene, PF4J, etc. |
| `:application` | The **runnable application**. Bundles the API, the database drivers, the front-end build output, and the Spring Boot launcher. Produces the `halo` boot JAR and the Docker image. |
| `:platform:application` | A **BOM / dependency-management platform** (`java-platform`) that pins versions for the whole build via `libs.versions.toml` bundles. |
| `:platform:plugin` | The dependency platform that **plugins** depend on, so plugin builds align with the host's library versions. |
| `:buildSrc` | Groovy Gradle convention plugins (e.g. `halo.publish`). |
| `:ui` | The Vue front-end (pnpm workspace). Built with Node and copied into `:application` resources at build time. |

The front-end `:ui` module is itself a pnpm workspace with published packages:

| Package | Purpose |
|---------|---------|
| `@halo-dev/api-client` | OpenAPI-generated TypeScript REST client (core/console/uc/public). |
| `@halo-dev/components` | Base UI component library (MIT). |
| `@halo-dev/ui-shared` | Shared utilities/types/stores for plugins (`definePlugin`, etc.). |
| `@halo-dev/richtext-editor` | The default Tiptap/ProseMirror editor. |
| `@halo-dev/ui-plugin-bundler-kit` | Vite/Rsbuild build presets for plugin front-ends. |

> [!TIP]
> The console (`console.html`) and User Center (`uc.html`) are **two entry points of one Vite multi-page app** sharing `ui/src`. In production the backend serves `/console/**` → `console.html`, `/uc/**` → `uc.html`, and `/ui-assets/**` → static assets.

---

## 3. The Extension (Custom-Resource) Model — Architectural Keystone

Everything Halo stores — posts, pages, users, roles, themes, plugins, comments, settings — is an **Extension**: a typed, versioned, JSON document modeled directly on Kubernetes custom resources. Internalizing this model is the single most important thing for operating or extending Halo.

### 3.1 Core concepts

| Concept | Kubernetes analog | In Halo (`run.halo.app.extension`) |
|---------|-------------------|-------------------------------------|
| **Extension** | Custom Resource | `Extension` / `AbstractExtension` — a document with `apiVersion`, `kind`, `metadata`, `spec`, `status`. |
| **GVK** | GroupVersionKind | `GroupVersionKind` / `GVK` annotation — identifies a type, e.g. `content.halo.run/v1alpha1, Kind=Post`. |
| **Scheme** | API schema registration | `Scheme` / `SchemeManager` — registers a type, its GVK, JSON schema, and plural/singular names. |
| **Client** | API server client | `ReactiveExtensionClient` / `ExtensionClient` — reactive CRUD over the store (`create`, `get`, `update`, `delete`, `list`, `listBy`, `watch`). |
| **Metadata** | ObjectMeta | `Metadata` — `name`, `labels`, `annotations`, `creationTimestamp`, `version` (optimistic locking), `finalizers`. |
| **Reconciler** | Controller | `run.halo.app.extension.controller.*` — watches a kind and drives actual state toward desired state. |
| **Watcher** | Informer/watch | `Watcher` — receives create/update/delete events. |
| **Unstructured** | Unstructured | `Unstructured` / `JsonExtension` — schema-less access to any extension. |
| **ConfigMap / Secret** | ConfigMap / Secret | `ConfigMap`, `Secret` — built-in extension kinds for configuration and credentials. |

### 3.2 How a request flows

1. A console/API call hits a reactive endpoint (for example "create post").
2. The handler builds a `Post` extension and persists it via `ReactiveExtensionClient.create(...)`.
3. The extension is serialized to JSON and stored as a row in the database (R2DBC).
4. A **watcher** notices the new/changed extension and notifies the registered **reconciler** (controller).
5. The reconciler performs side effects: generating permalinks, updating counters, reindexing for search, rendering, cleanup, etc. — and may update the extension's `status`.
6. **Indexers** maintain in-memory indexes (backed by Lucene for full-text) so list/query operations (`ListOptions`, field/label selectors) are fast without scanning the database.

> [!IMPORTANT]
> Optimistic concurrency is enforced via `metadata.version`. Updates carry the version they were read at; a mismatch means another writer won, and the update is rejected so the caller can retry. Plugin code must handle these retries.

### 3.3 Why this matters operationally

- **The database is a document store of JSON extensions**, not a hand-designed relational schema you query directly. Do not edit rows by hand.
- **Backups must capture extensions + working files together** — which is exactly what the full-site backup feature does (Section 9).
- **Reconcilers run asynchronously.** After an import or restore, give controllers time to converge (rebuild indexes, regenerate derived data).
- **Plugins extend the model** by registering new schemes and reconcilers — they participate as first-class citizens in the same machinery as core types.

---

## 4. Configuration Mastery

Halo is configured the standard Spring Boot way. **Every** setting can be supplied as:

1. A command-line argument: `--halo.external-url=https://example.com/`
2. An environment variable: `HALO_EXTERNAL_URL=https://example.com/` (relaxed binding: dots/dashes → underscores, uppercased)
3. A key in `application.yaml` placed in the working directory or on the classpath.

Precedence (highest first): command-line args → environment variables → external `application.yaml` → bundled defaults. In the official Docker image, `SPRING_CONFIG_LOCATION` is set to `optional:classpath:/;optional:file:/root/.halo2/`, so a `application.yaml` you drop into the mounted working directory is automatically picked up.

### 4.1 `halo.*` — application settings

These are bound from `run.halo.app.infra.properties.HaloProperties` and its nested classes (`@ConfigurationProperties(prefix = "halo")`). This is the authoritative, source-derived reference.

#### Top-level `halo`

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `halo.work-dir` | Path | `${user.home}/.halo2` | Root directory for all mutable state (DB file, attachments, themes, plugins, logs, backups). **Required.** |
| `halo.external-url` | URL | *(none)* | Public base URL of the site. Used to build absolute links (feeds, sitemap, social cards, emails). Must be a valid `http`/`https` URL. |
| `halo.use-absolute-permalink` | boolean | `false` | Use absolute permalinks for posts/pages/categories/tags. **Requires `external-url` to be set** (validated at startup). |
| `halo.initial-extension-locations` | Set\<String\> | *(empty)* | Extra locations of extension YAML to load on startup. |
| `halo.required-extension-disabled` | boolean | `false` | Disables initialization of required classpath extensions (advanced/testing). |

#### `halo.security.*`

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `halo.security.basic-auth.disabled` | boolean | `true` | HTTP Basic auth is **off** by default. |
| `halo.security.two-factor-auth.disabled` | boolean | `false` | Globally disable TOTP 2FA. |
| `halo.security.frame-options.disabled` | boolean | `false` | Disable `X-Frame-Options`. |
| `halo.security.frame-options.mode` | enum | `SAMEORIGIN` | `X-Frame-Options` mode (`DENY`, `SAMEORIGIN`). |
| `halo.security.referrer-options.policy` | enum | `STRICT_ORIGIN_WHEN_CROSS_ORIGIN` | `Referrer-Policy` header value. |
| `halo.security.cors-options.disabled` | boolean | `false` | Disable built-in CORS handling. |
| `halo.security.cors-options.configs[].path-pattern` | String | — | Path pattern for a CORS config entry. |
| `halo.security.cors-options.configs[].config` | object | — | Standard CORS settings for that path. |
| `halo.security.remember-me.token-validity` | Duration | `14d` | Lifetime of the remember-me token. |
| `halo.security.remember-me.rotation-cooldown` | Duration | `5m` | Minimum interval between token rotations (prevents false-positive theft detection from stale cookies). |
| `halo.security.password-reset-methods[].name` | String | `email` (preset) | A password-reset method identifier. |
| `halo.security.password-reset-methods[].href` | URI | `/password-reset/email` | The reset flow endpoint. |
| `halo.security.password-reset-methods[].icon` | URI | `/images/...` | Icon shown on the reset page. |

> [!NOTE]
> JWT/OAuth2 signing keys are bound separately (`JwtProperties`) under `halo.security.oauth2.jwt.*`: `public-key-location`, `private-key-location`, `issuer-uri`, and `jws-algorithm` (default `RS256`). Halo manages RSA keys in the working directory by default; only override these if you have a specific key-management strategy.

#### `halo.attachment.*`

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `halo.attachment.resource-mappings[].path-pattern` | String | `/upload/**` (preset) | URL pattern mapped to a local attachment folder (compatibility with Halo 1.x paths). |
| `halo.attachment.resource-mappings[].locations` | List\<String\> | `migrate-from-1.x` | Folders (relative to the attachments dir) served under the pattern. |
| `halo.attachment.thumbnail.disabled` | boolean | `false` | Disable automatic thumbnail generation. |
| `halo.attachment.thumbnail.concurrent-threads` | int (≥1) | *(impl default)* | Threads used for thumbnail generation. |
| `halo.attachment.thumbnail.quality` | double (0.0–1.0) | *(impl default)* | JPEG quality of generated thumbnails. |

#### `halo.theme.*`, `halo.ui.*`, `halo.extension.*`

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `halo.theme.initializer.disabled` | boolean | `false` | Skip installing the default theme on first start. |
| `halo.theme.initializer.location` | String | `classpath:themes/theme-earth.zip` | Default theme package to install. |
| `halo.theme.generator-meta-disabled` | boolean | `false` | Suppress the `generator` meta tag in rendered pages. |
| `halo.ui.location` | String | `classpath:/ui/` | Location of the built front-end assets. |
| `halo.ui.proxy.enabled` | boolean | `false` | **Dev only:** proxy console/uc HTML to a running Vite dev server. |
| `halo.ui.proxy.endpoint` | URI | *(none)* | **Dev only:** the dev server URL (e.g. `http://localhost:3000/`). |
| `halo.extension.controller.disabled` | boolean | `false` | Disable the extension reconciler controllers (advanced/testing). |

#### `halo.notification.*`

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `halo.notification.auto-cleanup.enabled` | boolean | `false` | Periodically delete old notifications. |
| `halo.notification.auto-cleanup.retention-days` | int | `30` | Age threshold for cleanup (`0` = disabled). |
| `halo.notification.auto-cleanup.cleanup-cron` | cron | `0 0 3 * * ?` | When cleanup runs (default daily at 03:00). |

### 4.2 `spring.*` and server settings (from `application.yaml`)

| Property | Default | Description |
|----------|---------|-------------|
| `server.port` | `8090` | HTTP listen port. |
| `server.forward-headers-strategy` | `native` | Honor `X-Forwarded-*` from a reverse proxy. |
| `server.compression.enabled` | `true` | Gzip responses. |
| `spring.r2dbc.url` | `r2dbc:h2:file:///${halo.work-dir}/db/halo-next?MODE=MySQL...` | Reactive DB URL. |
| `spring.r2dbc.username` / `.password` | `admin` / `123456` | DB credentials (H2 file default — change for external DBs). |
| `spring.sql.init.mode` | `always` | Run schema initialization. |
| `spring.sql.init.platform` | `h2` | Selects schema/migration dialect (`h2`/`mysql`/`mariadb`/`postgresql`). |
| `spring.http.codecs.max-in-memory-size` | `10MB` | Max in-memory buffer for request/response bodies. |
| `spring.cache.type` | `caffeine` | Cache provider. |
| `spring.cache.caffeine.spec` | `expireAfterAccess=1h, maximumSize=10000` | Caffeine sizing/eviction. |
| `spring.threads.virtual.enabled` | `true` | Use virtual threads. |
| `spring.web.resources.cache.cachecontrol.max-age` | `365d` | Static asset cache lifetime. |
| `spring.thymeleaf.reactive.max-chunk-size` | `8KB` | Reactive template chunk size. |
| `logging.file.name` | `${halo.work-dir}/logs/halo.log` | Log file path. |
| `logging.logback.rollingpolicy.max-file-size` | `10MB` | Per-file log rotation size. |
| `logging.logback.rollingpolicy.total-size-cap` | `1GB` | Total log size cap. |
| `r2dbc.migrate.resources-path` | `classpath:/db/migration/${platform}/*.sql` | Migration scripts location. |

### 4.3 Rate limiters (`resilience4j.ratelimiter.configs.*`)

Halo ships these named limiters (defaults from `application.yaml`). They protect abuse-prone flows.

| Limiter | Limit | Window | Guards |
|---------|-------|--------|--------|
| `authentication` | 3 | 1 min | Login attempts. |
| `comment-creation` | 10 | 1 min | Comment posting. |
| `signup` | 3 | 1 hour | New-user signup. |
| `send-email-verification-code` | 1 | 1 min | Sending verification codes. |
| `verify-email` | 3 | 1 hour | Email verification attempts. |
| `send-password-reset-email` | 10 | 1 min | Password-reset emails. |
| `password-reset-verification` | 10 | 1 min | Password-reset verification. |

### 4.4 Environment variables and JVM options

| Variable | Default (image) | Description |
|----------|-----------------|-------------|
| `HALO_WORK_DIR` | `/root/.halo2` | Working directory (binds `halo.work-dir`). |
| `SPRING_CONFIG_LOCATION` | `optional:classpath:/;optional:file:/root/.halo2/` | Where Spring looks for `application.yaml`. |
| `JVM_OPTS` | *(empty)* | Extra JVM flags (heap, GC). Example: `-Xmx512m -Xms512m`. |
| `TZ` | `Asia/Shanghai` | Container timezone. |

> [!TIP]
> Any `application-<profile>.yaml` (e.g. `application-postgresql.yaml`) ships in the image as a convenience profile. Activate with `--spring.profiles.active=postgresql`, or just set the three `spring.r2dbc.*` / `spring.sql.init.platform` values directly as shown in the User Guide.

---

## 5. Database

### 5.1 Supported engines and selection

Halo uses **R2DBC** (reactive) drivers, not JDBC. The bundled runtime drivers are `r2dbc-h2`, `r2dbc-postgresql` (+ `postgresql`), `r2dbc-mariadb`, and `r2dbc-mysql` (`io.asyncer:r2dbc-mysql`).

| Engine | `spring.r2dbc.url` example | `spring.sql.init.platform` |
|--------|----------------------------|----------------------------|
| H2 (file) | `r2dbc:h2:file:///${halo.work-dir}/db/halo-next?MODE=MySQL&DB_CLOSE_ON_EXIT=FALSE` | `h2` |
| PostgreSQL | `r2dbc:pool:postgresql://HOST:5432/halo` | `postgresql` |
| MySQL | `r2dbc:pool:mysql://HOST:3306/halo` | `mysql` |
| MariaDB | `r2dbc:pool:mariadb://HOST:3306/halo` | `mariadb` |

> [!IMPORTANT]
> `spring.sql.init.platform` must match your engine. It selects both the schema initialization SQL and the migration script directory (`classpath:/db/migration/<platform>/*.sql`). A mismatch causes startup failures or wrong-dialect SQL.

### 5.2 Schema and migrations

- Initial schemas ship as `schema-h2.sql`, `schema-mysql.sql`, `schema-mariadb.sql`, `schema-postgresql.sql`.
- Ongoing migrations live in `db/migration/{h2,mysql,mariadb,postgresql}/` and are applied by **r2dbc-migrate** at startup.
- `spring.sql.init.mode: always` means initialization runs on every boot (idempotent).

Because all domain data is stored as JSON **extensions** (Section 3), the relational schema is intentionally generic (a small set of tables holding serialized extensions plus supporting structures). **Do not hand-edit rows.** Use the API or the console.

### 5.3 Connection pooling

Prefix the R2DBC URL with `r2dbc:pool:` to enable connection pooling for external databases (as in the Compose example). Tune pool size via standard Spring R2DBC pool properties if needed.

---

## 6. Deployment

### 6.1 Docker image internals

The image is built from `eclipse-temurin:21-jre` using Spring Boot's **layered jar** + **tools** extraction, and it pre-computes a **CDS archive** for faster startup:

- `java -Djarmode=tools -jar application.jar extract --layers` splits the jar into `dependencies/`, `spring-boot-loader/`, `snapshot-dependencies/`, `application/` layers (better Docker layer caching).
- A build step runs `-XX:ArchiveClassesAtExit=application.jsa -Dspring.context.exit=onRefresh` to generate an **AppCDS** archive, then the entrypoint launches with `-XX:SharedArchiveFile=application.jsa` for reduced startup time and memory.
- `EXPOSE 8090`; entrypoint execs `java ${JVM_OPTS} ... -jar application.jar "$@"`, so extra `--flag=value` args you pass to `docker run` reach the app.

Official image coordinates:

| Registry | Image |
|----------|-------|
| Docker Hub | `halohub/halo` |
| GHCR | `ghcr.io/halo-dev/halo` |

> [!TIP]
> Set `JVM_OPTS` to bound heap in containers, e.g. `-e JVM_OPTS="-Xmx512m -Xms512m"`. The Compose example uses `256m`; size it to your traffic and plugin set.

### 6.2 Reverse proxy

Run Halo behind a proxy that terminates TLS and forwards **all** paths (`/`, `/console`, `/uc`, `/apis/**`, `/ui-assets/**`). Because `server.forward-headers-strategy: native`, Halo trusts `X-Forwarded-Proto`/`X-Forwarded-Host`/`X-Forwarded-For` to reconstruct correct external URLs.

Minimal Nginx location:

```nginx
location / {
    proxy_pass http://127.0.0.1:8090;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_http_version 1.1;
}
```

> [!NOTE]
> The project recommends **1Panel** (a server management panel) for turnkey deployment, which wires up the reverse proxy, SSL certificates, and backup/upgrade tasks automatically.

### 6.3 Building from source

```bash
# Full build (backend + frontend) — produces application/build/libs/halo-*.jar
./gradlew clean build

# Skip checks for a faster local build
./gradlew clean build -x check

# Build the Docker image to local Docker
./gradlew :application:bootBuildImage
```

The build flow: `:ui:doBuild` (pnpm builds Vue → `ui/build/dist`) → `copyUiDist` copies it into the application's resources → `bootJar` packages the executable JAR → `createChecksums` writes SHA-256 sidecars. Several first-party plugins (`comment-widget`, `search-widget`, `sitemap`, `feed`, `shiki`, `editor-hyperlink-card`, app-store) are downloaded into `resources/main/presets/plugins` by the `downloadPluginPresets` task so a fresh install has working defaults.

> [!NOTE]
> Requirements to build from source: **JDK 21**, **Node `^24`**, and **pnpm 10.x** (the repo pins `packageManager`). The Gradle Node plugin provisions Node automatically for CI builds.

---

## 7. Security

### 7.1 Authentication mechanisms

Halo's reactive Spring Security chain (`run.halo.app.security.authentication.*`) supports multiple credential types:

| Mechanism | Package | Notes |
|-----------|---------|-------|
| **Username/password (form login)** | `authentication/login` | The primary console/UC login. Encrypted password transport (jsencrypt webjar on the client). |
| **OAuth2 / OIDC** | `authentication/oauth2` | Social/SSO login via `spring-security-oauth2-client`; resource-server + jose for JWT validation. |
| **Two-factor (TOTP)** | `authentication/twofactor/totp` | Time-based one-time passwords (`two-factor-auth` library), per-user opt-in. |
| **Remember-me** | `authentication/rememberme` | 14-day rotating token with theft detection and rotation cooldown. |
| **Personal access tokens (PAT)** | `authentication/token` | Bearer tokens for API/automation, scoped, revocable. |
| **Pre-auth / device** | `security/preauth`, `security/device` | Device tracking (ua-parser) and pre-authentication handling. |
| **Switch-user (impersonation)** | `security/switchuser` | Administrative user switching. |
| **HTTP Basic** | (built-in) | **Disabled by default** (`halo.security.basic-auth.disabled=true`). |

JWTs are signed with **RSA** keys (`RS256` by default) managed under the working directory; OAuth2 jose handles validation.

### 7.2 Authorization (RBAC)

Authorization (`security/authorization`) is **role-based**. Roles are extensions that bundle permission rules; users are granted roles; effective permissions are the union. UI routes also carry `permissions` metadata so the console hides actions a user cannot perform. The `super-role` has unrestricted access (including sensitive actuator data — see below).

### 7.3 Transport and header hardening

Defaults set sane security headers: `X-Frame-Options: SAMEORIGIN`, `Referrer-Policy: strict-origin-when-cross-origin`, configurable CORS, and HTTPS-awareness via forwarded headers. Run behind TLS in production.

### 7.4 Abuse protection

Resilience4j rate limiters (Section 4.3) throttle login, signup, comment creation, and email/password-reset flows. These materially reduce credential-stuffing and spam without extra infrastructure.

### 7.5 Hardening checklist and honest risk notes

- [ ] **Change default database credentials.** The bundled `application.yaml` ships `admin`/`123456` for the *file H2* database. For any external database, set strong `spring.r2dbc.username`/`password`. Never expose the H2 file DB beyond the host.
- [ ] **Restrict Actuator exposure.** `application.yaml` sets `management.endpoints.web.exposure.include: "*"` and marks **`shutdown` and `heapdump` access as `unrestricted`**. These are powerful: `shutdown` can stop the app and `heapdump` can leak memory contents. They run on the **same port (8090)**. **Do not expose `/actuator/**` to the public** — block it at the reverse proxy, or override the exposure to a minimal set (`health,info`) and lock down `shutdown`/`heapdump` in production. Health details are already gated (`show-details: when-authorized`, `roles: super-role`).
- [ ] **Set `halo.external-url`** to your real HTTPS URL so links and OAuth redirects are correct.
- [ ] **Enable 2FA** for administrator accounts (`halo.security.two-factor-auth.disabled=false`, then enroll per user in `/uc`).
- [ ] **Scope and rotate PATs**; treat them as secrets.
- [ ] **Vet plugins/themes.** Plugins run in-process with broad access; only install trusted packages.
- [ ] **Keep Halo updated** — schema migrations run automatically; security fixes land in patch releases.
- [ ] **Run as non-root** where possible and mount the working directory on durable, backed-up storage.

> [!CAUTION]
> The combination of `exposure.include: "*"` + unrestricted `shutdown`/`heapdump` is convenient in development but is the single most important thing to lock down before exposing Halo to the internet.

---

## 8. The REST API

Halo is API-first: the entire console and User Center are built on the same public REST API, documented via **OpenAPI** (springdoc) and consumed through the generated `@halo-dev/api-client`.

### 8.1 API groups

The OpenAPI spec is generated into five grouped documents (from the build's `openApi.groupedApiMappings`):

| Group | File | Audience / purpose |
|-------|------|--------------------|
| **aggregated** | `aggregated.json` | The full combined surface (used to generate the JS client). |
| **public** | `apis_public.api_v1alpha1.json` | Endpoints safe for anonymous/public consumption. |
| **console** | `apis_console.api_v1alpha1.json` | Admin console operations. |
| **uc** | `apis_uc.api_v1alpha1.json` | User Center (self-service) operations. |
| **extension** | `apis_extension.api_v1alpha1.json` | Generic CRUD over custom resources (extensions). |

Endpoints live under `/apis/...` paths, versioned `v1alpha1`, grouped by API group (e.g. `content.halo.run`, `api.console.halo.run`, `uc.api.halo.run`). Example: `GET /apis/api.console.halo.run/v1alpha1/posts`.

### 8.2 Authentication

Authenticate API calls with either a session (cookie, as the SPA does) or a **personal access token** as a bearer token:

```bash
curl -H "Authorization: Bearer <your-PAT>" \
  https://blog.example.com/apis/api.console.halo.run/v1alpha1/posts
```

### 8.3 JavaScript client

The official client wraps every group:

```javascript
import {
  coreApiClient,     // CRUD over custom models (extensions)
  consoleApiClient,  // console operations
  ucApiClient,       // user-center operations
  publicApiClient,   // public endpoints
  createCoreApiClient,
} from "@halo-dev/api-client";

coreApiClient.content.post.listPost().then((res) => {
  console.log(res.data.items);
});

// External app: bring your own axios instance
import axios from "axios";
const core = createCoreApiClient(axios.create({ baseURL: "http://localhost:8090" }));
```

> [!NOTE]
> The client is **OpenAPI-generated** (`openapi-generator` `typescript-axios`) from `api-docs/openapi/v3_0/aggregated.json`. Regenerate it with the `api-client:gen` pnpm script whenever the backend API changes. The OpenAPI doc itself is produced by running the app under the `doc` profile (`--spring.profiles.active=doc`) and scraping `/v3/api-docs/...`.

---

## 9. Operations

### 9.1 Working-directory layout

Everything mutable lives under `halo.work-dir` (`~/.halo2`):

| Path | Contents |
|------|----------|
| `db/` | H2 database file (if using H2). |
| `attachments/` | Uploaded media (for local storage policies). |
| `themes/` | Installed themes. |
| `plugins/` | Installed plugin JARs and their data. |
| `logs/halo.log` | Application log (rotated, 10MB/file, 1GB cap). |
| `backups/` | Full-site backup archives. |
| *(security keys)* | RSA keys for JWT signing (managed by Halo). |

> [!IMPORTANT]
> The working directory **is the site.** Mount it on durable storage, monitor its disk usage (attachments and logs grow), and include it in your backup strategy.

### 9.2 Start / stop / logs

```bash
# Docker
docker logs -f halo
docker restart halo
docker stop halo && docker start halo

# JAR (systemd recommended in production)
java -jar halo.jar --halo.work-dir=/opt/halo/data
tail -f /opt/halo/data/logs/halo.log
```

### 9.3 Health and monitoring

Spring Boot Actuator is enabled with Kubernetes-style probes:

| Endpoint | Use |
|----------|-----|
| `/actuator/health` | Overall health (details gated to `super-role`). |
| `/actuator/health/liveness` | Liveness probe (`probes.enabled: true`). |
| `/actuator/health/readiness` | Readiness probe. |
| `/actuator/info` | Build/Java/OS info. |

Example Kubernetes probes:

```yaml
livenessProbe:
  httpGet: { path: /actuator/health/liveness, port: 8090 }
readinessProbe:
  httpGet: { path: /actuator/health/readiness, port: 8090 }
```

> [!WARNING]
> Restrict `/actuator/**` at the proxy (see Section 7.5). Probes can be allowed internally while the rest of the actuator surface is blocked from the public internet.

### 9.4 Backup and restore

The full-site backup (`migration.halo.run/v1alpha1, Kind=Backup`) bundles **database extensions + working-directory files** into a single ZIP under `${work-dir}/backups/`. Operate it from **System → Backup** in the console:

- Backups run **asynchronously**; the `Backup` resource's `status.phase` moves `Pending → Running → Succeeded/Failed`, with `status.filename` on success.
- `spec.autoDeleteWhen` controls retention; users can grant backup permission to specific accounts.
- Backup is the **supported cross-database migration path** (H2 → PostgreSQL, etc.).

> [!IMPORTANT]
> Take a fresh backup **before** restoring or upgrading. Restored data may have `creationTimestamp` overwritten by the current time. Supported for Halo 2.8.x and newer.

### 9.5 Upgrades

1. Back up (console backup + DB dump if external).
2. Pull the new image tag / replace the JAR.
3. Recreate the container with the **same working-directory volume and same database**.
4. On startup, r2dbc-migrate applies any new schema migrations automatically.
5. Verify the console loads and reconcilers have converged (search index, counters).

---

## 10. Performance Tuning

| Area | Lever | Guidance |
|------|-------|----------|
| **JVM heap** | `JVM_OPTS=-Xmx… -Xms…` | Start at 512MB for small sites; raise with traffic/plugins. CDS (`application.jsa`) already reduces startup cost. |
| **Concurrency** | `spring.threads.virtual.enabled` | Already `true`; virtual threads handle high request fan-out cheaply. |
| **Cache** | `spring.cache.caffeine.spec` | Default `expireAfterAccess=1h, maximumSize=10000`. Increase `maximumSize` for large catalogs. |
| **DB pool** | `r2dbc:pool:` + pool props | Use pooling for external DBs; size to DB capacity. |
| **Static assets** | `spring.web.resources.cache.cachecontrol.max-age` | Default 365d; assets are fingerprinted, so long caching is safe. |
| **Thumbnails** | `halo.attachment.thumbnail.concurrent-threads` / `quality` | Tune for image-heavy sites; disable if you offload to a CDN/storage plugin. |
| **Compression** | `server.compression.enabled` | On by default; keep on (or move to the proxy). |
| **Body buffers** | `spring.http.codecs.max-in-memory-size` | Default 10MB; raise for large API payloads, watch memory. |
| **Search** | Lucene index | Rebuilt by reconcilers; large sites benefit from adequate heap and fast disk. |

> [!TIP]
> Halo scales **vertically** very well on a single node thanks to the reactive stack + virtual threads. The usual bottlenecks are the database and image processing, not the web tier.

---

## 11. Extending Halo

### 11.1 Plugins (PF4J)

Plugins are JARs loaded by **PF4J** with isolated classloaders. A plugin can:

- Register new **extension schemes** and **reconcilers** (new custom resources + controllers).
- Add **REST endpoints** (`@ApiVersion`-style routes under `/apis/...`).
- Contribute **extension points** and implementations (e.g. storage policies, auth providers, attachment handlers, notifier types).
- Ship a **front-end** (console/UC assets) built with `@halo-dev/ui-plugin-bundler-kit`, registered via `definePlugin` (routes, menu entries, components, `extensionPoints`).
- Declare settings (a `setting.yaml` form) and persist values as a `ConfigMap` extension.

Plugin lifecycle: **install → enable → (configure) → disable → uninstall**, all from **System → Plugins** or the API. Plugins depend on the `:platform:plugin` BOM and the `:api` module so they compile against the host's exact library versions.

> [!NOTE]
> Build plugin front-ends with **Rsbuild** for large/code-split UIs and **Vite** for simpler ones (`@halo-dev/ui-plugin-bundler-kit` supports both). Exclude `@halo-dev/api-client` and `axios` from the bundle — the host provides them.

### 11.2 Themes (Thymeleaf)

Themes render the public site with **Thymeleaf** reactive templates. The theme system provides:

- **Router factories** (`theme/router/factories`) that build dynamic routes for posts, pages, categories, tags, archives, and custom patterns based on permalink settings.
- **Finders** (`theme/finders`) — a safe, read-only data API (posts, categories, comments, menus, site stats) that templates call to fetch content.
- **Template resolution** that supports per-post/page/category template overrides.
- A theme **settings form** (rendered from the theme's definition) and theme-scoped configuration stored as extensions.

A theme package is a `.zip` containing templates, static assets, and a manifest. The default bundled theme is "earth" (`classpath:themes/theme-earth.zip`).

> [!TIP]
> The front-end `@halo-dev/components` (UI), `@halo-dev/ui-shared` (plugin SDK), and `@halo-dev/richtext-editor` packages are published to npm so plugin authors reuse the exact components and editor the core uses. Custom FormKit inputs (`code`, `attachment`, `repeater`, `list`, `select`, `secret`, `postSelect`, `categorySelect`, `tagSelect`, menu selectors, `verificationForm`, …) are available to plugin/theme setting forms.

### 11.3 Front-end module/route generation

Console routes and sidebar menus are generated dynamically from three sources: base routes (`src/router/routes.config.ts`), core module routes (`src/modules/**/module.ts`), and plugin routes — all declared via `definePlugin`. A route's `meta.menu` object (name, group, icon, priority, mobile) places it in the sidebar; `meta.permissions` gates visibility by RBAC permission. Core menu groups: `dashboard`, `content`, `interface`, `system`, `tool`.

---

## 12. Repository Structure (for contributors)

```text
halo/
├── api/                      # Public plugin API (run.halo.app.*), published library
│   └── src/main/java/run/halo/app/{extension,security,core,plugin,theme,...}
├── application/              # Runnable Spring Boot app
│   └── src/main/
│       ├── java/run/halo/app/
│       │   ├── Application.java            # main()
│       │   ├── infra/properties/           # @ConfigurationProperties (halo.*)
│       │   ├── extension/                   # extension store, controllers, indexers
│       │   ├── security/                    # auth (oauth2, twofactor, rememberme, token, pat)
│       │   ├── core/                        # users, attachments, counters, endpoints
│       │   ├── content/                     # posts, pages, comments, permalinks
│       │   ├── theme/                       # router factories, finders, dialect, engine
│       │   ├── plugin/                      # PF4J integration, extension points
│       │   ├── notification/                # notifications + cleanup
│       │   ├── search/                      # Lucene full-text search
│       │   └── migration/                   # backup/restore
│       └── resources/
│           ├── application*.yaml            # config + per-DB profiles
│           ├── db/migration/{h2,mysql,mariadb,postgresql}/
│           ├── schema-*.sql
│           ├── themes/                       # default theme
│           └── config/                       # i18n, uap (ua-parser regexes)
├── platform/{application,plugin}/  # Gradle java-platform BOMs
├── buildSrc/                  # Groovy convention plugins (halo.publish)
├── ui/                        # Vue 3 + TS front-end (pnpm workspace)
│   ├── console-src/           # Console SPA
│   ├── uc-src/                # User Center SPA
│   ├── src/                   # shared (components, formkit, locales, router)
│   └── packages/              # api-client, components, ui-shared, editor, bundler-kit
├── api-docs/openapi/v3_0/     # generated OpenAPI specs
├── docs/                      # developer/extension-point design proposals
├── e2e/                       # api-testing based end-to-end suite
├── Dockerfile                 # layered + CDS image
└── settings.gradle / build.gradle / gradle.properties
```

### Module reference (selected core packages)

| Package | Responsibility |
|---------|----------------|
| `extension` | The custom-resource engine: `Scheme`, `ReactiveExtensionClient`, `Metadata`, `controller` (reconcilers), `index` (query indexers), `router`. |
| `security.authentication` | All auth flows: `login`, `oauth2`, `twofactor/totp`, `rememberme`, `token` (PAT), exceptions. |
| `security.authorization` | RBAC evaluation. |
| `content` | Posts, pages, comments, categories, tags, permalinks, snapshots. |
| `theme` | `router/factories` (dynamic routing), `finders` (template data API), `dialect` (Thymeleaf extensions), `engine`. |
| `plugin` | PF4J plugin manager, lifecycle, extension-point wiring, plugin resources. |
| `infra` | Properties, config, exception handling, web filters, actuator, utilities. |
| `search` | Lucene-backed post search (`search/lucene`, `search/post`). |
| `migration` | Full-site backup and restore. |
| `notification` | In-app/email notifications + scheduled cleanup. |

---

## 13. Testing Strategy

| Layer | Tooling |
|-------|---------|
| Backend unit/slice | JUnit 5 (`spring-boot-starter-test`), Reactor Test (`StepVerifier`), Spring Security Test, WebTestClient. |
| Backend coverage | JaCoCo (XML reports for CI). |
| Front-end unit | Vitest + Vue Test Utils (`pnpm test:unit`). |
| Front-end quality | ESLint (zero warnings), `vue-tsc` type-check, Prettier/spotless formatting. |
| End-to-end | `e2e/` suite driven by LinuxSuRen **api-testing** (`atest`) against a Docker-Compose stack (Halo + DB), runnable for H2/MySQL/PostgreSQL. |
| Java formatting | Spotless with Palantir Java Format; SQL via dbeaver; web resources via Prettier. |

Run the backend tests: `./gradlew test`. Run the full check (lint + typecheck + unit) for the UI: `pnpm --filter ... check` (wired through Gradle `:ui:check`). E2E: `cd e2e && ./start.sh`.

> [!NOTE]
> Add or update e2e cases when you add or change APIs — the `e2e/README` makes this a contribution expectation.

---

## 14. Troubleshooting (Operations)

### Startup fails: `External URL is required when property use-absolute-permalink is set to true.`
`halo.use-absolute-permalink=true` without `halo.external-url`. Set the external URL or disable absolute permalinks. (Validated by `HaloProperties.validate`.)

### Startup fails: `External URL must be a http or https URL.`
`halo.external-url` is not a valid HTTP/HTTPS URL (opaque or missing authority/scheme). Provide a full URL like `https://blog.example.com/`.

### Startup fails: `Public/Private key location does not exist`
JWT signing keys missing at the configured `halo.security.oauth2.jwt.*-key-location`. Use the defaults (Halo manages keys under the work dir) or point to existing RSA key files.

### App starts but database errors / wrong SQL dialect
`spring.sql.init.platform` does not match the engine in `spring.r2dbc.url`. Align them (`postgresql`/`mysql`/`mariadb`/`h2`).

### `/console` 404 or assets missing behind a proxy
The proxy isn't forwarding all paths. Forward `/`, `/console`, `/uc`, `/apis/**`, `/ui-assets/**`. Confirm `X-Forwarded-*` headers are passed (`forward-headers-strategy: native`).

### Data lost on container recreate
No persistent volume. Always mount `-v <host>:/root/.halo2`. The working directory holds the entire site.

### Actuator endpoint reachable from the internet
`exposure.include: "*"`. Block `/actuator/**` at the proxy and/or override exposure to `health,info`; lock down `shutdown`/`heapdump`.

### High memory / OOM
Bound heap via `JVM_OPTS`; check Caffeine `maximumSize`, `max-in-memory-size`, thumbnail concurrency, and large attachment processing. Inspect `logs/halo.log`.

### Slow search or stale lists after import/restore
Reconcilers/indexers are still converging. Allow time; check logs for indexing activity. A restart forces index rebuild.

---

## 15. Quick Reference

### Key files

| File | Purpose |
|------|---------|
| `application/src/main/resources/application.yaml` | Base configuration + defaults. |
| `application/src/main/resources/application-<db>.yaml` | Per-database profiles. |
| `.../infra/properties/HaloProperties.java` | `halo.*` configuration binding (source of truth). |
| `.../Application.java` | Spring Boot entry point. |
| `Dockerfile` | Layered + CDS image build. |
| `gradle.properties` | Project version (`2.24.0-SNAPSHOT`), pinned lib versions. |
| `api-docs/openapi/v3_0/*.json` | Generated OpenAPI specs. |

### Default endpoints

| Path | Purpose |
|------|---------|
| `/` | Public site (theme-rendered). |
| `/console` | Admin SPA. |
| `/uc` | User Center SPA. |
| `/apis/**` | REST API (versioned, grouped). |
| `/actuator/health/{liveness,readiness}` | Probes. |
| `/upload/**` | Legacy (1.x) attachment compatibility mapping. |

### Minimal production startup

```bash
docker run -d --name halo \
  -p 8090:8090 \
  -v /srv/halo:/root/.halo2 \
  -e TZ=UTC \
  -e JVM_OPTS="-Xmx512m -Xms512m" \
  halohub/halo:2.24 \
  --halo.external-url=https://blog.example.com/ \
  --spring.r2dbc.url=r2dbc:pool:postgresql://db:5432/halo \
  --spring.r2dbc.username=halo \
  --spring.r2dbc.password=CHANGE_ME \
  --spring.sql.init.platform=postgresql \
  --halo.security.two-factor-auth.disabled=false
```

### Source / reference

| Resource | Location |
|----------|----------|
| Source code | `github.com/halo-dev/halo` |
| Official docs | `docs.halo.run` |
| Application market | `halo.run/store/apps` |
| Community | `bbs.halo.run` |
| License | GPL-3.0 |

---

_Generated June 2026 from halo-dev/halo at 7bf96ee. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
