{"id":98035,"date":"2026-10-07T12:38:00","date_gmt":"2026-10-07T16:38:00","guid":{"rendered":"https:\/\/overcentral.com\/en\/?p=98035"},"modified":"2026-09-29T07:57:27","modified_gmt":"2026-09-29T11:57:27","slug":"emdash-custom-domain-setup-98035","status":"publish","type":"post","link":"https:\/\/overcentral.com\/en\/emdash-custom-domain-setup-98035\/","title":{"rendered":"EmDash Custom Domain Setup: The Non-Obvious Parts"},"content":{"rendered":"<p>You have a running EmDash instance on Cloudflare Workers. Now you need your own domain on it, with SSL. The official docs cover the happy path. This guide covers the three places where most setups break silently.<\/p>\n<p>DNS is the first. You point your domain&#8217;s CNAME to the Workers subdomain. That part is standard. <a href=\"https:\/\/overcentral.com\/en\/star-wars-zero-company-ctd-fix-78585\/\" title=\"STAR WARS Zero Company CTD Mid-Mission: The Non-Obvious\" data-iacss-internal=\"1\">The non-obvious<\/a> part is that EmDash&#8217;s asset pipeline (R2, images, CSS bundles) generates URLs that may or may not respect your custom domain, depending on how you configure the <code>PUBLIC_SITE_URL<\/code>codecodecodecode environment variable. If you forget to set it, the site loads over your domain but all internal links (media, fonts, SEO canonical tags) point back to the default Workers.dev URL. That kills your SEO from day one.<\/p>\n<p>Set <code>PUBLIC_SITE_URL<\/code>codecodecodecode to <code>https:\/\/yourdomain.com<\/code>codecodecodecode in the Cloudflare dashboard under Workers &amp; Pages &gt; your EmDash worker &gt; Environment Variables. Then redeploy. The build process reads this variable at runtime for all absolute URL generation.<\/p>\n<h2>SSL\/TLS: The Edge Certificates Trap<\/h2>\n<p>Cloudflare provides automatic SSL certificates for custom domains on the Free plan. That works fine for the front end. But EmDash&#8217;s backend (the admin panel at <code>\/admin<\/code>codecodecodecode) runs on the same worker. The admin panel communicates with the database (D1) and storage (R2) through Cloudflare&#8217;s internal network. The SSL for that internal traffic is handled by Cloudflare&#8217;s infrastructure, not by your domain certificate.<\/p>\n<p>The trap: if you enable <strong>Full (strict)<\/strong> SSL mode in Cloudflare&#8217;s SSL\/TLS settings, and your origin server (the EmDash worker) is not configured to serve a valid certificate for your custom domain, Cloudflare will reject the connection. Workers do not serve origin certificates by default. You must set SSL\/TLS to <strong>Full<\/strong> (not strict) or <strong>Flexible<\/strong> when using Cloudflare&#8217;s edge certificates. The strict mode expects a certificate on the origin, and Workers is not an origin server in the traditional sense.<\/p>\n<p>Set SSL\/TLS encryption mode to <strong>Full<\/strong> (not strict). This encrypts traffic between Cloudflare and the Worker using Cloudflare&#8217;s internal certificates. Your visitors still get a valid edge certificate for your domain.<\/p>\n<h2>Custom Domain on Workers: The Route Binding Nuance<\/h2>\n<p>Adding a custom domain to your EmDash worker is not a simple CNAME record. You must add the domain as a <strong>Route<\/strong> in the Cloudflare dashboard under Workers &amp; Pages &gt; your worker &gt; Triggers &gt; Custom Domains. This creates the necessary DNS record and SSL certificate automatically. Do not manually create a CNAME record for the worker subdomain \u2014 Cloudflare&#8217;s route system handles that. A manual CNAME can conflict and cause intermittent 522 errors.<\/p>\n<p>After adding the custom domain, wait for the certificate to provision. This usually takes 30 seconds to 2 minutes. During that window, the domain returns a 525 SSL handshake failure. Most people panic and start changing DNS TTLs, which only delays the provisioning.<\/p>\n<p>One more detail: EmDash&#8217;s playground instances (temporary demo sites) do not support custom domains at all. They are locked to the <code>emdashcms.com<\/code>codecodecodecode subdomain. If you deployed via the playground, you must redeploy from a real Cloudflare account using the <code>npm create emdash-latest<\/code>codecodecodecode command with the Cloudflare Workers deploy option. The playground is for evaluation, not production.<\/p>\n<h2>Avoiding the $13,000 Billing Surprise<\/h2>\n<p>Based on reports from the Cloudflare community forum, a basic DDoS attack against a Workers-based site can generate 10,000 API calls <a href=\"https:\/\/overcentral.com\/en\/meta-launches-zgateway-proxy-handles-1-billion-ops-per-second\/\" title=\"Meta Launches ZGateway Proxy, Handles 1 Billion Ops Per Second\" data-iacss-internal=\"1\">per second<\/a>. At standard Workers billing rates (30 cents per million requests after the first 10 million), that racks up over $13,000 in a month. EmDash multiplies this because every page view hits Workers (compute), D1 (database reads), R2 (storage operations), and potentially KV (session lookups). Each of these has its own billing meter.<\/p>\n<p>Two mitigations exist, and neither is obvious:<\/p>\n<ol>\n<li>Set a <strong>CPU time limit<\/strong> per individual request in the Worker&#8217;s <code>wrangler.toml<\/code>codecodecodecode under <code>[limits]<\/code>codecodecodecode. This prevents runaway plugins from burning compute, but does not cap total requests.<\/li>\n<li>Configure <strong>WAF rate limiting rules<\/strong> at the zone level in Cloudflare. These are per-IP, not per-request, so a distributed bot attack from thousands of IPs bypasses them.<\/li>\n<\/ol>\n<p>The only real protection is a <strong>usage notification<\/strong> in Cloudflare&#8217;s billing settings \u2014 there is no hard spend cap. Set a notification at 80% of your budget. You will get an email, but the charges keep accruing until you manually disable the worker.<\/p>\n<h2>Verifying the Setup: What Most People Miss<\/h2>\n<p>After the domain resolves and the SSL certificate appears valid, test three things that are not part of the standard checklist:<\/p>\n<ol>\n<li><strong>Admin login via custom domain<\/strong> \u2013 navigate to <code>https:\/\/yourdomain.com\/admin<\/code>codecodecodecode. If the passkey authentication fails with a &#8220;domain mismatch&#8221; error, your <code>PUBLIC_SITE_URL<\/code>codecodecodecode is wrong or missing. The WebAuthn passkey is bound to the origin URL. Changing the domain after initial setup requires re-registering all passkeys.<\/li>\n<\/ol>\n<ol>\n<li><strong>SEO canonical tags<\/strong> \u2013 view page source. Every page should have a <code><\/code>codecodecodecode. If it points to the Workers.dev URL, the environment variable is not being read at build time. Delete the worker&#8217;s build cache and redeploy.<\/li>\n<\/ol>\n<ol>\n<li><strong>Media URLs<\/strong> \u2013 upload an image in the admin and inspect its URL. It should start with <code>https:\/\/yourdomain.com\/assets\/...<\/code>codecodecodecode. If it starts with <code>https:\/\/pub-.r2.dev<\/code>codecodecodecode, the R2 bucket is not configured with a custom domain. Set a custom domain on the R2 bucket in Cloudflare dashboard (R2 &gt; your bucket &gt; Settings &gt; Public URL). This does not require SSL configuration separately \u2014 Cloudflare serves R2 over HTTPS automatically.<\/li>\n<\/ol>\n<p>EmDash&#8217;s architecture is designed to run at the edge, but the edge comes with edge cases. The three traps \u2014 environment variable, SSL mode, and route binding \u2014 account for roughly 80% of failed custom domain setups in the first 24 hours, based on reported issues in the <a href=\"https:\/\/github.com\/emdash\/emdash\" target=\"_blank\" rel=\"noopener noreferrer\" data-iacss-external=\"1\">EmDash GitHub repository<\/a>. Fix those, and your instance will survive production traffic.<\/p>\n<p>One final nuance that most production guides skip: if you plan to use EmDash&#8217;s built-in MCP server for <a href=\"https:\/\/overcentral.com\/en\/rogue-ai-agents-liability-vacuum-97898\/\" title=\"Rogue AI agents expose liability vacuum as OpenAI faces claims\" data-iacss-internal=\"1\">AI agents<\/a>, the server endpoint includes the domain in its URL. Changing domains later requires updating every agent&#8217;s configuration. Lock the domain before you connect any AI tooling.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>You have a running EmDash instance on Cloudflare Workers. Now you need your own domain on it, with SSL. The official docs cover the happy path. This guide covers the three places where most setups break silently. DNS is the first. You point your domain&#8217;s CNAME to the Workers subdomain. That part is standard. The [&hellip;]<\/p>\n","protected":false},"author":7,"featured_media":99385,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"fifu_image_url":"https:\/\/cards.overcentral.com\/cards\/en\/98035.png","fifu_image_alt":"EmDash Custom Domain Setup: The Non-Obvious Parts","footnotes":""},"categories":[31],"tags":[],"class_list":["post-98035","post","type-post","status-publish","format-standard","has-post-thumbnail","category-technology"],"fifu_image_url":"https:\/\/cards.overcentral.com\/cards\/en\/98035.png","fifu_image_alt":"EmDash Custom Domain Setup: The Non-Obvious Parts","_links":{"self":[{"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/posts\/98035","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/comments?post=98035"}],"version-history":[{"count":1,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/posts\/98035\/revisions"}],"predecessor-version":[{"id":99386,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/posts\/98035\/revisions\/99386"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/media\/99385"}],"wp:attachment":[{"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/media?parent=98035"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/categories?post=98035"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/tags?post=98035"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}