Skip to content

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:

  1. Create the client once in server.ts
  2. Share it via createContextKey + setContext in onBeforeRequest
  3. 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.