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.