Cloudflare Workers + Drizzle

Integrate fragments with Cloudflare Workers, React Router v7, and Drizzle

This guide shows how to integrate Fragno Fragments into a Cloudflare Workers application using React Router v7, Neon PostgreSQL, and Drizzle ORM, using the Stripe fragment as an example.

Prerequisites

  • A React Router v7 project configured for Cloudflare Workers
  • Any PostgreSQL database (such as Neon or PlanetScale)
  • Basic familiarity with Drizzle ORM

Step 1: Installation

Install the Stripe fragment and required dependencies:

npm install @fragno-dev/stripe @fragno-dev/db drizzle-orm pg dotenv
npm install -D @fragno-dev/cli drizzle-kit @types/pg

Step 2: Database Configuration

2.1 Configure Environment Variables

Create a .dev.vars file in your project root:

PG_DATABASE_URL=postgresql://user:password@your-neon-host.neon.tech/dbname?sslmode=require
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

For production, set these in your Cloudflare Workers secrets:

wrangler secret put PG_DATABASE_URL
wrangler secret put STRIPE_SECRET_KEY
wrangler secret put STRIPE_WEBHOOK_SECRET

2.2 Create Database Client and Drizzle Setup

Create app/db/postgres.ts:

app/db/postgres.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { schema } from "./postgres.schema.ts";
import { Client } from "pg";

export function createPostgresClient() {
  return new Client({
    connectionString: process.env.PG_DATABASE_URL!,
  });
}

export function createDrizzleDatabase(client: Client) {
  return drizzle({ client, schema });
}

export type DrizzleDatabase = ReturnType<typeof createDrizzleDatabase>;

Cloudflare Worker invocations are short-lived and stateless, we therefore want to establish a new database connection for each request. The same pattern would also work if we later decided to start using Cloudflare Hyperdrive, in that case we'd obtain the HYPERDRIVE instance from the env context in the Fetch handler.

2.3 Create Drizzle Adapter for Fragno

Create app/fragno/database-adapter.ts:

app/fragno/database-adapter.ts
import { DrizzleAdapter } from "@fragno-dev/db/adapters/drizzle";
import type { DrizzleDatabase } from "../db/postgres.ts";

export function createAdapter(db: DrizzleDatabase | (() => DrizzleDatabase)) {
  if (typeof db === "function") {
    return new DrizzleAdapter({
      db: db(),
      provider: "postgresql",
    });
  }

  return new DrizzleAdapter({
    db,
    provider: "postgresql",
  });
}

This adapter accepts either a database instance or a factory function, providing flexibility for different initialization patterns.

Step 3: Create Server-Side Fragment Instance

Create app/fragno/stripe-server.ts:

import { createStripeFragment } from "@fragno-dev/stripe";
import { createAdapter } from "./database-adapter.ts";
import {
  createPostgresClient,
  createDrizzleDatabase,
  type DrizzleDatabase,
} from "../db/postgres.ts";

export function createStripeServer(db: DrizzleDatabase | (() => DrizzleDatabase)) {
  return createStripeFragment(
    {
      stripeSecretKey: process.env.STRIPE_SECRET_KEY!,
      webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
      onStripeCustomerCreated: async (stripeCustomerId, referenceId) => {
        // Update your user/org record with the Stripe customer ID
      },
      resolveEntityFromRequest: async (context) => {
        // Return authenticated user/org info
        return {
          referenceId: "user_id",
          customerEmail: "user@example.com",
        };
      },
    },
    {
      databaseAdapter: createAdapter(db),
    },
  );
}

// Top-level instance for CLI tools (schema generation)
export const fragment = createStripeServer(() => {
  const client = createPostgresClient();
  return createDrizzleDatabase(client);
});

It's important for the Fragment instance to be exported on the top-level so that the Fragno CLI can find it and generate the schema file.

Step 4: Configure Cloudflare Worker

Update your workers/app.ts to create database connections per request and pass them via context:

import { createRequestHandler } from "react-router";
import { createDrizzleDatabase, createPostgresClient, type DrizzleDatabase } from "~/db/postgres";

declare module "react-router" {
  export interface AppLoadContext {
    cloudflare: {
      env: CloudflareEnv;
      ctx: ExecutionContext;
    };
    db: DrizzleDatabase;
  }
}

const requestHandler = createRequestHandler(
  () => import("virtual:react-router/server-build"),
  import.meta.env.MODE,
);

