Services under this directory own domain and integration logic used by Ghost Core through application-owned instances. The directory is a catalogue of runtime services, not a general home for all server-side business logic or I/O. Use the definition below when adding or relocating a top-level entry; existing placement alone is not evidence that something belongs here.
A service root is the application entry point for a capability whose instance, dependencies, and availability are owned by Ghost's boot lifecycle. It wires an implementation for callers to use, rather than merely exporting a class for callers to construct themselves.
This includes both:
- Resource-owning services, which need lifecycle coordination for work such as event subscriptions, scheduled jobs, or resources requiring cleanup.
- Composition-only services, which assemble dependencies and expose an application-owned instance without needing background work or a stop hook.
Having a timer or a shutdown method is not a prerequisite. Equally, performing I/O, reading configuration, containing business rules, or having a large implementation does not by itself make a module a service root. Do not add an empty lifecycle wrapper just to justify a directory's location.
These rules apply to top-level entries, not every file inside a service. A service can own ordinary classes, repositories, functions, types, templates, and provider adapters as private implementation details.
- Keep implementation used only by one service inside that service.
- Put shared server libraries, reusable constructors, and provider clients in
server/lib/, grouped by a specific capability. - Put HTTP routing and middleware in the appropriate part of
server/web/. - Keep API-specific support with its API owner rather than making it a service.
For example:
email-serviceexposes an application-owned wrapper initialized during boot.email-addressconstructs and exposes the application's email-address implementation throughinit()andservice. It qualifies without owning a background worker.- Magic-link token and URL helpers are reusable support for several workflows, not a separately boot-managed capability. They belong in their own library, not a mail service merely because some callers send the links by email.
Relocation does not resolve resource ownership by itself. If a supporting client acquires resources, its owner must still arrange any necessary cleanup.
The definition above guides new roots and directory cleanup. Existing roots
still use different construction and export patterns; a uniform managed
lifecycle interface is not yet the implemented contract throughout this
directory. Do not assume every root exports init, service, and shutdown.
Standardizing that interface must include the implementation, boot wiring, and
tests, not just a documentation change.
The temporary service inventory tracks this migration. Update it when adding, removing or renaming a root. It includes standalone files and supporting modules still awaiting relocation. Boot never reads it.
Each entry records its intended ownership and audit status. pending means
the contract audit has not started; auditing means evidence is incomplete;
audited means all facets have been investigated and recorded from source or
test evidence. It describes audit completeness, not test coverage or migration
readiness. An audited entry can still have test gaps or known bugs in blockers.
The notes describe audit priorities and intended changes, not verified contracts.
auditedAt is the full source commit SHA the record was last checked
against. Add it when completing or refreshing an audit; older records may
omit it. Advance it only after reviewing relevant changes, not merely
rebasing. Recorded contracts describe that revision. Preserve them and recheck
relevant changes when work on that service begins. If cited evidence has since
been deleted, keep a link to the file at the audited revision in the summary;
sources and tests list files that still exist, without implying freshness.
Before relying on a record, compare its service directory and cited source/test paths with that revision, and check for new callers. Changed files flag a recheck; the reference does not prove tests ran at that revision. Inventory validation checks its format without requiring Git history.
The unverified facets are open questions:
| Facet | Question to resolve |
|---|---|
| exports | What runtime exports and import conventions do callers use? |
| construction | Where are instances constructed, including deep imports? |
| consumers | Which application, request, job or CLI callers use them? |
| startup | What must be ready before each supported use? |
| lifetime | What identity and state survive initialization or restart? |
| configuration | Which values stay live and which are captured? |
| resources | Who owns, drains and releases resources, including nested ones? |
| failures | Which failures abort, continue, retry or disable the capability? |
Recorded summaries link to source and test evidence using paths relative to
ghost. Entry points are relative to this services directory and include
separate composition paths reached through deep imports. They are not a list
of every importable helper, type or job definition. An empty test list means
the summary has source evidence only, unless it links historical tests. A nonempty
list does not imply that every claim is tested: when coverage is partial, the
summary must say what the cited tests assert and which claims are source-only. Keep established source facts
under contracts; reserve unverified for questions the audit has not answered.
Record missing coverage and known bugs in blockers. Before changing a service,
address the gaps relevant to that change; neither audited nor a passing
inventory check proves that its behavior is protected against regressions.
The unit test checks the inventory's shape, paths and directory coverage. It
does not import services or drive initialization. README.md and the inventory
itself are the only metadata exclusions. Retire the inventory after migration;
keep the behavioral tests and replace the legacy check with a check for the
established service convention.
Follow an existing service in the same area when extending established code, without treating its legacy placement or lifecycle shortcuts as requirements. New standalone service logic should be TypeScript unless it must extend an existing JavaScript module.
The gifts, donations, and related services show the current transition pattern:
domain logic uses TypeScript with named exports, while a thin CommonJS
index.js or wrapper remains only where boot code or an existing require()
boundary needs it.
Ghost's boot sequence owns service construction. A new service that requires
initialization must expose an explicit init() and be called from
ghost/core/boot.js in the appropriate boot phase. Do not make the first
request responsible for constructing the service.
Keep wrapper initialization idempotent when callers may safely reach it more than once. Add shutdown or cleanup handling to the boot lifecycle when the service owns resources that must be released.