# Ghost Admin Guide

This is the operator's and engineer's reference for **Ghost**, the professional publishing platform produced by the Ghost Foundation (MIT licensed). It is written for administrators, DevOps engineers, and architects who deploy, configure, secure, operate, and extend Ghost — and for developers who want an exhaustive, source-based map of how the application is built.

Where the **User Guide** covers *using* a Ghost publication, this guide covers *running and understanding* one: the boot sequence, the configuration system, the data layer, the service architecture, the HTTP/API surface, security controls, operations, and the internal structure of the codebase.

> [!NOTE]
> Ghost is a **pnpm + Nx monorepo** targeting **Node.js `^22.13.1`**, with the package manager pinned to `pnpm@11.4.0`. The backend is a Node/Express application; the admin and reader widgets are a mix of Ember.js and React apps. All facts in this guide are derived from the repository at commit `f283e8e`.

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [Repository & Monorepo Structure](#2-repository--monorepo-structure)
3. [Architecture Overview](#3-architecture-overview)
4. [The Boot Sequence](#4-the-boot-sequence)
5. [Configuration System](#5-configuration-system)
6. [Configuration Reference](#6-configuration-reference)
7. [Data Layer](#7-data-layer)
8. [Service Catalogue](#8-service-catalogue)
9. [HTTP & API Surface](#9-http--api-surface)
10. [Adapters & Extensibility](#10-adapters--extensibility)
11. [Security](#11-security)
12. [Email & Newsletter Pipeline](#12-email--newsletter-pipeline)
13. [Analytics Pipeline (Tinybird)](#13-analytics-pipeline-tinybird)
14. [Deployment](#14-deployment)
15. [Operations](#15-operations)
16. [Development Workflow](#16-development-workflow)
17. [Testing Strategy](#17-testing-strategy)
18. [Code Patterns & Conventions](#18-code-patterns--conventions)
19. [Potential Improvements](#19-potential-improvements)
20. [Quick Reference](#20-quick-reference)

---

## 1. Executive Summary

Ghost is a monolithic-but-modular Node.js application that serves three logical surfaces from one process:

- A **frontend** (the public website) — Handlebars themes, dynamic routing, RSS, sitemaps, members content gating.
- A **backend** (admin + API) — the management UI and the Content/Admin REST APIs.
- A set of **background services** — email sending and analytics, scheduled publishing, mentions/webmentions, milestones, update checks, and federation (ActivityPub).

The application is built around a **layered configuration system** (nconf), a **Knex/Bookshelf data layer** over MySQL (or SQLite for development), and a large catalogue of cohesive **services** that are initialised in a carefully ordered boot sequence. Frontend and reader-facing widgets (Portal, Comments, Search, Signup form, Announcement bar) are independently versioned packages delivered as UMD bundles, served from a CDN in production.

| Attribute | Value |
|-----------|-------|
| Language/runtime | Node.js `^22.13.1`, JavaScript + TypeScript |
| Web framework | Express (multiple mounted apps via vhost) |
| Data layer | Knex query builder + Bookshelf models |
| Production database | MySQL 8 (`mysql2` client) |
| Dev database | SQLite 3 |
| Templating (themes) | Handlebars (via the theme engine + gscan validation) |
| Payments | Stripe |
| Web analytics | Tinybird |
| Package manager | pnpm `11.4.0` (Nx task runner) |
| Default bind | `127.0.0.1:2368` |
| License | MIT |

---

## 2. Repository & Monorepo Structure

The monorepo is organised into a backend core, a family of frontend apps, end-to-end tests, and architecture records.

```text
Ghost/
├── apps/                       # Frontend applications (React/Preact/Ember-adjacent)
│   ├── admin/                  # New React admin shell + Ember bridge
│   ├── admin-x-settings/       # React settings app
│   ├── admin-x-framework/      # Shared admin app framework (API hooks, routing, test utils)
│   ├── admin-x-design-system/  # Shared admin design-system components
│   ├── shade/                  # Newer shared component/design library (Radix + Tailwind)
│   ├── portal/                 # Member signup/account widget (UMD)
│   ├── comments-ui/            # Comments widget (UMD, Tiptap editor)
│   ├── sodo-search/            # Search overlay (UMD, flexsearch)
│   ├── signup-form/            # Embeddable signup form (UMD)
│   ├── announcement-bar/       # Announcement bar widget (UMD)
│   ├── admin-toolbar/          # Front-of-site admin toolbar (UMD, Preact)
│   ├── posts/                  # React posts/members/analytics app
│   ├── stats/                  # React stats app (Tinybird charts)
│   └── activitypub/            # ActivityPub/federation UI
├── ghost/
│   ├── core/                   # The Ghost backend application (the heart)
│   │   ├── index.js            # Process entry point
│   │   └── core/
│   │       ├── boot.js         # The boot sequence
│   │       ├── app.js          # Root Express app (maintenance/site-id middleware)
│   │       ├── bridge.js       # Server → frontend communication
│   │       ├── cli/            # Internal dev CLI (generate-data, repl, timetravel)
│   │       ├── shared/         # config, settings cache, events, labs
│   │       ├── server/         # services, models, data, web, adapters, api
│   │       └── frontend/       # theme engine, routing, rendering, helpers
│   ├── admin/                  # Legacy Ember.js admin application
│   └── i18n/                   # Internationalisation strings & loader
├── e2e/                        # Playwright end-to-end suite (own infra harness)
├── adr/                        # Architecture Decision Records
└── docker/                     # Dev container & service definitions (minio, stripe, analytics, …)
```

> [!NOTE]
> The scope `@tryghost` namespaces internal packages. Within the monorepo, packages reference each other via `workspace:*`, and many third-party dependency versions are centralised through pnpm **catalog** entries (`"catalog:"`).

### Backend core sub-structure (`ghost/core/core`)

| Path | Responsibility |
|------|----------------|
| `shared/config/` | nconf-based configuration loader, defaults, overrides, helpers |
| `shared/settings-cache/` | In-memory cache of editable settings |
| `shared/labs.js` | Feature flags ("labs") |
| `server/boot.js` *(via `core/boot.js`)* | Ordered initialisation of everything |
| `server/models/` | Bookshelf models (72 files in the models module alone) |
| `server/data/` | DB connection, schema, migrations, importer/exporter, seeders, Tinybird datafiles |
| `server/services/` | The service catalogue (members, stripe, email, themes, …) |
| `server/web/` | Express apps: parent, admin, api, members, comments, webmentions |
| `server/api/endpoints/` | API controllers, serializers, validators |
| `server/adapters/` | Pluggable storage, cache, scheduling, SSO, redirects |
| `frontend/` | Theme engine, dynamic routing, rendering, RSS, sitemap, helpers |

---

## 3. Architecture Overview

### High-level request topology

```text
                         ┌─────────────────────────────────────────┐
                         │              Root Express app            │
                         │   (maintenance + site-id middleware)     │
                         └───────────────────┬─────────────────────┘
                                             │  mounted at config subdir
                         ┌───────────────────▼─────────────────────┐
                         │              Parent app (vhost)          │
              ┌──────────┴───────────┐               ┌─────────────┴──────────┐
              │   Backend mount      │               │   Frontend mount       │
              │  (admin URL host)    │               │  (all other hosts)     │
              ├──────────────────────┤               ├────────────────────────┤
              │  Admin app (/ghost)  │               │  Site (themes/render)  │
              │  Admin API           │               │  Members (portal/auth) │
              │  Content API         │               │  Comments, Webmentions │
              └──────────┬───────────┘               │  RSS, Sitemap, Search  │
                         │                           └────────────┬───────────┘
                         └──────────────┬─────────────────────────┘
                                        ▼
                    Services layer (members, stripe, email, themes, url, …)
                                        ▼
                    Data layer (Bookshelf models → Knex → MySQL/SQLite)
```

The root app starts immediately in **maintenance mode** and serves a 503 page until the full Ghost app is mounted (after the database and services are ready). A `vhost` split routes requests to the **backend** (admin + API) when a separate admin host is configured, and to the **frontend** (public site + members) otherwise.

### Frontend vs. backend hosting

Ghost supports serving the admin from a **separate hostname** than the public site. The config helpers compute mount paths:

- If an admin URL with a different host is configured, the backend serves only on that host and the frontend serves on everything else.
- Otherwise both serve on all hosts.

This is what enables setups like `admin.example.com` (backend) + `example.com` (frontend).

### Independently versioned reader widgets

Portal, Comments, Search, Signup form, Announcement bar, and the Admin toolbar are **separate UMD packages** with their own semantic versions, loaded from `cdn.jsdelivr.net` in production. Ghost's config records the CDN URL and pinned version line for each, so they can be upgraded independently of the core app.

> [!IMPORTANT]
> This decoupling is a core architectural principle: the *application*, the *theme*, and the *widgets* version and deploy independently. Upgrading Ghost does not force a theme or widget rewrite, and vice versa.

---

## 4. The Boot Sequence

Boot lives in `ghost/core/core/boot.js` and is deliberately written as one linear, heavily instrumented async function so timings are visible with `DEBUG=ghost:boot* node ghost`. Understanding it is the key to diagnosing startup problems.

```text
Step 0  Load version, config, logging      (fundamentals; nothing works without these)
Step 1  Load Sentry + Prometheus client     (early observability)
Step 2  Start GhostServer with minimal app  (maintenance mode 503s until ready)
Step 3  Initialise database                 (DatabaseStateManager.makeReady → migrations)
Step 4  initCore                            (url-utils, limits, settings, i18n, URL service, jobs)
        initServicesForFrontend             (route settings, redirects, link redirects, themes, offers)
        initFrontend                        (frontend helpers)
        initExpressApps                     (mount backend + frontend via vhost)
        initDynamicRouting                  (routes.yaml → router manager; bridge.init)
        initAppService                      (internal frontend apps, e.g. private-blogging)
        initServices                        (the big service fan-out — see below)
Step 5  Mount full app, disable maintenance (rootApp.disable('maintenance'))
Step 6  notifyServerReady                   ("booted" metric)
Step 7  initBackgroundServices              (fire-and-forget recurring jobs)
```

### `initCore`

Loads, in order: URL utils → **limits** service (booted before settings so limits are available for calculated settings) → **settings** service (and email-settings sync) → **i18n** → **URL service** (precomputes resource URLs unless running in lazy mode) → **job service** and **mentions job service** (background workers, with test-mode variants). Cleanup tasks are registered with the server for graceful shutdown.

### `initServices` — the service fan-out

A large `Promise.all` initialises the bulk of Ghost's services concurrently after a few ordered prerequisites (`emailAddressService.init()`, the scheduler adapter, and `stripe.init()` run first). The concurrently-initialised set includes:

```text
identity-tokens, member-attribution, mentions, staff, members, tiers,
tags-public, posts-public, members-events, permissions, indexnow, slack,
audience-feedback, email-service, email-analytics, webhooks, comments,
link-tracking, email-suppression-list, slack-notifications, media-inliner,
donations, recommendations, stats, explore-ping, gifts, automations
```

After services are up, if the scheduler adapter supports it, all scheduled posts are rescheduled (`postScheduling.rescheduleAll()`).

### `initBackgroundServices`

Fire-and-forget (boot does **not** await it): loads inactive themes, then — unless under test — starts **ActivityPub**, schedules **email-analytics** recurring jobs (gated by `backgroundJobs.emailAnalytics`), the **update-check** job, the **milestones** service, and the (deprecated) **outbox** service.

> [!TIP]
> When boot is slow, set `DEBUG=ghost:boot*` to see per-step timings. The `theme-service-init` and `boot-time` metrics are emitted explicitly; the URL service building its cache is a common contributor to first-boot latency on large sites.

> [!NOTE]
> The `GHOST_CI_SHUTDOWN_AFTER_BOOT` environment variable makes the process exit immediately after a successful boot — used in CI to verify the app boots cleanly without keeping it running.

### The bridge

`bridge.js` is the sanctioned place for server→frontend communication. It listens for setting changes (`settings.locale.edited`, `settings.labs.edited`, `settings.timezone.edited`) and reacts — re-initialising theme i18n, rebuilding card/admin-auth assets on theme activation, and triggering frontend reloads. Data flows **only** server → frontend.

---

## 5. Configuration System

Ghost configuration is built on **nconf** and assembled in `ghost/core/core/shared/config/loader.js`. Values are resolved from multiple sources with a strict precedence (highest wins):

```text
1. overrides.json            (baked-in; nothing can override these)
2. command-line arguments    (nconf.argv)
3. environment variables     (separator "__", values parsed)
4. config.{env}.json         (custom env file in CWD)
5. config.development.docker.json   (only when GHOST_DEV_IS_DOCKER=true)
6. config.local.json / config.local.jsonc   (skipped in testing envs)
7. config.{env}.json         (default env file in config dir)
8. defaults.json             (the baseline — applied last)
```

Where `{env}` is `process.env.NODE_ENV` (default `development`).

### Environment-variable mapping

Nested keys map to environment variables with a **double-underscore** separator. For example:

```bash
database__client=mysql2
database__connection__host=127.0.0.1
database__connection__user=ghost
database__connection__password=secret
database__connection__database=ghost_prod
server__port=2368
url=https://example.com
logging__level=info
```

> [!IMPORTANT]
> Values from `nconf.env({separator: '__', parseValues: true})` are **parsed** — so `"true"`/`"false"`/numbers become real booleans/numbers. This is the recommended way to inject secrets and host-specific settings in containerised deployments.

### Startup sanitisation & validation

After loading, the loader performs important fix-ups and guards:

- **Relative → absolute paths.** All `paths.*` (and sqlite `database.connection.filename`) are normalised to absolute paths relative to the core directory.
- **Database client normalisation.** A `database.client` of `mysql` is rewritten to `mysql2`. Irrelevant connection keys are stripped depending on client (e.g. `filename` removed for MySQL; host/user/password/database removed for SQLite).
- **URL protocol check.** The `url` must begin with `http://` or `https://`, or boot throws: *"URL in config must be provided with protocol…"*.
- **Content path existence.** `paths.contentPath` must exist, or boot throws: *"Your content path does not exist!…"*.

### Config helpers

The loader binds helper methods onto the nconf instance, including:

- `getContentPath(type)` — resolves `images`/`media`/`files`/`themes`/`adapters`/`logs`/`data`/`settings`/`public` sub-paths under `contentPath`.
- `isPrivacyDisabled(flag)` — evaluates the `privacy` block (including the `useTinfoil` "disable everything" mode with per-feature opt-ins).
- `getBackendMountPath()` / `getFrontendMountPath()` — compute vhost mount paths from the configured site/admin URLs.

> [!TIP]
> Prefer a `config.production.json` (or environment variables) for production settings. `config.local.json`/`.jsonc` is for developer overrides and is **skipped** in testing environments. JSONC (`config.local.jsonc`) is supported, so you can comment your local config.

---

## 6. Configuration Reference

The following are the shipped defaults from `shared/config/defaults.json` and key baked-in `overrides.json` values. Override any of them via the layered config sources above. Values are exact as found in source.

### Server, URL & admin

| Key | Default | Notes |
|-----|---------|-------|
| `url` | `http://localhost:2368` | Public site URL; must include protocol |
| `server.host` | `127.0.0.1` | Bind address |
| `server.port` | `2368` | Bind port |
| `server.shutdownTimeout` | `60000` | Graceful shutdown timeout (ms) |
| `admin.redirects` | `true` | Redirect to canonical admin URL |
| `adminFrameProtection` | `true` | Prevents admin being framed |
| `maintenance.enabled` | `false` | Force maintenance mode (overrides) |

### Database (set per-install; not in defaults)

Configured via `database.client` (`mysql2` or `sqlite3`) and `database.connection`. Production uses MySQL 8; development can use SQLite. The loader normalises `mysql` → `mysql2` automatically.

### Storage, scheduling & adapters

| Key | Default | Notes |
|-----|---------|-------|
| `storage.active` | `LocalImagesStorage` | Image storage adapter |
| `storage.media` | `LocalMediaStorage` | Media storage adapter |
| `storage.files` | `LocalFilesStorage` | File storage adapter |
| `scheduling.active` | `scheduling-default` | In-process JS scheduler |
| `adapters.cache.active` | `MemoryCache` | Cache adapter |
| `adapters.sso.active` | `DefaultSSOAdapter` | SSO adapter |
| `adapters.redirects.active` | `FileStore` | Redirects store (`FileStore` or `S3RedirectsStore`) |

### Logging

| Key | Default |
|-----|---------|
| `logging.level` | `info` |
| `logging.transports` | `["stdout"]` |
| `logging.rotation.enabled` | `false` |
| `logging.rotation.period` | `1d` |
| `logging.rotation.count` | `10` |
| `logging.useLocalTime` | `false` |

### Caching (selected `maxAge`, seconds)

| Resource | `maxAge` |
|----------|----------|
| `301` redirects | `31536000` (1 year) |
| Public assets | `31536000` |
| Admin assets | `31536000` |
| Theme | `31536000` |
| Favicon | `86400` |
| Sitemap | `3600` |
| `llms` | `3600` |
| `robotstxt` | `3600` |
| Content API | `0` |
| Frontend | `0` |

### Members, payments & email

| Key | Default | Meaning |
|-----|---------|---------|
| `sendWelcomeEmail` | `true` | Email new members on signup |
| `stripeDirect` | `false` | Use direct Stripe keys vs. Connect |
| `enableStripePromoCodes` | `false` | Allow promo codes at checkout |
| `enableTipsAndDonations` | `true` | One-time tips/donations |
| `bulkEmail.batchSize` | `1000` | Newsletter recipients per batch |
| `emailAnalytics.enabled` | `true` | Open/click analytics |
| `emailAnalytics.batchProcessing` | `false` | Batch analytics processing |
| `emailAnalytics.openedJobLagWarningMinutes` | `30` | Warn if analytics lag exceeds |
| `backgroundJobs.emailAnalytics` | `true` | Schedule email-analytics jobs on boot |
| `backgroundJobs.clickTrackingLastSeenAtUpdater` | `true` | Update member last-seen on click |
| `linkClickTrackingCacheMemberUuid` | `false` | Cache member UUID for click tracking |

### Image optimization (overrides)

| Key | Default |
|-----|---------|
| `imageOptimization.resize` | `true` |
| `imageOptimization.srcsets` | `true` |
| `imageOptimization.defaultMaxWidth` | `2000` |
| Content image sizes | `600 / 1000 / 1600 / 2400` px |

Internal image sizes are defined for icons, signup-form icons, and several email image formats (header, latest-posts thumbnails, social, CTA).

### Theme upload limits

| Key | Default (bytes) | Human |
|-----|-----------------|-------|
| `theme.uploadLimits.compressedBytes` | `1073741824` | 1 GB |
| `theme.uploadLimits.entryUncompressedBytes` | `536870912` | 512 MB |
| `theme.uploadLimits.totalUncompressedBytes` | `4294967296` | 4 GB |

### Optimization & limits

| Key | Default |
|-----|---------|
| `optimization.maxLimit` | `100` (max API page size) |
| `optimization.allowLimitAll` | `false` |
| `optimization.getHelper.timeout.threshold` | `5000` ms |
| `optimization.themes.skipBootChecks` | `false` |

### Security defaults

| Key | Default |
|-----|---------|
| `security.allowWebhookInternalIPs` | `false` |
| `security.staffDeviceVerification` | `true` |
| `privacy` | `false` (privacy features off by default) |

### Update check

| Key | Default |
|-----|---------|
| `updateCheck.url` | `https://updates.ghost.org` |
| `updateCheck.forceUpdate` | `false` |

### Widget CDN versions (defaults)

| Widget | URL template version |
|--------|----------------------|
| `portal.version` | `2.68` |
| `sodoSearch.version` | `1.8` |
| `announcementBar.version` | `1.1` |
| `comments.version` | `1.5` |
| `signupForm.version` | `0.3` |
| `adminToolbar.version` | `0.1` |

### Baked-in overrides (cannot be changed by user config)

| Key | Value |
|-----|-------|
| `paths.corePath` | `core/` |
| `paths.migrationPath` | `core/server/data/migrations` |
| `apps.internal` | `["private-blogging"]` |
| `slugs.protected` | `["ghost", "rss", "amp"]` |
| `uploads.members.extensions` | `[".csv"]` |
| `uploads.themes.extensions` | `[".zip"]` |
| `times.publishAPostBySchedulerToleranceInMinutes` | `5` |
| `times.cannotScheduleAPostBeforeInMinutes` | `-2` |

---

## 7. Data Layer

### Knex + Bookshelf

Ghost uses **Knex** as its query builder and connection pool, and **Bookshelf** as its model/ORM layer on top. The `server/data/db/connection` module exposes the configured Knex instance; CLI tools and the data generator obtain it directly.

The models module (`server/models/`) is large — the single highest-priority models directory contains **72 model files** — covering posts, pages, users, roles, permissions, members, subscriptions, tiers, newsletters, tags, labels, redirects, webhooks, settings, and more. A `base/` model with plugins provides shared behaviour (events, serialisation, filtering).

> [!NOTE]
> The `NUMERIC_IDS` environment variable (referenced in `models/base/plugins/events.js`) toggles numeric-vs-ObjectId behaviour in the events plugin. Most installs never touch it.

### Schema & migrations

- **Schema** is defined in `server/data/schema/` with default settings under `default-settings/`. The schema enumerates every table and column (including `dateTime` columns the `timetravel` CLI shifts).
- **Migrations** live in `server/data/migrations/versions/{x.y}/` — hundreds of versioned migration folders spanning Ghost 2.x → 6.x. The `DatabaseStateManager` runs them to bring the DB to a ready state during boot Step 3.
- Migrations are applied by **knex-migrator**; the repo exposes `pnpm knex-migrator` (filtered to the `ghost` package).

> [!IMPORTANT]
> Migrations run automatically on boot via `DatabaseStateManager.makeReady()`. On production, an upgrade therefore applies pending migrations at startup. Always back up the database before upgrading a major version, and expect first boot after an upgrade to take longer.

### Importer / Exporter

- **Exporter** (`server/data/exporter/`) produces the canonical JSON export of content + settings.
- **Importer** (`server/data/importer/`) ingests Ghost JSON exports and member CSVs, with dedicated handlers for data and images. Member imports record attribution as `Imported` / `Member Importer`.

### Seeders & data generation

The `server/data/seeders/` directory and the `generate-data` CLI build realistic datasets for development. Quantities and tables are configurable; a `--seed` makes runs reproducible.

### Settings cache

Editable settings (site title, locale, timezone, labs flags, membership config) are cached in memory (`shared/settings-cache`) and kept fresh via events. Theme i18n, routing, and timezone behaviour react to settings changes through the bridge.

---

## 8. Service Catalogue

Services live under `server/services/` and are the unit of feature cohesion. The table below maps the major services (initialised in the boot fan-out and background phase) to their responsibilities.

| Service | Responsibility |
|---------|----------------|
| `settings` | Editable key/value settings, calculated settings, email-settings sync |
| `members` | Member accounts, signin (magic links), membership state |
| `members-api` (within members) | Repositories, controllers, services for the member domain |
| `stripe` | Stripe API wrapper + webhook registration/handling |
| `tiers` | Subscription products (free/paid tiers) |
| `offers` | Discount offers on tiers |
| `donations` / `gifts` | One-time tips and gift subscriptions |
| `email-service` | Renders posts to email, batches & sends newsletters |
| `email-analytics` | Ingests opens/clicks; recurring background jobs |
| `email-suppression-list` | Bounces/complaints suppression |
| `email-address` | Sender address validation/management |
| `newsletters` | Multiple newsletter definitions & subscriptions |
| `member-attribution` | Tracks signup source/referrer/UTM (Last Post Algorithm) |
| `members-events` | Member domain events |
| `link-tracking` / `link-redirection` | Click-tracking redirects (`/r/{hash}`) and analytics |
| `comments` | Member commenting |
| `mentions` / `mentions-jobs` | Webmentions (send/receive) and their jobs |
| `recommendations` | Site-to-site recommendations |
| `stats` | Member/MRR/email statistics aggregation |
| `tinybird` | Web-analytics integration |
| `themes` | Theme load/validate (gscan)/activate, inactive-theme loading |
| `route-settings` | `routes.yaml` parsing and reload |
| `custom-redirects` | User-defined URL redirects |
| `url` | Canonical URL computation for every resource (eager or lazy) |
| `permissions` | Role/permission model and checks |
| `staff` | Staff (user) lifecycle, invites, emails |
| `auth` | Sessions, API keys, member auth, SSO |
| `identity-tokens` | Signed identity tokens |
| `webhooks` | Outbound webhook registration & delivery |
| `slack` / `slack-notifications` | Slack publish notifications |
| `automations` | Event-driven automations (with scheduler + internal keys) |
| `post-scheduling` | Scheduled publish via the scheduling adapter |
| `media-inliner` | Inlines external media into content |
| `milestones` | Member/ARR milestone detection & notification |
| `update-check` | Phones `updates.ghost.org` for available updates |
| `explore` / `explore-ping` | Ghost Explore directory integration |
| `activitypub` | Fediverse federation (background) |
| `audience-feedback` | Post sentiment/feedback |
| `outbox` | **Deprecated** — slated for removal (see source TODO `NY-1220`) |
| `i18n` | Internationalisation |
| `limits` | Plan/host limits (booted before settings) |
| `jobs` / `mentions-jobs` | Background job queues (with test-mode variants) |

> [!NOTE]
> The **Stripe service** wraps the Stripe API and a webhook manager/controller. It is designed for **idempotent, order-independent** webhook handling because Stripe events can arrive out of order or in parallel (`customer.subscription.*`, `invoice.payment_succeeded`, `checkout.session.completed`).

---

## 9. HTTP & API Surface

### Express app composition

The web layer (`server/web/`) is composed of multiple Express apps mounted under a `parent` app:

- **parent/backend** — admin UI + Admin API + Content API (served on the admin host).
- **parent/frontend** — the public site, members endpoints, comments, webmentions, gift previews, announcement.
- **shared middleware** — request IDs, locals, API middleware, brute-force protection, caching.

The root app (`app.js`) adds Sentry request/tracing handlers, an optional **site-id** guard (used by Ghost(Pro) to prevent cross-site request bleed via the `x-site-id` header vs. `hostSettings.siteId`), and the **maintenance** middleware.

### The two REST APIs

| API | Base path | Auth | Audience |
|-----|-----------|------|----------|
| **Content API** | `/ghost/api/content/` | Content API key (read-only) | Public consumers, headless frontends |
| **Admin API** | `/ghost/api/admin/` | JWT (Admin API key) or staff session cookie | Tooling, automation, admin UI |

API endpoints are implemented in `server/api/endpoints/` with a clean separation of **controllers**, **input/output serializers**, and **input/output validators** (`endpoints/utils/serializers/{input,output}`, `endpoints/utils/validators/{input,output}`). API versioning compatibility is handled by the `api-version-compatibility` service.

> [!NOTE]
> AST-based extraction over this JS/TS monorepo surfaces few endpoints automatically because routes are assembled dynamically through Express routers and middleware factories rather than static decorators. The authoritative endpoint list is the Ghost API documentation; the *structure* (controllers → serializers → validators) is consistent across every resource.

### Frontend routing

The public site uses **dynamic routing** driven by `routes.yaml` (parsed by `route-settings`, applied by the routing service's `routerManager`). This controls collections, channels, taxonomies, and URL patterns. The URL service precomputes canonical URLs for every resource (unless `lazyRouting` is enabled).

---

## 10. Adapters & Extensibility

Ghost's adapter pattern lets operators replace default subsystems without forking. Adapters are configured under `adapters.*` / `storage.*` / `scheduling.*` and discovered from `content/adapters/{type}/`.

| Adapter type | Default | Replace for |
|--------------|---------|-------------|
| **Storage (images/media/files)** | `LocalImagesStorage` / `LocalMediaStorage` / `LocalFilesStorage` | S3/GCS/object storage |
| **Cache** | `MemoryCache` | Redis-backed cache |
| **Scheduling** | `scheduling-default` (in-process JS) | External scheduler |
| **SSO** | `DefaultSSOAdapter` | Enterprise SSO |
| **Redirects store** | `FileStore` | `S3RedirectsStore` |

> [!TIP]
> To override a default adapter, add a folder under `content/adapters/storage` or `content/adapters/scheduling` and place your adapter there, then point the relevant `*.active` config key at it. The default `LocalFileStorage` writes to `content/images`; the default scheduler is pure JavaScript and needs no external `cron`.

### Feature flags (labs)

`shared/labs.js` exposes feature flags toggled in **Settings → Labs**. Some flags participate in environment identity for tests (e.g. `themeTranslation`, which the bridge watches to re-init theme i18n).

### Internal frontend apps

`apps.internal` is fixed to `["private-blogging"]` — Ghost's built-in app that password-protects an entire site. App routing is wired during `initAppService`.

---

## 11. Security

### Authentication surfaces

| Surface | Mechanism |
|---------|-----------|
| Staff (admin) | Session cookies; new-device email verification |
| Admin API | JWT signed with an Admin API key |
| Content API | Read-only Content API key |
| Members | Passwordless magic-link sign-in; member sessions |
| SSO | Pluggable SSO adapter |

### Brute-force / spam protection

Ghost ships extensive rate-limiting (`spam` config block) keyed by action. Each entry defines `freeRetries`, `minWait`, `maxWait`, and `lifetime`. Selected defaults:

| Action | freeRetries | minWait | maxWait |
|--------|-------------|---------|---------|
| `user_login` | 4 | 10 min | 7 days |
| `user_reset` | 4 | 1 hour | 1 hour |
| `member_login` | 8 | 10 min | 12 hours |
| `content_api_key` | 99 | 1 hour | 24 hours |
| `global_block` / `private_block` | 99 | 1 hour | 1 hour |
| `send_verification_code` | 4 | 1 hour | 1 hour |
| `otc_verification` | 4 | 1 hour | 1 hour |

> [!IMPORTANT]
> `security.staffDeviceVerification` is **on by default** — staff signing in from a new device may be challenged via email. Keep it enabled in production.

### Webhook SSRF protection

`security.allowWebhookInternalIPs` defaults to `false`, blocking webhooks from targeting internal/private IP ranges — an SSRF mitigation. Only enable it in trusted, isolated networks.

### Frame & site protections

- `adminFrameProtection` (default `true`) blocks the admin from being embedded in frames.
- The optional **site-id middleware** rejects requests whose `x-site-id` header doesn't match `hostSettings.siteId`, preventing one site's requests from being served by another (multi-tenant safety).

### Upload validation

Every upload type has an allow-list of extensions **and** content types (`overrides.json → uploads`). Images, media, files, themes (`.zip` only), members (`.csv` only), redirects, and routes each have explicit, restrictive lists. This is the first line of defence against malicious upload content.

### Secrets & transport

- Database credentials, Stripe keys, and mail credentials are supplied via config/env and should be injected as environment variables in production.
- The `url` must be HTTPS in production (enforced to include a protocol; use `https://`).
- Sentry DSN and Prometheus are optional and config-gated.

> [!CAUTION]
> Never commit `config.production.json` with live secrets to source control. Prefer `KEY__SUBKEY` environment variables (double-underscore separator) so secrets stay out of files.

---

## 12. Email & Newsletter Pipeline

### Two email channels

| Channel | Used for | Notes |
|---------|----------|-------|
| **Transactional** | Magic links, staff invites, welcome emails | Configured mail transport (e.g. SMTP) |
| **Bulk** | Newsletters | High-volume provider (commonly Mailgun); batched |

### Send flow

1. A post is published with **Publish & email** or **Email only**.
2. `email-service` renders the post into email-safe HTML (the "EmailRenderer").
3. Recipients are resolved from the newsletter's subscriber list and segmented.
4. Recipients are processed in batches of `bulkEmail.batchSize` (**1,000** by default).
5. When click tracking is on, links are rewritten to `/{site}/r/{hash}?m={member_uuid}`.

### Click & open analytics

The link-redirection + link-tracking services implement the redirect flow:

```text
Member clicks /r/{hash}?m={uuid}
  → LinkRedirectsService looks up the redirect, emits RedirectEvent, responds 302
  → LinkClickTrackingService records a members_click_events row
  → emits MemberLinkClickEvent → LastSeenAtUpdater updates member.last_seen_at (once/day)
  → member.edited webhook fires with labels + newsletters included
```

`email-analytics` ingests opens/clicks (recurring jobs scheduled on boot when `backgroundJobs.emailAnalytics` is true) and surfaces them in stats. `email-suppression-list` handles bounces/complaints.

> [!NOTE]
> A **"golden post"** fixture (`test/utils/fixtures/email-service/golden-post.json`) contains one of every editor card and is used in tests to catch email-rendering regressions across clients (especially Outlook). When a new card type is added, the golden post must be updated.

---

## 13. Analytics Pipeline (Tinybird)

Ghost's web analytics are implemented on **Tinybird Forward**. The datafiles live in `server/data/tinybird/` (datasources, pipes, endpoints, fixtures, scripts).

### Session model

Hits are aggregated into sessions. The pipeline distinguishes:

- **Session-level attributes** (`source`, `utm_source/medium/campaign/term/content`) — captured from the **first** hit via `argMinState(field, timestamp)` in the `_mv_session_data` materialised view (an `AggregatingMergeTree`). Constant for the whole session.
- **Hit-level attributes** (`pathname`, `post_uuid`, `member_status`) — can vary across pageviews in a session.

### Filtering architecture

`filtered_sessions.pipe` implements a multi-stage filter:

1. **Query filters (hit-level)** — sessions where *any* hit matches `pathname`/`post_uuid`/`member_status`.
2. **Session-attribute filters (session-level)** — `-Merge` combinators (e.g. `argMinMerge(source)`) over `_mv_session_data` for `source`/`utm_*`.
3. **Final output** — session IDs matching *all* criteria.

Endpoints join `_mv_hits` with `filtered_sessions`, returning **all** hits from matching sessions — answering *"what pages did users visit when they arrived from X?"* rather than *"which hits had X in the URL?"*.

### Running analytics in development

```bash
# Recommended: Docker
docker compose --profile analytics up -d   # start Tinybird local + analytics service
docker compose --profile split up          # start Ghost wired to local analytics

# Or the pnpm flow
pnpm dev:analytics
pnpm data:analytics:generate 5000          # seed 5,000 analytics events
pnpm data:analytics:clear
```

Configuration is supplied under a `tinybird` config block (workspace ID, admin token, tracker endpoint, stats endpoint, optional local override).

> [!NOTE]
> The Docker analytics manager connects to MySQL at `localhost:3306` (overridable via `MYSQL_HOST/PORT/USER/PASSWORD/DATABASE`) and reads Tinybird tokens from the Docker volume (overridable via `TINYBIRD_ADMIN_TOKEN`, `TINYBIRD_TRACKER_TOKEN`, `TINYBIRD_HOST`).

---

## 14. Deployment

### Production install (Ghost-CLI)

Self-hosted production deployments are managed by **Ghost-CLI** (a separate package), not this repository's tooling:

```bash
npm install ghost-cli@latest -g
mkdir /var/www/ghost && cd /var/www/ghost
ghost install            # provisions app, MySQL, systemd, nginx, SSL
ghost start
```

Ghost-CLI configures a MySQL database, a systemd service, an nginx reverse proxy, and Let's Encrypt SSL. Lifecycle: `ghost start|stop|restart|ls|update|doctor`.

### Production requirements

- [ ] **Node.js `^22.13.1`**
- [ ] **MySQL 8** with a dedicated database & user
- [ ] A reverse proxy terminating TLS (Ghost binds `127.0.0.1:2368` by default)
- [ ] Persistent `content/` directory (images, media, files, themes, logs)
- [ ] SMTP transport for transactional mail; a bulk provider (e.g. Mailgun) for newsletters
- [ ] Outbound access to `updates.ghost.org` (update check) and your CDN for widgets
- [ ] Optional: Stripe account, Tinybird workspace, Redis cache adapter, object storage adapter

### Container deployment notes

- Inject config via `KEY__SUBKEY` environment variables (double-underscore separator, values parsed).
- Mount a persistent volume for `content/`.
- Ensure `url` is the public HTTPS address and `paths.contentPath` exists in the image/volume.
- The dev `compose.dev.yaml` profiles (`analytics`, `split`, storage, stripe) are for development, not production.

### Upgrades

```bash
# Ghost-CLI install
ghost update                # pulls latest, runs migrations on restart

# Development repo
pnpm main                   # checkout latest main + reinstall
```

> [!WARNING]
> Database migrations run automatically on boot (`DatabaseStateManager.makeReady()`). For major-version upgrades: back up the database and `content/` first, expect a longer first boot (migrations + URL cache rebuild), and verify boot completes (watch for the `booted` log line) before sending traffic.

> [!TIP]
> A blue/green or maintenance-window upgrade is safest. The root app serves a 503 maintenance page until the full app mounts, so brief unavailability during boot is expected and graceful.

---

## 15. Operations

### Health, readiness & maintenance

- The server starts in **maintenance mode** and returns **503** until services are ready, then disables maintenance.
- `maintenance.enabled` in config forces maintenance mode for planned downtime.
- The URL service finishing its cache build gates readiness on large sites.

### Logging

- Logs go to `stdout` by default at `info` level; production file logs land in `content/logs/`.
- Optional rotation (`logging.rotation`) — disabled by default, `1d` period, keep `10`.
- Set `DEBUG=ghost:boot*` for boot timings; `DEBUG=ghost:*` for verbose internals.

### Background jobs

| Job | Gate | Cadence |
|-----|------|---------|
| Email analytics | `backgroundJobs.emailAnalytics` | Recurring (scheduled on boot) |
| Click-tracking last-seen updater | `backgroundJobs.clickTrackingLastSeenAtUpdater` | On click events |
| Update check | always (non-test) | Recurring |
| Milestones | always (non-test) | On boot + run |
| ActivityPub | always (non-test) | On boot |
| Mentions | mentions-jobs | Recurring |
| Post scheduling | scheduler adapter | At scheduled times |

> [!NOTE]
> Background services are **skipped under test** (`NODE_ENV` starting with `test`) and have test-mode variants (`jobService.initTestMode()`), so they don't interfere with the suite.

### Backups & DR

A complete backup = **database** + **`content/` directory**. For SQLite installs the database file lives in `content/data/` (do not overwrite it manually). For MySQL installs, back up the MySQL server and the `content/` volume separately.

### Observability hooks

- **Sentry** — request handler, tracing handler (config-gated), and query tracing wired in boot.
- **Prometheus** — `prometheus.enabled` starts the client early and instruments Knex/connection pool.
- **Metrics** — boot emits `theme-service-init` and `boot-time` metrics.

### Scaling

- The `bulkEmail.batchSize` (1,000) and email-analytics batching tune newsletter throughput.
- The `optimization.maxLimit` (100) caps API page sizes; `allowLimitAll` is off by default.
- Swap `MemoryCache` for a Redis cache adapter and local storage for object storage to scale horizontally.
- The repo ships a 2M-member data generator (`pnpm reset:data:xxl`) for load testing.

---

## 16. Development Workflow

### Initial setup

```bash
git clone --recurse-submodules https://github.com/TryGhost/Ghost.git
cd Ghost
corepack enable pnpm
pnpm run setup       # install deps + init submodules
pnpm dev             # Docker backend + frontend dev servers
```

### Everyday commands

| Command | Purpose |
|---------|---------|
| `pnpm dev` | Full dev stack (Nx orchestrated) |
| `pnpm dev:sqlite` | Dev with SQLite |
| `pnpm dev:stripe` | Dev with local Stripe listener |
| `pnpm dev:analytics` | Dev with Tinybird analytics |
| `pnpm build` | Build all packages (`nx run-many -t build`) |
| `pnpm lint` | Lint all packages |
| `pnpm test` | Run tests (excludes e2e + admin) |
| `pnpm test:unit` | Unit tests across packages |
| `pnpm knip` | Dead-code / unused-dependency analysis |
| `pnpm fix` | Prune + reinstall to fix dependency issues |
| `pnpm main` | Update to latest `main` (+ submodules) |
| `pnpm knex-migrator` | Run DB migrations (ghost package) |

### Internal CLI (`node index.js`)

| Command | Purpose | Permitted environments |
|---------|---------|------------------------|
| `generate-data` | Seed members/posts/etc. | all (development, local, staging, production) |
| `repl` | Interactive shell with `knex`/`models` | development, local |
| `timetravel` | Shift all DB dates to present | (destructive; testing only) |

> [!CAUTION]
> `repl` (and most internal commands) refuse to run when `NODE_ENV=production` — they check `permittedEnvironments()` and abort with *"Command … is not permitted in {env}"*. `generate-data` is the exception (permitted everywhere) but is destructive with `--clear-database`.

### Nx & dependency graph

Nx orchestrates builds/tests with explicit `dependsOn` chains (e.g. apps depend on `^build` of shared libraries like `@tryghost/shade`, `admin-x-design-system`, `admin-x-framework`). The package manager is enforced via a `preinstall` hook (`enforce-package-manager.js`).

---

## 17. Testing Strategy

Ghost has an extensive, multi-tier test suite (the briefing is dominated by test directories — a strong signal of test investment).

| Tier | Location | Tooling |
|------|----------|---------|
| **Unit** | `ghost/core/test/unit/`, per-app `test/unit/` | Mocha (core), Vitest (apps) |
| **Integration** | `ghost/core/test/integration/` | Mocha + real DB |
| **E2E API** | `ghost/core/test/e2e-api/`, `e2e-server/`, `e2e-webhooks/` | Mocha + booted Ghost |
| **E2E browser** | `e2e/` | Playwright (own Docker infra harness) |
| **Legacy** | `ghost/core/test/legacy/` | Older Mocha specs |
| **Visual regression** | `e2e/visual-regression/` | Playwright snapshots |

### App testing utilities

`@tryghost/admin-x-framework` provides a shared **Vitest config factory**, **MSW** (Mock Service Worker) server with realistic Ghost API fixtures, `renderWithProviders`/`renderHookWithProviders`, test data factories (`post()`, `member()`, `user()`, `newsletter()`), timer mocking, and console filtering. Apps are encouraged to use `@src`/`@test` path aliases and the centralised config.

### E2E harness

The Playwright suite (`e2e/`) boots real Ghost against MySQL/Redis/Mailpit/Tinybird via Docker. It supports **dev mode** (mounts source, proxies assets to host dev servers) and **build mode** (prebuilt image). Isolation is configurable per file or per test; `config`/`labs` participate in per-file environment identity, while `stripeEnabled` forces per-test isolation. Tests use the **Page Object Model** and `data-testid` selectors.

```bash
# From e2e/
pnpm infra:up        # start MySQL/Redis/Mailpit/Tinybird
pnpm test            # run main project
PRESERVE_ENV=true pnpm test   # keep containers for debugging
```

> [!NOTE]
> Retry-sensitive tests exist; the suite uses a mocha retry reporter and CI uploads Playwright traces on failure. CI runs unit/integration on every PR and runs the Playwright suite inside the official Playwright container.

---

## 18. Code Patterns & Conventions

- **Boot-as-narrative.** `boot.js` is intentionally one linear file with dense `debug()` statements so the startup sequence reads top-to-bottom and timings are observable. Don't scatter boot logic.
- **Service modules with `init()`.** Most services export an object with an idempotent `init()`; the boot fan-out `Promise.all`s them after ordered prerequisites.
- **The bridge for cross-layer events.** Server→frontend communication is funneled through `bridge.js`; it is the sanctioned place to require across `shared`/`server`/`frontend`.
- **Adapter pattern everywhere.** Storage, cache, scheduling, SSO, and redirects are all swappable via config + `content/adapters/`.
- **Config helpers bound on nconf.** Path resolution, privacy checks, and mount-path computation are methods on the config object, not free functions.
- **Serializer/validator separation.** API endpoints split input/output serialization and validation into discrete `utils/{serializers,validators}/{input,output}` trees.
- **Centralised dependency versions.** pnpm catalogs (`"catalog:"`) pin shared dependency versions across the monorepo; React 17 vs 18 are tracked via separate catalog channels (`catalog:react17`).
- **Filename linting.** A custom `eslint-plugin-ghost` enforces conventions (e.g. `filenames/match-regex` `^[a-z0-9.-]+$`).
- **Events plugin on models.** Bookshelf models emit domain events via a base plugin; many services subscribe rather than calling each other directly (event-driven decoupling).
- **TypeScript adoption is incremental.** Newer apps are TS-first (strict mode, `erasableSyntaxOnly`); the backend core is largely JS with growing `.ts`/`.d.ts` coverage.

> [!TIP]
> When adding a feature to the backend, the idiomatic path is: a **model** (if it needs persistence) → a **service** with `init()` (wired into the boot fan-out) → **API endpoint** controllers/serializers/validators → optional **webhook**/**event** emission. Mirror an existing service (e.g. `recommendations`) as a template.

---

## 19. Potential Improvements

These observations are drawn from signals visible in the source at this commit. They are suggestions for maintainers and operators, not defects.

- **Outbox deprecation.** The `outbox` service carries a source TODO (`NY-1220`) marking it deprecated and slated for removal. Operators should avoid building on it; maintainers can plan its removal.
- **API route discoverability.** Endpoints are assembled dynamically, which makes static analysis (and onboarding) harder. A generated, machine-readable route manifest would aid tooling, docs, and the kind of automated extraction this guide relied on.
- **Privacy defaults.** `privacy` is `false` by default and `security.allowWebhookInternalIPs` is `false` (good), but operators in regulated environments should review the full `privacy` block (including `useTinfoil`) and document their chosen posture.
- **Email-rendering fragility.** The "golden post" mechanism exists precisely because email-client rendering (Outlook) is brittle. Teams adding editor cards must keep the golden post current or risk silent newsletter regressions.
- **First-boot latency on large sites.** The eager URL service cache build dominates first-boot time. Sites with very large content sets should evaluate `lazyRouting` (`lazyRouting: false` by default) and measure the trade-off.
- **Config sprawl.** The layered config + many feature flags ("labs") give great flexibility but a large surface area; a single generated "effective config" dump (beyond `DEBUG=ghost-config`) would help operators audit production settings.
- **Monorepo build coupling.** Heavy `dependsOn: ['^build']` chains mean a clean build rebuilds many shared libraries; Nx caching mitigates this, but CI cold-starts are costly. Keep the Nx cache warm.

---

## 20. Quick Reference

### Key files

| File | Purpose |
|------|---------|
| `ghost/core/index.js` | Process entry point |
| `ghost/core/core/boot.js` | Boot sequence |
| `ghost/core/core/app.js` | Root Express app (maintenance/site-id) |
| `ghost/core/core/bridge.js` | Server→frontend events |
| `ghost/core/core/shared/config/loader.js` | Config assembly |
| `ghost/core/core/shared/config/defaults.json` | Default configuration |
| `ghost/core/core/shared/config/overrides.json` | Immutable overrides |
| `ghost/core/core/server/data/db/connection` | Knex instance |
| `ghost/core/core/server/data/migrations/` | Versioned migrations |

### Default network & paths

| Item | Value |
|------|-------|
| Bind | `127.0.0.1:2368` |
| Site URL | `http://localhost:2368` |
| Admin | `/ghost/` |
| Content API | `/ghost/api/content/` |
| Admin API | `/ghost/api/admin/` |
| Content dir | `content/` (images, media, files, themes, adapters, logs, data, settings, public) |

### Environment variables (selected, from source)

| Variable | Effect |
|----------|--------|
| `NODE_ENV` | Selects config env (default `development`) |
| `KEY__SUBKEY` | Override any nested config (double-underscore) |
| `GHOST_DEV_IS_DOCKER` | Loads the docker dev config layer |
| `GHOST_CI_SHUTDOWN_AFTER_BOOT` | Exit after successful boot (CI) |
| `DEBUG=ghost:boot*` | Boot timing diagnostics |
| `NUMERIC_IDS` | Toggle numeric IDs in the events plugin |
| `REQUIRE_INFILE_STREAM` | Set by `generate-data` for streamed imports |

### Lifecycle commands

| Goal | Command |
|------|---------|
| Install (prod) | `ghost install` |
| Start / stop (prod) | `ghost start` / `ghost stop` |
| Update (prod) | `ghost update` |
| Diagnose (prod) | `ghost doctor` |
| Dev environment | `pnpm dev` |
| Build all | `pnpm build` |
| Run migrations | `pnpm knex-migrator` |
| Seed data | `node index.js generate-data --quantities members:1000,posts:100 --seed 123` |
| REPL | `node index.js repl` |

### Default adapters

| Type | Default | Alternative |
|------|---------|-------------|
| Image/media/file storage | `Local*Storage` | Object storage adapter |
| Cache | `MemoryCache` | Redis adapter |
| Scheduling | `scheduling-default` | External scheduler |
| Redirects store | `FileStore` | `S3RedirectsStore` |
| SSO | `DefaultSSOAdapter` | Enterprise SSO |

---

_Generated June 2026 from TryGhost/Ghost at f283e8e. 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._
