Integrations Overview
Juice does not ship adapters. Every integration follows the same pattern: create the client, share it via context, read it in your routes. If it works in TypeScript, it works in Juice.
The Pattern
Every integration — database, auth, email, payments, storage — follows the same three steps:
- Create the client once in
server.ts - Share it via
createContextKey+setContextinonBeforeRequest - Read it in routes via
getContext(request, key)
Here is the generic pattern that every integration follows:
// server.ts
import { createRouter } from '@cmj/juice/runtime';
import { createContextKey, setContext } from '@cmj/juice/runtime';
// 1. Create the client once
import { SomeClient } from 'some-library';
const client = new SomeClient({ /* config */ });
// 2. Define a typed context key
export const clientKey = createContextKey<SomeClient>('some-client');
const router = createRouter({
onBeforeRequest: async (req) => {
// 3. Share the client with every request
setContext(req, clientKey, client);
},
// ... routes
});Then in any route or server component:
// app/routes/home.tsx
import { getContext } from '@cmj/juice/runtime';
import { clientKey } from '../../server.js';
export default async function Home({ request }: { request: Request }) {
// 4. Read the client — fully typed
const client = getContext(request, clientKey);
const data = await client.query('...');
return <div>{/* render data */}</div>;
}Why This Works
Juice uses standard Request and Response objects. There is no proprietary middleware format, no special adapter interface, no plugin system to learn. Any library that works in TypeScript works in Juice.
The context system provides type-safe dependency injection scoped to the current request. You get full TypeScript inference from the context key through to the route — no casting, no as any, no runtime checks.
Available Integrations
This section walks through complete examples for the most common integrations:
- SQLite — Built into Bun via
bun:sqlite. Zero dependencies. - Drizzle ORM — Type-safe SQL with schema definitions and migrations.
- Prisma — Full-featured ORM with Prisma Client and schema-first workflow.
- better-auth — Drop-in authentication with email/password and OAuth.
Other Integrations
The same pattern works for anything else. Redis, S3, Stripe, Resend, OpenAI, Upstash, Turso — create the client, share it via context, read it in your routes. No adapter needed.
// Redis example — same pattern
import { createContextKey, setContext } from '@cmj/juice/runtime';
const redis = new Redis(process.env.REDIS_URL);
export const redisKey = createContextKey<Redis>('redis');
// in onBeforeRequest:
setContext(req, redisKey, redis);
// in a route:
const redis = getContext(request, redisKey);
await redis.set('key', 'value');// Stripe example — same pattern
import Stripe from 'stripe';
import { createContextKey, setContext } from '@cmj/juice/runtime';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export const stripeKey = createContextKey<Stripe>('stripe');
// in onBeforeRequest:
setContext(req, stripeKey, stripe);
// in a route:
const stripe = getContext(request, stripeKey);
const session = await stripe.checkout.sessions.create({ /* ... */ });No adapter tax. Frameworks that require adapters create a bottleneck: you cannot use a library until someone writes an adapter for it. Juice skips this entirely. If it has a JavaScript API, it works today.