Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
9147ae2
Add files via upload
ljanaideh Apr 19, 2026
e69385e
Merge pull request #1 from ljanaideh/ljanaideh-patch-1
ljanaideh Apr 19, 2026
7f2661a
Add emailOnPublish plugin to Astro config
ljanaideh Apr 19, 2026
2400694
Rename package to @emdash-cms/plugin-email-on-publish
ljanaideh Apr 19, 2026
37b9a67
Add files via upload
ljanaideh Apr 19, 2026
3852536
Add files via upload
ljanaideh Apr 19, 2026
6ebd16c
Replace emailOnPublish with emailOnPublishPlugin
ljanaideh Apr 19, 2026
36bc058
Merge pull request #2 from ljanaideh/ljanaideh-patch-2
ljanaideh Apr 19, 2026
4f9141d
fix: update lockfile for email-on-publish plugin
ljanaideh Apr 19, 2026
0ea21f3
Update emailOnPublishPlugin import path
ljanaideh Apr 19, 2026
f704ca8
Add files via upload
ljanaideh Apr 19, 2026
51f1210
Merge pull request #3 from ljanaideh/ljanaideh-patch-3
ljanaideh Apr 19, 2026
fc1f81c
Add files via upload
ljanaideh Apr 19, 2026
abfc6e4
Delete demos/cloudflare/plugins/sandbox-test/email-on-publish.ts
ljanaideh Apr 19, 2026
7ea5e61
style: format
emdashbot[bot] Apr 19, 2026
9f94bfe
Merge pull request #4 from ljanaideh/ljanaideh-patch-3
ljanaideh Apr 19, 2026
e512ded
Add notify-on-publish plugin using Resend
ljanaideh Apr 19, 2026
4112ed3
Merge pull request #5 from ljanaideh/mail_resend_1
ljanaideh Apr 19, 2026
d34c520
Configure CF bindings for emdash-laith deployment
ljanaideh Apr 19, 2026
811ec81
Merge pull request #6 from ljanaideh/mail_resend_1
ljanaideh Apr 19, 2026
10eb370
Remove Cloudflare Access auth; use EmDash passkey default
ljanaideh Apr 19, 2026
e78b960
Merge pull request #7 from ljanaideh/mail_resend_1
ljanaideh Apr 19, 2026
7c4c335
Clean up gitignore and stale files from upstream sync
ljanaideh Apr 19, 2026
d1af944
Disable CF cache adapter blocking publishes
ljanaideh Apr 20, 2026
206b4de
Wire notify-on-publish recipients/from/siteUrl to env vars
ljanaideh Apr 20, 2026
3957c99
Simplify Cloudflare config: remove unused cache + media providers
ljanaideh Apr 20, 2026
7b559dd
Hardcode notify-on-publish recipients (env vars not available at build)
ljanaideh Apr 20, 2026
1d27824
notify-on-publish: read recipient from post email field
ljanaideh Apr 20, 2026
652aa19
notify-on-publish: deeper logging + recursive email discovery
ljanaideh Apr 20, 2026
2f0b4d3
TEMPORARY: hardcode Resend key for sandbox env debug (REVERT ME)
ljanaideh Apr 20, 2026
07a8b9c
debug: wrap sandbox hook in try/catch with verbose logging
ljanaideh Apr 20, 2026
63d01e3
debug: swap resend for webhook.site to isolate sandbox fetch issue
ljanaideh Apr 20, 2026
ac92f5e
debug: use ctx.http.fetch instead of global fetch in sandbox
ljanaideh Apr 20, 2026
66a43d5
fix: await content:afterSave/afterPublish hooks so sandboxed plugin f…
ljanaideh Apr 20, 2026
7b6e7d3
feat: send Resend notification via ctx.http.fetch + forward host secr…
ljanaideh Apr 20, 2026
b263550
feat(notify-on-publish): email on every save of published posts (remo…
ljanaideh Apr 20, 2026
ee49b58
feat(notify-on-publish): switch to content:afterPublish hook (fires o…
ljanaideh Apr 20, 2026
4359148
fix(core): await content:afterDelete/afterUnpublish/media:afterUpload…
ljanaideh Apr 20, 2026
87666b6
feat(notify-on-publish): opt-in per-collection via email field; suppo…
ljanaideh Apr 20, 2026
414d22e
feat: add notify-postmark plugin + forward POSTMARK_SERVER_TOKEN/POST…
ljanaideh Apr 20, 2026
92f30c8
feat(cloudflare): add Hyperdrive adapter and switch demo-cloudflare t…
ljanaideh May 4, 2026
536eb67
fix(cloudflare): add Hyperdrive localConnectionString for build-time …
ljanaideh May 4, 2026
f669187
fix(cloudflare): use env var for Hyperdrive local connection string
ljanaideh May 4, 2026
bcfe144
fix(hyperdrive): disable pg SSL layer to avoid TLS-within-TLS in Workers
ljanaideh May 4, 2026
df9f51c
feat(demo-cloudflare): add bootstrap-postgres script to initialize RD…
ljanaideh May 4, 2026
ee173b2
feat(demo-cloudflare): run bootstrap migrations as part of deploy
ljanaideh May 4, 2026
e442599
fix(demo-cloudflare): add kysely + pg as direct deps for bootstrap sc…
ljanaideh May 4, 2026
6a92057
chore: update lockfile with kysely + pg deps for demo-cloudflare
ljanaideh May 4, 2026
afed4dd
fix(deploy): build packages before running bootstrap migration script
ljanaideh May 4, 2026
4c68bc6
chore: trigger build
ljanaideh May 4, 2026
f4d514e
chore: trigger build (simplify deploy script to just wrangler deploy)
ljanaideh May 4, 2026
b586fb7
chore: trigger CI build
ljanaideh May 4, 2026
cef950b
fix(cloudflare): add connectionTimeoutMillis to Hyperdrive pg pool
ljanaideh May 4, 2026
f81df07
chore: trigger CI deploy (connectionTimeoutMillis + RDS public access)
ljanaideh May 4, 2026
0a76c5a
chore: trigger build against new RDS (emdash-demo)
ljanaideh May 5, 2026
c45a837
chore: retrigger build (fix DATABASE_URL sslmode)
ljanaideh May 5, 2026
0698dbc
fix(cloudflare): new Hyperdrive config + SESSION KV binding
ljanaideh May 5, 2026
11605e6
fix(cloudflare): replace pg-pool timeout with Promise.race for Worker…
ljanaideh May 5, 2026
30691de
fix(cloudflare): disable pg-pool idle timeout to prevent reconnect ha…
ljanaideh May 5, 2026
53ec8f1
debug: add middleware trace logs to identify hang location
ljanaideh May 5, 2026
c572fda
debug: add trace logs in admin.astro to locate hang
ljanaideh May 5, 2026
d513e3e
fix(cloudflare): use fresh pg.Client per query to fix Hyperdrive reco…
ljanaideh May 5, 2026
d5dc34d
fix(core): add idleTimeoutMillis + connectionTimeoutMillis to postgre…
ljanaideh May 6, 2026
a4183da
fix(core): read created collection inside transaction to avoid post-c…
ljanaideh May 6, 2026
7b354d6
fix(core): move createField reads inside transaction to bypass Hyperd…
ljanaideh May 6, 2026
962df5a
fix(core): move collection existence checks inside transactions to by…
ljanaideh May 6, 2026
544459f
fix(core): avoid Hyperdrive cache poisoning in taxonomy def creation …
ljanaideh May 6, 2026
b7412c5
fix(core): quote \"window\" column in rate-limit SQL + fix taxonomy t…
ljanaideh May 6, 2026
a7e9512
fix(core): wrap setup reads in transactions to bypass Hyperdrive quer…
ljanaideh May 6, 2026
6a03c9a
fix(core): correct import path for withTransaction in setup middleware
ljanaideh May 6, 2026
ff6cac7
style: format
emdashbot[bot] May 6, 2026
6be0e79
chore: remove debug console.log statements added during diagnosis
ljanaideh May 6, 2026
7c3bcf9
docs: add Hyperdrive + PostgreSQL deployment guide and skill
ljanaideh May 6, 2026
71cffe1
chore(cloudflare): update Hyperdrive binding to private instance
ljanaideh May 7, 2026
4858ca1
style: format
emdashbot[bot] May 7, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,4 @@ examples/wp-theme-unit-test/

.perf-query-counts
query-counts-out/
**/.dev.vars
1 change: 1 addition & 0 deletions demos/cloudflare/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.dev.vars
58 changes: 24 additions & 34 deletions demos/cloudflare/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
import cloudflare from "@astrojs/cloudflare";
import react from "@astrojs/react";
import {
d1,
hyperdrive,
r2,
access,
sandbox,
cloudflareCache,
cloudflareImages,
cloudflareStream,
// cloudflareCache,
} from "@emdash-cms/cloudflare";
import { formsPlugin } from "@emdash-cms/plugin-forms";
import { notifyOnPublishPlugin } from "@emdash-cms/plugin-notify-on-publish";
import { notifyPostmarkPlugin } from "@emdash-cms/plugin-notify-postmark";
import { webhookNotifierPlugin } from "@emdash-cms/plugin-webhook-notifier";
import { defineConfig, fontProviders } from "astro/config";
import emdash from "emdash/astro";
Expand All @@ -36,55 +35,46 @@ export default defineConfig({
integrations: [
react(),
emdash({
// D1 database - binding name must match wrangler.jsonc
// session: "auto" enables read replicas (nearest replica for anon,
// bookmark-based consistency for authenticated users)
database: d1({ binding: "DB", session: "auto" }),
// Hyperdrive database — binding name must match wrangler.jsonc
database: hyperdrive({ binding: "HYPERDRIVE" }),
// R2 storage for media
storage: r2({ binding: "MEDIA" }),
// Cloudflare Access authentication
// Reads CF_ACCESS_AUDIENCE from env (wrangler secret or .dev.vars)
auth: access({
teamDomain: "cloudflare-cto.cloudflareaccess.com",
autoProvision: true,
defaultRole: 30, // Author
// Map your IdP groups to roles (optional)
// roleMapping: {
// "Admins": 50,
// "Editors": 40,
// },
}),
// Media providers - Cloudflare Images and Stream
// Reads from env vars at runtime: CF_ACCOUNT_ID, CF_IMAGES_TOKEN, CF_STREAM_TOKEN
// Or customize with accountIdEnvVar/apiTokenEnvVar options
mediaProviders: [
cloudflareImages({
accountIdEnvVar: "CF_MEDIA_ACCOUNT_ID",
apiTokenEnvVar: "CF_MEDIA_API_TOKEN",
accountHash: "5LGXGUnHU18h6ehN_xjpXQ",
}),
cloudflareStream({
accountIdEnvVar: "CF_MEDIA_ACCOUNT_ID",
apiTokenEnvVar: "CF_MEDIA_API_TOKEN",
}),
],
// Trusted plugins (run in host worker)
plugins: [
// Test plugin that exercises all v2 APIs
formsPlugin(),
notifyOnPublishPlugin(),
notifyPostmarkPlugin(),
// notifyOnPublishPlugin({
// recipients: ["ljanaideh@atypon.com"],
// collections: ["posts"],
// from: "onboarding@resend.dev",
// siteUrl: "https://emdash-laith.laithaljanaideh.workers.dev",
// }),
// notifyOnPublishPlugin({
// recipients: (process.env.EMAIL_TO || "").split(",").map(s => s.trim()).filter(Boolean),
// collections: ["posts"],
// from: process.env.EMAIL_FROM || "onboarding@resend.dev",
// siteUrl: process.env.SITE_URL || "https://emdash-laith.laithaljanaideh.workers.dev",
// }),
],
// Sandboxed plugins (run in isolated workers)
sandboxed: [webhookNotifierPlugin()],
sandboxed: [],
// Sandbox runner for Cloudflare
sandboxRunner: sandbox(),
// Plugin marketplace
marketplace: "https://marketplace.emdashcms.com",
}),
],
experimental: {
cache: {
provider: cloudflareCache(),
},
// cache: {
// provider: cloudflareCache(),
// },
routeRules: {
"/": {
maxAge: 3_600,
Expand Down
1 change: 0 additions & 1 deletion demos/cloudflare/emdash-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export interface Page {
slug: string | null;
status: string;
title: string;
template?: "Default" | "Full Width";
content?: PortableTextBlock[];
createdAt: Date;
updatedAt: Date;
Expand Down
11 changes: 7 additions & 4 deletions demos/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@
"build": "astro build",
"build:all": "pnpm run --filter @emdash-cms/demo-cloudflare... build",
"preview": "astro preview",
"deploy": "pnpm build:all && wrangler deploy",
"deploy": "wrangler deploy",
"db:bootstrap": "node scripts/bootstrap-postgres.mjs",
"db:create": "wrangler d1 create emdash-demo",
"db:reset:remote": "./scripts/reset-db.sh",
"typecheck": "astro check"
},
"dependencies": {
"kysely": "^0.27.0",
"pg": "^8.0.0",
"@astrojs/cloudflare": "catalog:",
"@astrojs/react": "catalog:",
"@emdash-cms/cloudflare": "workspace:*",
"@emdash-cms/plugin-forms": "workspace:*",
"@emdash-cms/plugin-notify-on-publish": "workspace:*",
"@emdash-cms/plugin-notify-postmark": "workspace:*",
"@emdash-cms/plugin-webhook-notifier": "workspace:*",
"@tanstack/react-query": "catalog:",
"@tanstack/react-router": "catalog:",
Expand All @@ -34,7 +39,5 @@
},
"emdash": {
"seed": "seed/seed.json"
},
"peerDependencies": {},
"optionalDependencies": {}
}
}
40 changes: 40 additions & 0 deletions demos/cloudflare/scripts/bootstrap-postgres.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Bootstrap script — runs EmDash migrations against a PostgreSQL database.
*
* Run as part of the deploy command, or manually:
*
* DATABASE_URL="postgres://user:pass@host:5432/db" node scripts/bootstrap-postgres.mjs
*/

import { runMigrations } from "emdash/db";
import { Kysely, PostgresDialect } from "kysely";
import pg from "pg";

const { Pool } = pg;

const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
console.error("Error: DATABASE_URL environment variable is required.");
process.exit(1);
}

console.log("Connecting to PostgreSQL...");
const ssl = process.env.DATABASE_SSL === "false" ? false : { rejectUnauthorized: false };
const pool = new Pool({ connectionString, max: 1, ssl });
const db = new Kysely({ dialect: new PostgresDialect({ pool }) });

try {
console.log("Running migrations...");
const { applied } = await runMigrations(db);

if (applied.length === 0) {
console.log("No new migrations — database is already up to date.");
} else {
console.log(`Applied ${applied.length} migration(s):`);
for (const m of applied) {
console.log(` ✓ ${m}`);
}
}
} finally {
await pool.end();
}
60 changes: 60 additions & 0 deletions demos/cloudflare/terraform/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
output "rds_endpoint" {
description = "RDS instance hostname"
value = aws_db_instance.emdash.address
}

output "rds_port" {
description = "RDS port"
value = aws_db_instance.emdash.port
}

output "rds_db_name" {
description = "Database name"
value = aws_db_instance.emdash.db_name
}

output "connection_string" {
description = "DATABASE_URL for bootstrap script (uses master user — swap to emdash_app after setup)"
value = "postgres://${var.master_username}:PASSWORD@${aws_db_instance.emdash.address}:${aws_db_instance.emdash.port}/${var.db_name}?sslmode=require"
sensitive = false
}

output "hyperdrive_origin" {
description = "Host to use when running: wrangler hyperdrive update <id> --origin-host <this>"
value = aws_db_instance.emdash.address
}

output "post_provision_steps" {
description = "Reminder of manual steps after terraform apply"
value = <<-EOT

── Post-provision checklist ────────────────────────────────────────────

1. Connect as master user and create the app user:

psql "postgres://${var.master_username}:PASSWORD@${aws_db_instance.emdash.address}:5432/${var.db_name}?sslmode=require"

CREATE USER emdash_app WITH PASSWORD 'your-app-password';
GRANT CONNECT ON DATABASE ${var.db_name} TO emdash_app;
GRANT CREATE ON SCHEMA public TO emdash_app;
GRANT USAGE ON SCHEMA public TO emdash_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO emdash_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO emdash_app;

2. Update Cloudflare Hyperdrive to point at the new endpoint:

wrangler hyperdrive update 01b192bf33194ecda6ad2aa1b2f2f8d2 \
--origin-host ${aws_db_instance.emdash.address} \
--origin-port 5432 \
--database ${var.db_name} \
--origin-user emdash_app

3. Update DATABASE_URL in Cloudflare Pages env vars:

postgres://emdash_app:PASSWORD@${aws_db_instance.emdash.address}:5432/${var.db_name}?sslmode=require

4. Push a new commit to trigger the Pages build (which runs migrations).

────────────────────────────────────────────────────────────────────────
EOT
}
2 changes: 1 addition & 1 deletion demos/cloudflare/worker-configuration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ declare namespace Cloudflare {
}
interface Env {
MEDIA: R2Bucket;
DB: D1Database;
HYPERDRIVE: Hyperdrive;
LOADER: WorkerLoader;
CF_ACCESS_AUDIENCE: string;
CF_MEDIA_API_TOKEN: string;
Expand Down
37 changes: 19 additions & 18 deletions demos/cloudflare/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -1,37 +1,38 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "emdash-demo",
"name": "emdash-laith",
"main": "./src/worker.ts",
"compatibility_date": "2026-01-14",
// disable_nodejs_process_v2 needed until unenv fix lands in Pages
// See: https://github.com/withastro/astro/issues/14511
"compatibility_flags": ["nodejs_compat", "disable_nodejs_process_v2"],
// Static assets served from dist/
"routes": [

// Hyperdrive binding — emdash-pg config pointing to emdash-demo RDS
"hyperdrive": [
{
"pattern": "demo.emdashcms.com",
"zone_name": "demo.emdashcms.com",
"custom_domain": true,
"binding": "HYPERDRIVE",
"id": "01b192bf33194ecda6ad2aa1b2f2f8d2",
},
],
// D1 Database binding
"d1_databases": [
{
"binding": "DB",
"database_name": "emdash_db",
},
],
// R2 bucket for media storage

// R2 bucket — points to existing my-emdash-media bucket
"r2_buckets": [
{
"binding": "MEDIA",
"bucket_name": "emdash-media",
"bucket_name": "my-emdash-media",
},
],
// Observability

"observability": {
"enabled": true,
},

// KV namespace for Astro session storage
"kv_namespaces": [
{
"binding": "SESSION",
"id": "0516c5af42c24460b6a9eba751ffc0e3",
},
],

// Worker Loader for plugin sandboxing
"worker_loaders": [
{
Expand Down
Loading
Loading