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 inapps/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 inapps/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.
| Tool | Config key | Default | Implementations | Loader |
|---|---|---|---|---|
| Queue | tools.queue.client | BullMQ | tooling-queue-bullmq, tooling-queue-sqs | apps/backend/src/tools/queue/loader.ts |
| Logger | tools.logger.client | Pino | tooling-logger-pino, tooling-logger-console | apps/backend/src/tools/logger/loader.ts |
| Metrics | tools.metrics.client | StatsD | tooling-metrics-statsd | apps/backend/src/tools/metrics/loader.ts |
| Cache | — | Redis | Redis (always) | apps/backend/src/tools/cache/ |
| Database | — | Sequelize | Sequelize + PostgreSQL | apps/backend/src/tools/rds/ |
| Mailer | tools.mailer.client | SendGrid (prod) / Local (dev) | Twilio SendGrid, Local | apps/backend/src/tools/mailer/ |
| Analytics | tools.analytics.client | Amplitude (prod) / Local (dev) | Amplitude, Local | apps/backend/src/tools/analytics/ |
| Payment Processor | tools.paymentProcessor.client | Stripe | Stripe, StripeMock (test) | apps/backend/src/tools/paymentProcessor/ |
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:- Add
tooling-queue-sqstoapps/backend/package.json. - Run
make pnpm-install. - Update
tools.queueinapps/backend/src/configuration/production.ts: - Set the required env vars in your environment.
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.- Define or extend the contract in
apps/shared(types + Zod schema). - Add an implementation package under
apps/tools/<tool>/<name>/— pull in any vendor SDKs here, not onapps/backend. - Add the workspace package to
apps/backend/package.jsonand wire it into the loader. - Run
make pnpm-lockfile-update, thenmake test module=backendandmake test module=<tooling-package>.
What not to do
❌ Never duplicate vendor SDKs onapps/backendfor a tool that’s already encapsulated in atooling-*package. Those packages exist to keep the backend clean.
❌ Never paste production credentials intodevelopment.tsor.env.development. If it ever touches version control, rotate it.
File conventions
What’s next?
- Configuration system — how
ConfigTypeand per-environment modules wire into the tool layer. - BullMQ — default queue adapter; SQS — full SQS adoption guide.
- Pino — structured logger; Console — minimal dev logger.
- Stripe — payment processor setup and webhook handling.
- Twilio SendGrid — production mailer; Local mailer — dev mailer.
- StatsD — metrics adapter.
- Redis cache — built-in cache helpers.
- Architecture overview — the full system map.