Providers

Providers are concrete implementations of ports. They connect your application to external services — databases, caches, mail, auth, and more.

How it works

Contract Kit uses a ports and adapters pattern. You define the shape of your dependencies (ports), then plug in implementations (providers).

import { createNextServer } from "@contract-kit/next";
import { redisProvider } from "@contract-kit/provider-redis";
import { loggerPinoProvider } from "@contract-kit/provider-logger-pino";

export const server = await createNextServer({
  ports,
  providers: [redisProvider, loggerPinoProvider],
  createContext: async ({ req, ports }) => ({
    ports,
  }),
});

Your handlers access these through ctx.ports. Swapping a provider (e.g. replacing Redis with an in-memory cache for tests) requires no changes to your application code.


Database — Drizzle + Turso

bun add @contract-kit/provider-drizzle-turso

SQLite database via Drizzle ORM and Turso.

import { createDrizzleTursoProvider } from "@contract-kit/provider-drizzle-turso";
import { sqliteTable, text } from "drizzle-orm/sqlite-core";

const todos = sqliteTable("todos", {
  id: text("id").primaryKey(),
  title: text("title").notNull(),
});

const schema = { todos };
const dbProvider = createDrizzleTursoProvider({ schema });

Requires TURSO_DB_URL and TURSO_DB_AUTH_TOKEN environment variables.


Cache — Redis

bun add @contract-kit/provider-redis

In-memory caching with Redis.

import { redisProvider } from "@contract-kit/provider-redis";

Requires REDIS_URL environment variable. Provides a CachePort with get, set, and delete methods.

// In a handler
const cached = await ctx.ports.cache.get("todo:123");
if (cached) return { status: 200, body: cached };

const todo = await ctx.ports.db.todos.findById("123");
await ctx.ports.cache.set("todo:123", todo, 60); // TTL in seconds
return { status: 200, body: todo };

Mail — Resend

bun add @contract-kit/provider-mail-resend

Send emails via the Resend API.

import { mailResendProvider } from "@contract-kit/provider-mail-resend";

Requires RESEND_API_KEY and RESEND_FROM environment variables.

await ctx.ports.mail.send({
  to: "user@example.com",
  subject: "Welcome",
  html: "<h1>Hello!</h1>",
});

Mail — SMTP

bun add @contract-kit/provider-mail-smtp

Send emails via any SMTP server.

import { mailSmtpProvider } from "@contract-kit/provider-mail-smtp";

Requires MAIL_HOST, MAIL_PORT, MAIL_USER, MAIL_PASS, and MAIL_FROM environment variables.


Logger — Pino

bun add @contract-kit/provider-logger-pino pino

Structured logging with Pino.

import { loggerPinoProvider } from "@contract-kit/provider-logger-pino";

Configured via LOG_LEVEL, LOG_FORMAT, LOG_SERVICE, and LOG_TIMESTAMP environment variables.

ctx.ports.logger.info("Todo created", { todoId: todo.id });
ctx.ports.logger.error("Failed to create todo", { error });

Rate Limiting — Upstash

bun add @contract-kit/provider-rate-limit-upstash

Rate limiting via Upstash Redis.

import { upstashRateLimitProvider } from "@contract-kit/provider-rate-limit-upstash";

Requires UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN environment variables. Use with contract metadata:

export const createTodo = todos
  .post("/api/todos")
  .meta({ rateLimit: { max: 10, windowSec: 60 } })
  .body(CreateTodoSchema)
  .response(201, TodoSchema);

Event Bus — In-Memory

bun add @contract-kit/provider-event-bus-memory

An in-memory event bus for domain events.

import { createInMemoryEventBus } from "@contract-kit/provider-event-bus-memory";

const eventBus = createInMemoryEventBus();
// Publish an event
await ctx.ports.eventBus.publish("todo.created", { todoId: todo.id });

// Subscribe to events (at setup time)
eventBus.subscribe("todo.created", async (event) => {
  console.log("Todo created:", event.todoId);
});

Auth — BetterAuth

bun add @contract-kit/provider-auth-better-auth

Authentication via BetterAuth. You configure BetterAuth yourself — this provider wraps your instance for use with Contract Kit ports.

import { createAuthBetterAuthProvider } from "@contract-kit/provider-auth-better-auth";
import { auth } from "./auth"; // your BetterAuth instance

const authProvider = createAuthBetterAuthProvider(auth);
// In a handler
const user = await ctx.ports.auth.requireUser();
// throws 401 if not authenticated

Jobs — Inngest

bun add @contract-kit/provider-inngest

Background jobs via Inngest.

import { inngestProvider } from "@contract-kit/provider-inngest";

Testing

For tests, create mock implementations of your ports instead of using real providers.

const testPorts = definePorts({
  db: {
    todos: {
      findById: async (id: string) => ({ id, title: "Test", completed: false }),
      create: async (data) => ({ id: "1", ...data }),
    },
  },
  cache: {
    get: async () => null,
    set: async () => {},
    delete: async () => {},
  },
});

No external services needed. Your handlers work exactly the same way.