Fastify Plugin System#
Cedar's API server is built on Fastify and leans heavily on Fastify's plugin encapsulation model. Understanding that model is essential when adding middleware, hooks, or decorators that need to apply across the whole server — especially when both the API function handler and GraphQL server are involved.
Plugin Topology#
createServer() builds the root Fastify instance and registers two first-class plugins as siblings on it:
| Plugin | File | Handles |
|---|---|---|
cedarFastifyAPI | packages/api-server/src/plugins/api.ts | Lambda-style API functions (/api/:routeName) |
redwoodFastifyGraphQLServer | packages/api-server/src/plugins/graphql.ts | GraphQL Yoga endpoints (/api/graphql, /api/graphql/health, etc.) |
Registration order matters — both plugins are awaited sequentially:
cedarFastifyAPIis registered first (it excludesdist/functions/graphql.jsfrom its glob scan viafastGlobOptions.ignore).redwoodFastifyGraphQLServeris conditionally registered afterward, only ifapi/dist/functions/graphql.jsis found.
A third plugin, redwoodFastifyWeb in packages/adapters/fastify/web/src/web.ts, handles static-file serving and is only relevant when running the full web + API server together (not the standalone API server).
Encapsulation Boundaries and Hook Propagation#
Fastify's encapsulation rules determine whether hooks, decorators, and plugins registered inside one plugin are visible to sibling or ancestor plugins.
The key rule: hooks/decorators registered inside a non-fastify-plugin-wrapped async function are scoped to that child context only. They are not visible to sibling plugins registered on the same parent.
Both cedarFastifyAPI and redwoodFastifyGraphQLServer are plain async functions — not wrapped with fastify-plugin (fp()) . Fastify therefore gives each its own isolated encapsulation scope.
Implication: configureApiServer is scoped to cedarFastifyAPI only#
The configureApiServer callback (passed as configureServer inside cedarFastifyAPI) is invoked on the cedarFastifyAPI instance — inside its child scope . Anything registered there is invisible to redwoodFastifyGraphQLServer.
The Compression / Sibling-Plugin Bug#
This encapsulation boundary is the root cause of a known, open bug :
Following the docs to register
@fastify/compressviaconfigureApiServercompresses regular API function responses, but not GraphQL responses.
Why: @fastify/compress adds an onSend hook. When registered via configureApiServer, that hook is added to cedarFastifyAPI's child context. redwoodFastifyGraphQLServer is a sibling on the root server, outside that boundary, so its routes never see the hook .
The same encapsulation gap affects any other global plugin registered via configureApiServer — rate limiting, CORS headers, custom decorators, etc.
Suggested fix (not yet implemented as of 2026-08-04): wrap cedarFastifyAPI with fastify-plugin so that plugins/hooks escape the child scope and apply to the whole server . An alternative is to register redwoodFastifyGraphQLServer as a child of the same scope rather than a top-level sibling.
Compression: Current State#
As of 2026-08-04, @fastify/compress is not a dependency of any Cedar package — it is not registered in cedarFastifyAPI, redwoodFastifyGraphQLServer, or redwoodFastifyWeb . Static assets are served with a bare @fastify/static registration and no maxAge, cacheControl, or immutable-asset handling .
This is tracked in issue #2301, which was raised in priority because the topology decision in #2294 blessed single-container deployments where Fastify serves assets directly instead of behind a CDN.
Hooks on the Root Server#
Some hooks are registered directly on the root server instance (outside any plugin), making them truly global:
onRequest— initializes the async context store for every requestonReady— sendsprocess.send?.('ready')for PM2/baremetal integrationonListen— logs the listening address
Hooks registered on the root instance propagate down to all child plugins, so these fire for both API and GraphQL routes.
Plugin Registration Inside Each Plugin#
Each plugin registers its own sub-plugins and hooks privately:
cedarFastifyAPI :
@fastify/url-data— URL parsing utilitiesfastify-raw-body— exposes raw request body (awaited to ensure readiness)onRequesthook — re-runs async context store for every request in this scope- Content-type parsers for
application/x-www-form-urlencodedandmultipart/form-data
redwoodFastifyGraphQLServer :
@fastify/url-data@fastify/multipart— multipart form data for file uploads (note: nofastify-raw-bodyhere)onRequesthook — async context store, same pattern
Configuring the Server (What Works Today)#
| Need | Correct approach |
|---|---|
| Global Fastify options (logger, body limit, etc.) | fastifyServerOptions in createServer() |
Plugins that only need to apply to /api/* functions | configureApiServer callback |
| Plugins that need to apply globally (compression, rate-limit, CORS) | Register on the returned server instance after createServer() — not via configureApiServer |
| Custom content-type parsers for API functions | configureApiServer callback |
The encapsulation note in the docs warns that the API plugin "cannot be mutated after it is registered," which is correct but does not yet surface the sibling-plugin limitation described above.
Key Files#
| File | Purpose |
|---|---|
packages/api-server/src/createServer.ts | Root server construction and plugin registration order |
packages/api-server/src/plugins/api.ts | cedarFastifyAPI plugin — API function routing |
packages/api-server/src/plugins/graphql.ts | redwoodFastifyGraphQLServer — GraphQL Yoga server |
packages/adapters/fastify/web/src/web.ts | redwoodFastifyWeb — static file serving |
docs/docs/server-file.md | User-facing guide on configuring the server file |