Dosu LogoDosu Logo
Ask
Join our Discord
Cedar's SpacePublic
Cedar
DocumentsCedar's Space
Fastify Plugin System
Fastify Plugin System
Type
Topic
Status
Published
Created
Aug 4, 2026
Updated
Aug 4, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

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:

PluginFileHandles
cedarFastifyAPIpackages/api-server/src/plugins/api.tsLambda-style API functions (/api/:routeName)
redwoodFastifyGraphQLServerpackages/api-server/src/plugins/graphql.tsGraphQL Yoga endpoints (/api/graphql, /api/graphql/health, etc.)

Registration order matters — both plugins are awaited sequentially:

  1. cedarFastifyAPI is registered first (it excludes dist/functions/graphql.js from its glob scan via fastGlobOptions.ignore).
  2. redwoodFastifyGraphQLServer is conditionally registered afterward, only if api/dist/functions/graphql.js is 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/compress via configureApiServer compresses 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 request
  • onReady — sends process.send?.('ready') for PM2/baremetal integration
  • onListen — 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 utilities
  • fastify-raw-body — exposes raw request body (awaited to ensure readiness)
  • onRequest hook — re-runs async context store for every request in this scope
  • Content-type parsers for application/x-www-form-urlencoded and multipart/form-data

redwoodFastifyGraphQLServer :

  • @fastify/url-data
  • @fastify/multipart — multipart form data for file uploads (note: no fastify-raw-body here)
  • onRequest hook — async context store, same pattern

Configuring the Server (What Works Today)#

NeedCorrect approach
Global Fastify options (logger, body limit, etc.)fastifyServerOptions in createServer()
Plugins that only need to apply to /api/* functionsconfigureApiServer 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 functionsconfigureApiServer 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#

FilePurpose
packages/api-server/src/createServer.tsRoot server construction and plugin registration order
packages/api-server/src/plugins/api.tscedarFastifyAPI plugin — API function routing
packages/api-server/src/plugins/graphql.tsredwoodFastifyGraphQLServer — GraphQL Yoga server
packages/adapters/fastify/web/src/web.tsredwoodFastifyWeb — static file serving
docs/docs/server-file.mdUser-facing guide on configuring the server file
Documents
Fastify Plugin System
Prisma Integration
Test Database Configuration