export default {
  async fetch(request, env, ctx) {
    const client = createPostgresClient();
    try {
      await client.connect();
      const db = createDrizzleDatabase(client);

      return await requestHandler(request, {
        cloudflare: { env, ctx },
        db,
      });
    } catch (error) {
      console.error("Error fetching request", error);
      return new Response("Internal Server Error", { status: 500 });
    } finally {
      await client.end();
    }
  },
} satisfies ExportedHandler<CloudflareEnv>;

Key Points:

  • Database connection is created per request
  • Connection is properly cleaned up in the finally block
  • Database instance is passed via AppLoadContext for type-safe access in routes

Step 5: Mount API Routes

Create a React Router v7 API route at app/routes/api/stripe.tsx:

import { createStripeServer } from "~/fragno/stripe-server";
import type { Route } from "./+types/stripe";

export async function loader({ request, context }: Route.LoaderArgs) {
  return createStripeServer(context.db).handler(request);
}

export async function action({ request, context }: Route.ActionArgs) {
  return createStripeServer(context.db).handler(request);
}

This creates a catch-all route at /api/stripe that forwards all requests to the Fragment handler. Both loader (GET) and action (POST/PUT/DELETE) are required to handle all HTTP methods.

Step 6: Schema Management

Fragno Fragments define their own schema that can be merged with your application schema. This means that you can query tables from the Fragment in your application code, using Drizzle's type-safe query builder.

6.1 Configure Drizzle Kit for Dual Schemas

Update your Drizzle config to also include the Fragno-generated schema.

drizzle.config.ts
import { defineConfig } from "drizzle-kit";
import dotenv from "dotenv";

dotenv.config({
  path: ".dev.vars",
});

export default defineConfig({
  dialect: "postgresql",
  schema: ["./app/db/postgres/postgres.schema.ts", "./app/db/postgres/fragno-schema.ts"],
  out: "./app/db/postgres/migrations",
  dbCredentials: {
    url: process.env.PG_DATABASE_URL!,
  },
});

Important: Both your application schema and the Fragno-generated schema must be included in the schema array.

6.2 Setup Package Scripts

Update your package.json to include the following script to generate the Fragno schema:

{
  "scripts": {
    "db:fragno:generate": "npx @fragno-dev/cli db generate app/fragno/stripe-server.ts -o app/db/postgres/fragno-schema.ts",
    // ...
  },
}

6.3 Generate and Merge Schemas

  1. Generate Fragno schema (creates Fragment database tables):

    npm run db:fragno:generate

    This creates app/db/postgres/fragno-schema.ts with the Fragment's database schema.

  2. Create your application schema at app/db/postgres/postgres.schema.ts:

    import { pgTable, text, serial, timestamp } from "drizzle-orm/pg-core";
    import { stripe_db_schema } from "./fragno-schema.ts";
    
    export const users = pgTable("users", {
      id: serial().primaryKey(),
      name: text().notNull(),
      email: text(),
      stripeCustomerId: text(),
    });
    
    // Export combined schema
    export const schema = {
      users,
      // ... your other tables
      ...stripe_db_schema, // Include Fragment tables
    };
  3. Generate and run migrations:

Depending on your method of choice you can either use generate or push to apply the migrations.

npm run db:postgres:generate
npm run db:postgres:push

6.4 Schema Update Workflow

When the Fragment is updated and requires schema changes, you can run the Fragno generate command again to update the Fragno-Drizzle schema.

npm run db:fragno:generate

Step 7: Client-Side Integration

7.1 Create Client Instance

Create app/lib/stripe-client.ts:

import { createStripeClient } from "@fragno-dev/stripe/react";

export const stripeClient = createStripeClient();

Note: The file should NOT end with .client.ts as this prevents server-side rendering in some frameworks.

7.2 Use in React Components

import { stripeClient } from "~/lib/stripe-client";

export default function SubscribeButton() {
  const { mutate, loading, error } = stripeClient.upgradeSubscription();

  const handleSubscribe = async () => {
    const { url, redirect } = await mutate({
      body: {
        priceId: "price_1234567890",
        successUrl: `${window.location.origin}/success`,
        cancelUrl: window.location.href,
      },
    });

    if (redirect) {
      window.location.href = url;
    }
  };

  return (
    <button onClick={handleSubscribe} disabled={loading}>
      {loading ? "Loading..." : "Subscribe"}
    </button>
  );
}

That's it!

The subscriptions in your database are now kept in sync with Stripe through webhook events. You can query the subscriptions directly using Drizzle or use the helpers either client-side via the stripeClient or server-side via the stripeFragment instance.

Next Steps

For more information about the Stripe fragment: