Skip to content

Instantly share code, notes, and snippets.

@Cyberistic
Created January 29, 2026 19:09
Show Gist options
  • Select an option

  • Save Cyberistic/b3152599b6849022d5aae879cbdf45fa to your computer and use it in GitHub Desktop.

Select an option

Save Cyberistic/b3152599b6849022d5aae879cbdf45fa to your computer and use it in GitHub Desktop.
Embedded Prisma Studio with Cloudflare D1 - workaround for sqlite_master blocking

Embedded Prisma Studio with Cloudflare D1

This guide shows how to embed Prisma Studio in your application with Cloudflare D1 database support.

The Problem

Cloudflare D1's Worker binding blocks introspection queries (sqlite_master, pragma_table_list, pragma_table_xinfo) that Prisma Studio needs to discover your schema. This results in errors like:

Error: not authorized

The Solution

Use Cloudflare's D1 HTTP REST API for schema introspection queries, which doesn't have the same restrictions as the Worker binding. Regular data queries still use the fast D1 Worker binding.

Setup

1. Install Dependencies

npm install @prisma/[email protected]
# or
bun add @prisma/[email protected]

2. Environment Variables

Add these to your .env:

CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
CLOUDFLARE_ACCOUNT_ID=your-cloudflare-account-id
D1_DATABASE_ID=your-d1-database-id  # Get from: npx wrangler d1 list

3. Create the BFF API Endpoint

Create src/routes/api/studio.$.ts (or equivalent for your framework):

import type { D1Database } from "@cloudflare/workers-types";

// Types for Prisma Studio BFF protocol
interface Query<T = Record<string, unknown>> {
  parameters: readonly unknown[];
  sql: string;
  transformations?: Partial<Record<keyof T, "json-parse">>;
}

interface StudioBFFQueryRequest {
  procedure: "query";
  query: Query<unknown>;
}

interface StudioBFFSequenceRequest {
  procedure: "sequence";
  sequence: readonly [Query<unknown>, Query<unknown>];
}

type StudioBFFRequest = StudioBFFQueryRequest | StudioBFFSequenceRequest;

interface D1HttpResponse {
  success: boolean;
  result?: { results: unknown[] }[];
  errors?: { code: number; message: string }[];
}

interface StudioEnv {
  D1: D1Database;
  CLOUDFLARE_ACCOUNT_ID: string;
  CLOUDFLARE_API_TOKEN: string;
  D1_DATABASE_ID: string;
}

// D1 HTTP API client - bypasses Worker binding restrictions
async function queryD1HttpApi(
  accountId: string,
  databaseId: string,
  apiToken: string,
  sql: string
): Promise<unknown[] | null> {
  const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/d1/database/${databaseId}/query`;
  const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${apiToken}`,
    },
    body: JSON.stringify({ sql }),
  });

  const data = (await response.json()) as D1HttpResponse;
  if (!data.success || !data.result?.[0]?.results) return null;
  return data.result[0].results;
}

// Fetch schema via HTTP API
async function fetchSchemaFromHttpApi(
  accountId: string,
  databaseId: string,
  apiToken: string
): Promise<unknown[] | null> {
  const tables = await queryD1HttpApi(
    accountId, databaseId, apiToken,
    `SELECT name, sql FROM sqlite_master WHERE type='table' 
     AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%' 
     AND name != '_prisma_migrations'`
  );

  if (!tables) return null;

  const schemaInfo = [];
  for (const table of tables as { name: string; sql: string }[]) {
    const columns = await queryD1HttpApi(
      accountId, databaseId, apiToken,
      `SELECT * FROM pragma_table_xinfo('${table.name}')`
    );

    const fks = await queryD1HttpApi(
      accountId, databaseId, apiToken,
      `SELECT * FROM pragma_foreign_key_list('${table.name}')`
    );

    const fkMap = new Map();
    if (fks) {
      for (const fk of fks as { from: string; table: string; to: string }[]) {
        fkMap.set(fk.from, { table: fk.table, to: fk.to });
      }
    }

    schemaInfo.push({
      name: table.name,
      sql: table.sql,
      columns: (columns || []).map((col: any) => ({
        name: col.name,
        datatype: col.type,
        default: col.dflt_value,
        pk: col.pk,
        computed: col.hidden,
        nullable: col.notnull === 0 ? 1 : 0,
        fk_table: fkMap.get(col.name)?.table || null,
        fk_column: fkMap.get(col.name)?.to || null,
      })),
    });
  }

  return schemaInfo;
}

// Cache schema for 1 minute
let cachedSchema: unknown[] | null = null;
let cacheTime = 0;

