Skip to main content

Tooling system

Here’s one of the most useful patterns in Refract: every external dependency your app cares about — queues, loggers, metrics — is wired through a pluggable tooling layer. Swap the underlying vendor by changing one config value. The rest of your code doesn’t notice. The contract lives in apps/shared. The implementation lives in an isolated workspace package under apps/tools/*. A thin loader in the backend reads config and import()s the right one at runtime. Your app only ever sees a typed tools object.

How it works

Each tool exposes a discriminator in config (client, strategy, etc.). The schema is a validated union in apps/shared, so every branch is type-safe and Zod-checked before it reaches your app. The backend loader reads the discriminator and dynamically imports the right package — nothing is bundled that you’re not using.

All pluggable tools

Every tool follows the same three-layer pattern: a contract in apps/shared, one or more implementations (either workspace packages under apps/tools/ or loaders in apps/backend/src/tools/), and a loader that reads config and wires up the right one.
ToolConfig keyDefaultImplementationsLoader
Queuetools.queue.clientBullMQtooling-queue-bullmq, tooling-queue-sqsapps/backend/src/tools/queue/loader.ts
Loggertools.logger.clientPinotooling-logger-pino, tooling-logger-consoleapps/backend/src/tools/logger/loader.ts
Metricstools.metrics.clientStatsDtooling-metrics-statsdapps/backend/src/tools/metrics/loader.ts
CacheRedisRedis (always)apps/backend/src/tools/cache/
DatabaseSequelizeSequelize + PostgreSQLapps/backend/src/tools/rds/
Mailertools.mailer.clientSendGrid (prod) / Local (dev)Twilio SendGrid, Localapps/backend/src/tools/mailer/
Analyticstools.analytics.clientAmplitude (prod) / Local (dev)Amplitude, Localapps/backend/src/tools/analytics/
Payment Processortools.paymentProcessor.clientStripeStripe, StripeMock (test)apps/backend/src/tools/paymentProcessor/
The tools object — assembled at startup — is passed down to every resolver and utility function. No part of the app imports a vendor SDK directly.

Example: swapping the queue adapter

To switch from BullMQ to SQS:
  1. Add tooling-queue-sqs to apps/backend/package.json.
  2. Run make pnpm-install.
  3. Update tools.queue in apps/backend/src/configuration/production.ts:
    queue: {
      client: QueueClientType.SQS,
      region: process.env.AWS_REGION,
      queueUrl: process.env.SQS_QUEUE_URL,
    }
    
  4. Set the required env vars in your environment.
The ConfigType.tools.queue field is a discriminated union — TypeScript will fail to compile if the config shape doesn’t match the selected adapter’s Zod schema.

Extending this system

Adding a new tool — or a new implementation of an existing one — is a four-step pattern that stays consistent across every tool in the repo.
  1. Define or extend the contract in apps/shared (types + Zod schema).
  2. Add an implementation package under apps/tools/<tool>/<name>/ — pull in any vendor SDKs here, not on apps/backend.
  3. Add the workspace package to apps/backend/package.json and wire it into the loader.
  4. Run make pnpm-lockfile-update, then make test module=backend and make test module=<tooling-package>.

What not to do

Never duplicate vendor SDKs on apps/backend for a tool that’s already encapsulated in a tooling-* package. Those packages exist to keep the backend clean.
Never paste production credentials into development.ts or .env.development. If it ever touches version control, rotate it.

File conventions

What’s next?