• Blog
  • Documentation
  • Courses
  • Changelog
  • AI Starters
  • UI Kit
  • FAQ
  • Supamode
    New
  • Pricing

Launch your next SaaS in record time with Makerkit, a React SaaS Boilerplate for Next.js and Supabase.

Makerkit is a product of Makerkit Pte Ltd (registered in the Republic of Singapore)Company Registration No: 202407149CFor support or inquiries, please contact us

About
  • FAQ
  • Contact
  • Verify your Discord
  • Consultation
  • Open Source
  • Become an Affiliate
Product
  • Documentation
  • Blog
  • Changelog
  • UI Blocks
  • Figma UI Kit
  • AI SaaS Starters
License
  • Activate License
  • Upgrade License
  • Invite Member
Legal
  • Terms of License
    • Account API
    • Team Account API
    • Authentication API
    • User Workspace API
    • Team Workspace API
    • OTP API
    • Registry API
    • Feature Policies API

Registry API for interchangeable services | Next.js Supabase Turbo

Understand Makerkit's typed registry helper and reuse it to swap implementations across environments.

Makerkit ships with a small but powerful registry helper at packages/shared/src/registry/index.ts.

The helper wraps a map of lazy factories and a coordinated setup queue, so we can decide which implementation to load only when the app needs it.

This keeps the monorepo flexible without exploding the number of conditionals or circular imports - with implementations that can be swapped at runtime using environment variables.

This is useful when you want to provide different implementations of a feature based on the environment.

We use this pattern extensively for:

  • billing providers (ex. Stripe, Lemon Squeezy, etc.)
  • monitoring providers (ex. Sentry, Signoz, etc.)
  • CMS providers (ex. Prismic, Sanity, etc.)
  • Mailers (ex. Resend, Mailgun, etc.)
  • and more...

Why we use a registry

  • Pluggable infrastructure: many features (billing, monitoring, CMS, mailers) can target different SaaS providers. The registry lets each package expose a single typed entry point while shipping multiple implementations side-by-side.
  • Lazy loading: factories only run during get(...), so optional features do not bloat the JavaScript bundle or initialize connections until required.
  • Type-safe ergonomics: the generic signature ensures that register('stripe', ...) and get('stripe') stay in sync. Asking for an unknown implementation fails fast with a descriptive error.
  • Coordinated setup: addSetup and setup allow packages to group async initialization work (for example, seeding demo data or configuring third-party SDKs) without leaking details to consumers.

Core API surface

The helper returns a Registry object with four methods:

  1. register(name, factory) stores an async factory for that implementation and returns the registry for chaining.
  2. get(...names) waits for any registered setup tasks, then resolves one or many implementations. Passing multiple names yields a tuple that keeps the original order.
  3. addSetup(group, callback) queues initialization callbacks under a named group.
  4. setup(group?) executes all registered setup groups (or a single group) exactly once.

Because ImplementationFactory<T> can return either T or Promise<T>, you can register synchronous helpers or full async imports with the same signature.

Example: selecting a billing gateway at runtime

import { createRegistry } from '@kit/shared/registry';
import type { BillingGateway } from './types';
type BillingProvider = 'stripe' | 'lemon-squeezy';
const billingRegistry = createRegistry<BillingGateway, BillingProvider>();
billingRegistry
.register('stripe', async () => {
const { createStripeGateway } = await import('./stripe-gateway');
return createStripeGateway();
})
.register('lemon-squeezy', async () => {
const { createLemonSqueezyGateway } = await import('./lemon-squeezy-gateway');
return createLemonSqueezyGateway();
});
export async function getBillingGateway() {
const provider = process.env.NEXT_PUBLIC_BILLING_PROVIDER ?? 'stripe';
return await billingRegistry.get(provider);
}

Calling getBillingGateway() will resolve only the factory that matches the active provider, keeping the rest of the codebase decoupled from the concrete implementation.

Reusing the pattern in your own packages

Follow these steps whenever you need interchangeable implementations in your app:

  1. Model the interface you expect the implementation to expose and the union of valid provider names.
  2. Instantiate a registry with createRegistry<Interface, ProviderUnion>() inside the package that owns the feature.
  3. Register implementations close to their code. Use dynamic import() to avoid bundling providers that are not in use.
  4. Expose a helper (for example, getCmsClient() or a React provider) that reads configuration and calls registry.get. Consumers stay unaware of the registry details.
  5. Optionally add setup hooks if your providers require shared initialization (e.g. registering cron jobs or verifying credentials). Group them with addSetup('billing', callback) and trigger them via await registry.setup('billing') from your bootstrapping code.

Because the registry is just TypeScript, you can reuse it outside Makerkit — drop the helper into any project where you want to ship multiple drop-in strategies without rewriting your public API.

On this page
  1. Why we use a registry
    1. Core API surface
      1. Example: selecting a billing gateway at runtime
        1. Reusing the pattern in your own packages