Service Providers

Providers are the central place for configuring and bootstrapping services. Every tekir package ships a provider that registers itself in the DI container.

Introduction

A service provider is a class with up to three lifecycle methods: register(), boot(), and shutdown(). Providers are the glue between your configuration and the application: they read config, create instances, and bind them to the container so the rest of your app can resolve them via service() or app.use().

Creating a Provider

Generate a provider with the CLI:

tekir make:provider Stripe

A provider is a plain class, no base class or interface import needed:

app/providers/stripe_provider.ts
import type { App } from '@tekir/core'

export class StripeProvider {
  async register(app: App) {
    const config = app.use('config')
    const stripeConfig = config('stripe')
    if (!stripeConfig) return

    const stripe = new Stripe(stripeConfig.secretKey)
    app.instance('stripe', stripe)
  }

  async boot(app: App) {
    // Called after ALL providers have registered
    // Safe to resolve services from other providers
    const logger = app.use('logger')
    logger.info('Stripe initialized')
  }

  async shutdown(app: App) {
    // Cleanup on application shutdown
  }
}

Lifecycle

Providers run in two phases. First, every provider's register() runs. Then, every provider's boot() runs. This guarantees that by the time boot() is called, all services from all providers are available.

// Provider lifecycle:
//
// 1. REGISTER phase: all providers' register() run first
//    StripeProvider.register(app)     → binds 'stripe' service
//    MailProvider.register(app)       → binds 'mail' service
//
// 2. BOOT phase: all providers' boot() run after register
//    StripeProvider.boot(app)         → can use app.use('mail')
//    MailProvider.boot(app)           → can use app.use('stripe')
//
// 3. SHUTDOWN phase: on app.shutdown(), reverse order
//    MailProvider.shutdown(app)       → cleanup mail
//    StripeProvider.shutdown(app)     → cleanup stripe

register()

Use register() to bind services into the container. Do not resolve services from other providers here; they may not be registered yet.

boot()

Use boot() for logic that depends on other services. By this point, all providers have registered, so app.use() is safe to call.

shutdown()

Called when the application shuts down (e.g. SIGTERM). Providers are shut down in reverse order: the last registered provider shuts down first. Use this to close connections, flush buffers, or clean up resources.

Registering Providers

Register providers in start/kernel.ts via app.registerAll(). Pass the class itself, the framework instantiates it.

start/kernel.ts
import type { TekirApp } from '@tekir/core'
import { DatabaseProvider } from '@tekir/db'
import { CacheProvider } from '@tekir/cache'
import { ViewProvider } from '@tekir/view'
import { StripeProvider } from '~/providers/stripe_provider'

export default function({ app }: TekirApp) {
  app.registerAll([
    DatabaseProvider,
    CacheProvider,
    ViewProvider,
    StripeProvider
  ])
}

The Container

Providers interact with the app container using three methods:

import type { App } from '@tekir/core'

export class PaymentProvider {
  register(app: App) {
    // instance(): register a ready-to-use value
    app.instance('stripe', new Stripe(key))

    // singleton(): factory called once, result cached
    app.singleton('payments', () => {
      const stripe = app.use('stripe')
      return new PaymentService(stripe)
    })

    // bind(): factory called every time (new instance each resolve)
    app.bind('invoice', () => new Invoice())
  }
}
  • app.instance(name, value): register a ready value
  • app.singleton(name, factory): lazy, created once on first use()
  • app.bind(name, factory): new instance every use() call

Resolve services with app.use(name) or via the service() lazy proxy in services.ts:

import { service } from '@tekir/core'
import type { PaymentService } from '~/services/payment'

// In services.ts: lazy proxy, resolved on first access
export const payments = service<PaymentService>('payments')

// In a handler
import { payments } from '#services'
await payments.charge(user, amount)

Providers can skip registration when config is missing:

import type { App } from '@tekir/core'

export class RedisProvider {
  register(app: App) {
    const config = app.use('config')
    // Skip if redis not configured
    if (!config('redis')) return

    app.singleton('redis', () => {
      return new Redis(config('redis'))
    })
  }
}

Built-in Providers

Every @tekir/* package that needs container access ships a provider:

// Framework packages that ship with providers:
//
// @tekir/db          → DatabaseProvider    → registers 'db'
// @tekir/cache       → CacheProvider       → registers 'cache'
// @tekir/auth        → AuthProvider        → registers 'auth'
// @tekir/session     → SessionProvider     → registers session middleware
// @tekir/view        → ViewProvider        → registers 'view'
// @tekir/hash        → HashProvider        → registers 'hash'
// @tekir/logger      → LoggerProvider      → registers 'logger'
// @tekir/redis       → RedisProvider       → registers 'redis'
// @tekir/static      → StaticProvider      → registers static middleware
// @tekir/mail        → MailProvider        → registers 'mail'
// @tekir/queue       → QueueProvider       → registers 'queue'