Everyone says EmDash is secure because plugins run in sandboxed environments. That’s the headline feature, the reason Cloudflare calls it the spiritual successor to WordPress. And it’s true — on Cloudflare’s runtime, with a paid plan, every plugin is isolated in a V8 isolate, incapable of touching your database or file system unless you explicitly grant permission.
But that’s not how self-hosting works.
The same architecture that protects you on Cloudflare leaves you exposed when you walk away.
When you run EmDash on your own Node.js server — using SQLite and local storage — the sandbox disappears. Plugins execute in the same process as the CMS core. The very architecture that makes EmDash secure on Cloudflare is absent. The common advice — “just use EmDash, it’s secure by design” — is dangerously incomplete for anyone running their own instance.
This article is for you if you’re self-hosting. You’ll learn exactly what security settings actually work, which ones don’t, and how to protect your instance when Cloudflare’s sandbox is off the table.
The Sandbox Myth: Why Self-Hosted EmDash Is Not Automatically Secure
Cloudflare’s dynamic workers are the engine behind plugin isolation. They spin up a fresh V8 isolate for each plugin execution, enforce capability manifests, and tear down the environment after use. This is a genuine architectural improvement over WordPress’s monolithic plugin model.
But dynamic workers require the Cloudflare Workers Paid Plan ($5/month). On the free Cloudflare tier, plugins run in-process — no sandbox. And on a self-hosted Node.js server, dynamic workers don’t exist at all. The code runs directly in Node.js, with full access to the filesystem, the database connection, and any other resource available to the process.
This isn’t a bug. It’s a design trade-off. EmDash is MIT-licensed and deployable anywhere, but the security model that makes it special is tightly coupled to Cloudflare’s proprietary runtime. If you self-host, you inherit the same risks that plague WordPress plugins: one compromised plugin can read, write, or delete everything.
The common advice — “EmDash is secure because plugins are sandboxed” — applies only if you deploy to Cloudflare and pay for the plan. For self-hosters, that advice is misleading.
Step-by-Step: Securing Your Self-Hosted EmDash Instance
Below are the concrete steps you need to take. Each step includes the expected outcome and a note on what it does not solve.
Step 1: Enforce HTTPS with a Reverse Proxy
What to do: Place Nginx or Caddy in front of your EmDash instance. Configure TLS certificates (use Let’s Encrypt via Certbot or Caddy’s automatic HTTPS). Redirect all HTTP traffic to HTTPS.
Expected outcome: All traffic between the browser and your server is encrypted. Passkeys, API tokens, and admin sessions cannot be intercepted in transit.
Why this matters on self-hosted: EmDash does not include built-in TLS termination. Without a reverse proxy, your admin panel communicates over plain HTTP. Attackers on the same network can sniff credentials.
What it doesn’t fix: It doesn’t isolate plugins or protect against server-side vulnerabilities.
Step 2: Create a Dedicated Database User with Limited Privileges
What to do: When setting up EmDash with SQLite, the database file is owned by the Node.js process. If you want to use a separate database server (PostgreSQL via Turso or similar), create a user that can only read/write the specific tables EmDash needs. Avoid using the database superuser.
Expected outcome: If a plugin or an attacker gains control of the Node process, they cannot drop other databases or modify system tables.
Why this matters on self-hosted: Without dynamic workers, any plugin can execute arbitrary SQL. Limiting database user permissions is your last line of defense.
What it doesn’t fix: The plugin can still read all content and media stored in the database.
Step 3: Enable Passkey Authentication (and Keep a Backup)
What to do: During initial setup, EmDash prompts you to create a passkey. Use a password manager (1Password, Bitwarden) to store the passkey. Do not rely on the browser’s local storage only. Also, note that passkeys are device-bound; if you lose the device, you lose access.
Expected outcome: No passwords to leak. Brute-force attacks are impossible because there is no password field.
Why this matters on self-hosted: Passkey authentication works exactly the same on self-hosted as on Cloudflare. It’s one of the few security features that doesn’t depend on the runtime.
What it doesn’t fix: Passkeys protect authentication, not authorization. A plugin with “read content” capability (or no sandbox at all) can still read every post.
Step 4: Vet Plugins Manually — No Marketplace Safety Net
What to do: Before installing any third-party plugin, read its source code. Look for database calls, file writes, external network requests, or eval() usage. Only install plugins from trusted authors with open repositories.
Expected outcome: You reduce the risk of installing malicious or poorly coded plugins.
Why this matters on self-hosted: On Cloudflare, even a malicious plugin is contained by the sandbox. On self-hosted, there is no containment. You are responsible for every line of code you add.
What it doesn’t fix: Zero-day vulnerabilities in trusted plugins still exist. You cannot patch them until the maintainer updates the code.
Step 5: Store Secrets in Environment Variables, Not the Database
What to do: Use a .envcodecodecodecodecode file or your hosting provider’s secret store for API keys, SMTP credentials, and database connection strings. Reference them in your EmDash configuration (e.g., import.meta.envcodecodecodecodecode). Never hardcode secrets.
Expected outcome: If the database is compromised, secrets remain inaccessible.
Why this matters on self-hosted: EmDash stores configuration in the database by default. A plugin that reads the database can see SMTP passwords and API tokens. Environment variables are outside the database.
What it doesn’t fix: The Node process can still read environment variables. A plugin with full process access can still steal them.
Step 6: Schedule Regular Backups and Monitor Logs
What to do: Back up the SQLite database file and the media directory daily. Use a cron job or a backup service. Monitor system logs for unexpected file changes or failed authentication attempts.
Expected outcome: If an incident occurs, you can restore from a known clean state. Logs help you detect breaches early.
Why this matters on self-hosted: There is no automatic rollback or version history for the database (EmDash does track content revisions, but not schema changes). You own the recovery process.
What it doesn’t fix: Backups don’t prevent attacks. They only reduce downtime and data loss.
Security Features: Cloudflare Deploy vs. Self-Hosted
| Feature | Cloudflare (Paid Plan) | Self-Hosted (Node.js) |
|---|---|---|
| Plugin sandbox (V8 isolate) | Yes, every plugin runs in its own isolated worker | No, plugins run in-process with full access |
| Capability manifest enforcement | Enforced by dynamic worker runtime | Not enforced — manifest is advisory only |
| Database access control | Plugin must declare read contentcodecodecodecodecode or write contentcodecodecodecodecode |
Plugin can execute any SQL via the Node.js database adapter |
| File system access | Plugin cannot access local storage unless granted (R2) | Plugin can read/write any file the Node process can access |
| Network egress control | Plugin must declare email sendcodecodecodecodecode or fetchcodecodecodecodecode |
Plugin can make unrestricted network calls |
| Authentication | Passkey + optional password (WebAuthn) | Passkey only (same as Cloudflare, no dependency) |
| TLS termination | Built-in via Cloudflare edge | Requires reverse proxy (Nginx, Caddy) |
| Backup/recovery | D1 backups via Cloudflare dashboard | Manual backup of SQLite file and media directory |
| Cost | $5/month + usage-based charges (unpredictable) | Server cost (fixed) + no Cloudflare fees, but no sandbox |
The table makes the trade-off clear: self-hosting saves you from usage-based billing surprises but strips away every security feature that makes EmDash architecturally superior to WordPress.
When Self-Hosted Security Fails: The Billing Trap Reversed
Common advice also warns about Cloudflare’s unpredictable billing — a DDoS attack could rack up thousands in worker invocations. Self-hosting avoids that risk. You pay a flat server fee and your bill stays the same regardless of traffic.
But the security cost is real. Without sandboxed plugins, you are back to the WordPress model of trusting every plugin with full system access. The difference is that WordPress has 60,000 plugins and a decade of community vetting. EmDash has zero production plugins and a two-month-old codebase.
If you self-host, you are betting that no plugin you install contains a vulnerability. History suggests that’s a bad bet.
Final Recommendation: Who Should Self-Host and Who Shouldn’t
Self-hosting EmDash makes sense only if you are an experienced developer who can audit every plugin’s source code, maintain strict server hardening, and accept the risk. For anyone else — bloggers, small businesses, agencies — the right move is to deploy on Cloudflare’s paid plan. The $5/month is cheap insurance against the most common attack vector in content management.
EmDash’s security model is not a lie. It is a real innovation. But it is also a lock-in. The same architecture that protects you on Cloudflare leaves you exposed when you walk away.
If you must self-host, use the six steps above. They are not a replacement for sandboxing. They are a damage-control kit for a CMS that was never designed to run alone.
- Why is EmDash not automatically secure when self-hosted?EmDash's security relies on Cloudflare's dynamic workers for plugin sandboxing. On a self-hosted Node.js server, plugins run in the same process as the CMS core, with full access to filesystem and database.
- What is the first step to secure a self-hosted EmDash instance?Enforce HTTPS by placing Nginx or Caddy as a reverse proxy with TLS certificates from Let's Encrypt. This encrypts all traffic between the browser and server.