Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions alchemy-web/src/content/docs/providers/cloudflare/hyperdrive.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ const db = await Hyperdrive("my-postgres-db", {
});
```

## Local Development Only

For local-first development when you don't have production credentials yet, you can omit the `origin` property and only provide `dev.origin`. This matches the wrangler.jsonc pattern where you can use a local connection string without needing a real Hyperdrive ID.

```ts
const db = await Hyperdrive("my-postgres-db", {
name: "my-postgres-db",
dev: {
origin: "postgres://postgres:postgres@localhost:5432/postgres",
},
});
```

:::caution
When you're ready to deploy to production, you'll need to add the `origin` property with your production database connection. Alchemy will throw a helpful error if you try to deploy without it.
:::

## With Explicit Origin Object

If you'd prefer to set parameters explicitly, you can use an object.
Expand Down
26 changes: 23 additions & 3 deletions alchemy/src/cloudflare/hyperdrive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,11 @@ export interface HyperdriveProps extends CloudflareApiOptions {

/**
* Database connection origin configuration
*
* Optional in local mode - if not provided, dev.origin will be used.
* Required for production deployments.
*/
origin: HyperdriveOriginInput;
origin?: HyperdriveOriginInput;

/**
* Caching configuration
Expand Down Expand Up @@ -311,13 +314,30 @@ export async function Hyperdrive(
id: string,
props: HyperdriveProps,
): Promise<Hyperdrive> {
const origin = normalizeHyperdriveOrigin(props.origin);
// In local mode, origin can be omitted if dev.origin is provided
const devOrigin = props.dev?.origin;
const productionOrigin = props.origin;

if (!Scope.current.local && !productionOrigin) {
throw new Error(
`Hyperdrive "${id}" requires 'origin' for production deployment. ` +
`Add the production database connection to enable deployment.\n\n` +
`For local development only, you can omit 'origin' and only provide 'dev.origin'.`,
);
}

// Use dev.origin as fallback if origin is not provided (local mode only)
const origin = productionOrigin
? normalizeHyperdriveOrigin(productionOrigin)
: normalizeHyperdriveOrigin(devOrigin!);

const dev = {
origin: toConnectionString(
normalizeHyperdriveOrigin(props.dev?.origin ?? origin),
normalizeHyperdriveOrigin(devOrigin ?? productionOrigin!),
),
force: Scope.current.local,
};

return await _Hyperdrive(id, {
...props,
origin,
Expand Down
19 changes: 19 additions & 0 deletions alchemy/test/cloudflare/hyperdrive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,25 @@ describe.concurrent("Hyperdrive Resource", () => {
}
});

test("hyperdrive with no production origin throws", async (scope) => {
try {
const project = await NeonProject(`${testId}-dev-only`, {
name: `Hyperdrive Test Dev Only ${BRANCH_PREFIX}`,
});

await expect(
Hyperdrive(`${testId}-dev`, {
name: `test-hyperdrive-dev-${BRANCH_PREFIX}`,
dev: {
origin: project.connection_uris[0].connection_parameters,
},
}),
).rejects.toThrowError(/requires 'origin' for production deployment/);
} finally {
await destroy(scope);
}
});

describe("normalizeHyperdriveOrigin", () => {
it("normalizes postgres string origin", () => {
const origin = normalizeHyperdriveOrigin(
Expand Down
4 changes: 4 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5956,6 +5956,8 @@

"ajv-keywords/ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],

"alchemy/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],

"alchemy/glob": ["[email protected]", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],

"alchemy-web/@astrojs/cloudflare": ["@astrojs/[email protected]", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-pQ8bokC59GEiXvyXpC4swBNoL7C/EknP+82KFzQwgR/Aeo5N1oPiAoPHgJbpPya/YF4E26WODdCQfBQDvLRfuw=="],
Expand Down Expand Up @@ -6994,6 +6996,8 @@

"alchemy-web/sharp/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],

"alchemy/@types/node/undici-types": ["[email protected]", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],

"alchemy/glob/jackspeak": ["[email protected]", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],

"alchemy/glob/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
Expand Down
19 changes: 19 additions & 0 deletions examples/cloudflare-dev-only-hyperdrive/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Dev only hyperdrive example

This example provisions a Prisma Postgres database and reference it using hyperdrive ONLY in dev mode.
This may not be the most useful example for end users, it primarily serves to improve coverage during Alchemy's smoke tests.

## Usage

```bash
bun i
bun alchemy deploy
```

The script prints the generated database connection string to stdout.

To tear down the resources:

```bash
bun run destroy
```
23 changes: 23 additions & 0 deletions examples/cloudflare-dev-only-hyperdrive/alchemy.run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import alchemy from "alchemy";
import { Hyperdrive, Worker } from "alchemy/cloudflare";
import { Connection, Database, Project } from "alchemy/prisma-postgres";

const app = await alchemy("alchemy-dev-only-hyperdrive");

const project = await Project("project");

const database = await Database("database", {
project,
region: "us-east-1",
});

const connection = await Connection("connection", { database });

const db = await Hyperdrive("dev-only-hyperdrive", {
origin: app.local ? undefined : connection.connectionString.unencrypted,
dev: {
origin: connection.connectionString.unencrypted,
},
});

await app.finalize();
18 changes: 18 additions & 0 deletions examples/cloudflare-dev-only-hyperdrive/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "cloudflare-dev-only-hyperdrive",
"version": "0.0.0",
"description": "Alchemy Cloudflare Dev Only Hyperdrive Example",
"type": "module",
"scripts": {
"build": "tsc -b",
"deploy": "alchemy deploy --env-file ../../.env",
"destroy": "alchemy destroy --env-file ../../.env"
},
"devDependencies": {
"alchemy": "workspace:*",
"typescript": "catalog:"
},
"dependencies": {
"pg": "^8.16.3"
}
}
30 changes: 30 additions & 0 deletions examples/cloudflare-dev-only-hyperdrive/src/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Client } from "pg";
import type { worker } from "../alchemy.run.ts";

export default {
async fetch(_request: Request, env: typeof worker.Env): Promise<Response> {
const client = new Client({
connectionString: env.HYPERDRIVE.connectionString,
});

try {
// Connect to the database
await client.connect();
console.log("Connected to PostgreSQL database");

// Perform a simple query
const result = await client.query("SELECT * FROM pg_tables");

return Response.json({
success: true,
result: result.rows,
});
} catch (error: any) {
console.error("Database error:", error.message);

return new Response("Internal error occurred", { status: 500 });
} finally {
await client.end();
}
},
};
8 changes: 8 additions & 0 deletions examples/cloudflare-dev-only-hyperdrive/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "."
},
"include": ["./alchemy.run.ts"]
}
Loading