Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Ghost Admin Guide

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.

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
  2. Repository & Monorepo Structure
  3. Architecture Overview
  4. The Boot Sequence
  5. Configuration System
  6. Configuration Reference
  7. Data Layer
  8. Service Catalogue
  9. HTTP & API Surface
  10. Adapters & Extensibility
  11. Security
  12. Email & Newsletter Pipeline
  13. Analytics Pipeline (Tinybird)
  14. Deployment
  15. Operations
  16. Development Workflow
  17. Testing Strategy
  18. Code Patterns & Conventions
  19. Potential Improvements
  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.

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

                         ┌─────────────────────────────────────────┐
                         │              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.

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:

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):

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:

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:

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

# 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:

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

# 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

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.

# 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.alls 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