function isIntrospectionQuery(sql: string): boolean {
  const lower = sql.toLowerCase();
  return lower.includes("pragma_table_list") ||
         lower.includes("pragma_table_xinfo") ||
         lower.includes("sqlite_schema") ||
         lower.includes("sqlite_master");
}

async function getSchemaInfo(env: StudioEnv): Promise<unknown[]> {
  if (!env.CLOUDFLARE_ACCOUNT_ID || !env.CLOUDFLARE_API_TOKEN || !env.D1_DATABASE_ID) {
    throw new Error("D1 HTTP API credentials not configured");
  }

  if (cachedSchema && Date.now() - cacheTime < 60000) {
    return cachedSchema;
  }

  const schema = await fetchSchemaFromHttpApi(
    env.CLOUDFLARE_ACCOUNT_ID,
    env.D1_DATABASE_ID,
    env.CLOUDFLARE_API_TOKEN
  );

  if (!schema) throw new Error("Failed to fetch schema from D1 HTTP API");

  cachedSchema = schema;
  cacheTime = Date.now();
  return schema;
}

async function executeD1Query<T>(
  d1: D1Database,
  query: Query<T>,
  env: StudioEnv
): Promise<[Error] | [null, T[]]> {
  // Intercept introspection queries → use HTTP API
  if (isIntrospectionQuery(query.sql)) {
    const schema = await getSchemaInfo(env);
    return [null, schema as T[]];
  }

  // Regular queries → use D1 Worker binding (fast)
  try {
    const stmt = d1.prepare(query.sql);
    const bound = query.parameters.length > 0
      ? stmt.bind(...query.parameters)
      : stmt;
    const result = await bound.all();
    return [null, result.results as T[]];
  } catch (error) {
    return [error instanceof Error ? error : new Error(String(error))];
  }
}

// POST handler - implement based on your framework
export async function handleStudioRequest(request: Request, env: StudioEnv) {
  const body = await request.json() as StudioBFFRequest;

  if (body.procedure === "query") {
    const [error, results] = await executeD1Query(env.D1, body.query, env);
    return Response.json(error ? [{ name: "Error", message: error.message }] : [null, results]);
  }

  if (body.procedure === "sequence") {
    const [q1, q2] = body.sequence;
    const [err1, res1] = await executeD1Query(env.D1, q1, env);
    if (err1) return Response.json([[{ name: "Error", message: err1.message }]]);
    
    const [err2, res2] = await executeD1Query(env.D1, q2, env);
    if (err2) return Response.json([[null, res1], [{ name: "Error", message: err2.message }]]);
    
    return Response.json([[null, res1], [null, res2]]);
  }

  return Response.json([{ name: "Error", message: "Invalid procedure" }], { status: 400 });
}

4. Create the Studio Page Component

import { useMemo } from "react";
import { Studio } from "@prisma/studio-core/ui";
import { createSQLiteAdapter } from "@prisma/studio-core/data/sqlite-core";
import { createStudioBFFClient } from "@prisma/studio-core/data/bff";
import "@prisma/studio-core/ui/index.css";

export function PrismaStudioPage() {
  const adapter = useMemo(() => {
    const executor = createStudioBFFClient({ url: "/api/studio" });
    return createSQLiteAdapter({ executor });
  }, []);

  return (
    <div style={{ height: "100vh" }}>
      <Studio adapter={adapter} />
    </div>
  );
}

5. Add CSS (Optional)

/* Override Prisma Studio styles if needed */
.prisma-studio-wrapper {
  height: 100vh;
  width: 100%;
}

How It Works

  1. Schema Discovery: When Prisma Studio loads, it queries sqlite_master and pragma_table_xinfo to discover tables/columns
  2. Interception: Our BFF detects these introspection queries and routes them to Cloudflare's D1 HTTP REST API
  3. Data Queries: Regular SELECT/INSERT/UPDATE/DELETE queries use the fast D1 Worker binding
  4. Caching: Schema info is cached for 1 minute to reduce HTTP API calls

Architecture

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  Prisma Studio  │────▶│   BFF Endpoint   │────▶│  D1 Worker API  │
│   (Frontend)    │     │  /api/studio     │     │  (Data queries) │
└─────────────────┘     └────────┬─────────┘     └─────────────────┘
                                 │
                                 │ Introspection
                                 ▼ queries only
                        ┌──────────────────┐
                        │  D1 HTTP REST    │
                        │  api.cloudflare  │
                        └──────────────────┘

Credits

Inspired by Drizzle's d1-http driver approach.

@Cyberistic
Copy link
Author

relevant issue: prisma/prisma#22184
In my setup I'm using Tanstack Start + Alchemy.run + Prisma studio + oRPC and this worked for me
special thanks to my LLM buddy. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment