Djed Local Development
Park your application directory, then open the project over trusted HTTPS:
djed park ~/Sitesdjed open blog# https://blog.testdjed gives every Maat application a real hostname while you
develop it: https://<name>.test, trusted by Safari and Chrome, so there
is no http://localhost:8000 and no certificate warning to click
through. It starts your app on its first request, restarts it when the
code changes, and stops it once it has sat idle for a while — the shape
of Laravel Valet and Herd, written in pure Dart with nothing from
Homebrew to install.
djed is a standalone package with its own djed executable and no
dependency on the rest of Maat. It works with any application shaped
like the ones maat new scaffolds: a bin/server.dart that honours
APP_HOST and APP_PORT.
Install
djed supports macOS only in this version.
dart compile exe packages/djed_dev/bin/djed.dart -o ~/bin/djeddjed installCompile it rather than dart pub global activate: the daemon is launched
by launchd from the plist install writes, and a path-activated package
resolves its dependencies on every single invocation — as root, in your
repository. A self-contained binary has no pub cache, no
package_config.json and no SDK path to keep valid, and every djed
command returns in milliseconds instead of seconds. Put it anywhere on
your PATH; install records wherever it actually is.
Trust and TLS
install needs sudo three times, and announces each step before it
prompts you:
- writing
/etc/resolver/test, which tells macOS to resolve*.testto your machine; - trusting
djed’s local certificate authority in the System keychain; - installing
/Library/LaunchDaemons/com.maat.djed.plistand loading it, so the daemon comes back after a reboot without you doing anything.
uninstall reverses all three. Re-running install boots the daemon out
of launchd before loading the new job, so every application it was
running stops with it — harmless, but worth knowing before you re-run it
mid-afternoon.
If Herd or Valet is already running, install refuses and tells you to
run herd stop or valet stop first: both already hold ports 80 and
443, and djed needs the same two.
Once installed, point it at your applications:
djed park ~/Sites # every child directory with bin/server.dart becomes <dir>.testdjed link blog # or serve one directory under a name you choosedjed statusCommands
| Command | Does |
|---|---|
install / uninstall [--purge] |
Set up or remove DNS, the certificate authority and the LaunchDaemon. --purge also deletes configuration and logs. |
start / stop / restart [site] |
Start or stop the daemon. restart alone restarts the daemon; restart <site> restarts just that app. |
status |
The daemon’s pid, its ports, a live DNS check, certificate expiry, and every running app with its port and idle time. |
park [dir] / forget [dir] |
Add or remove a parked directory. |
link [name] [dir] / unlink [name] / links |
Serve, stop serving, or list a single application under a chosen name. |
log [site] [-f] |
Show the daemon log, or one site’s log. -f follows it. |
open [site] |
Open a site in your default browser. |
secure [--restart] |
Regenerate the certificate — after adding a site, or before it expires — and re-trust it. --restart also restarts the daemon. |
daemon |
Run the daemon in the foreground. This is what launchd runs, as root. |
Root, and what still belongs to you
That third step installs a LaunchDaemon, not a per-user LaunchAgent,
and a LaunchDaemon runs as root. It has to: on macOS only root may bind a
port below 1024, and https://blog.test — no port number, no :8443 —
is the whole reason this tool exists. Laravel Valet and Herd take exactly
the same route for exactly the same reason.
Your applications are not root, and neither is anything they touch. The
plist carries your username in DJED_USER, and the daemon uses it to
give everything back:
~/.config/djed— its configuration, certificates and logs — is chowned to you the moment the daemon starts, so you can read and edit it withoutsudo. Which is also why the daemon does not believe all of it: a root process reading a file you own is reading untrusted input. It ignoresopensslentirely (it uses the path recorded in the root-owned plist, or the one onPATH), and it refuses any port below 1024 other thanhttpPort: 80andhttpsPort: 443— checked both when it starts and on every reload, sodjed linkcannot move it onto:22either. Everything else inconfig.json— your parked paths, links, tld, idle timeout and any port at or above 1024 — it acts on as written, because none of that needs root to do.- The control socket the
djedcommand talks to is yours as well, mode 0600: an unprivileged CLI can still drive a root daemon, and nobody else on the machine can. - Every application starts as
sudo -u <you> -H dart run bin/server.dart, so your project files, SQLite databases,.dart_toolcaches and pub cache stay owned by you and never become root’s.
One thing does not come back: because launchd starts the daemon in the
system bootstrap rather than your login session, your applications have
no access to your login keychain and are never granted the per-app
privacy permissions a GUI app would ask for — no TCC prompt for
Documents, Desktop or Downloads, just a denial. Valet’s apps run the same
way, for the same reason. Anything that needs a login-session credential
should read it from the project’s .env instead.
Behind Another Proxy
djed can sit behind nginx instead of owning ports 80 and 443 itself —
useful when Herd or Valet already has them:
herd proxy todo http://127.0.0.1:18080 --securewith httpPort set to 18080 in config.json. nginx terminates TLS and
serves the hostname; djed keeps starting the app on demand, stopping
it when idle and restarting it on a change.
Set "trustForwardedHeaders": true in the same file. djed’s own
listener is plain HTTP there, so without it every request reaches the
application declaring X-Forwarded-Proto: http and each absolute URL the
application builds — a pagination link, a redirect — sends an HTTPS client
back to HTTP. It is off by default, and that default is the safe one: when
djed owns 80 and 443 any client can set the header, and believing it
would let a plaintext request present itself to the app as secure.
One thing to know if you write the nginx config yourself. The standard WebSocket recipe is
map $http_upgrade $connection_upgrade { default upgrade; '' close; }proxy_set_header Connection $connection_upgrade;and the '' close; line is not optional. Without it — Herd’s generated
config is one example — nginx sends Connection: upgrade on every
request, and dart:io stops delivering a request body the moment it sees
that header, whatever the request is upgrading to. The bytes stay unread
in the socket while contentLength still promises them, so every form
submission arrives empty. djed handles this: a request marked for
upgrade is tunnelled rather than re-issued, which is the only path that
reaches those bytes. Any other Dart server behind such a config will
silently lose POST bodies.
How it works
- DNS. A UDP responder on
127.0.0.1:53535answers*.testlookups —Arecords with127.0.0.1,AAAAwith::1./etc/resolver/test, written byinstall, is what tells macOS to send.testlookups there instead of the internet. - TLS.
djedkeeps its own certificate authority and trusts it once, in your System keychain. There is no single*.testwildcard leaf: curl, browsers and Dart’s own TLS stack all refuse a wildcard pattern with fewer than two dots, and a single-label tld liketestcan never have two. So the certificate lists every registered site by name instead —blog.testand*.blog.test, one pair per site — alongsidetest,localhostand127.0.0.1.djedregenerates it, and reloads HTTPS with the new one, the moment the site list changes. - Proxy. HTTP on port 80 and HTTPS on port 443 route each request by
its
Hostheader to the matching app’s port, tunnel WebSocket upgrades, and addX-Forwarded-For,X-Forwarded-ProtoandX-Forwarded-Host. - Apps. Each site runs as
dart run bin/server.dart— spelled with the absolute path todart, because launchd hands the daemon a bare/usr/bin:/bin:/usr/sbin:/sbinPATH on which a plaindartdoes not resolve — withAPP_HOST,APP_PORTandAPP_URLset for it. When the daemon is root, that whole command goes throughsudo -u <you> -H. Its log lands in~/.config/djed/logs/<site>.log. An app idle for 15 minutes is stopped; saving a change underlib/,routes/,config/,bootstrap/orpubspec.yamlrestarts it a moment later.
Trying it with the todo example
The repository ships a small reference application at examples/todo.
Copy it into a parked directory, then set it up:
mkdir -p ~/Sitescp -r examples/todo ~/Sites/todocd ~/Sites/todocp .env.example .envdart run bin/maat.dart migratedjed park ~/Sitesopen https://todo.testThe task list loads over HTTPS with no warning to dismiss. Edit
~/Sites/todo/routes/web.dart — a comment is enough — and reload the
page a few seconds later: djed noticed the change, restarted the
app, and the new version is already the one answering.
Limitations
- macOS only. Linux (
systemd,resolved) and Windows support are on the roadmap. - One wildcard label per site.
api.blog.testis covered byblog’s certificate;a.b.blog.teststill routes to theblogsite, but no certificate covers it, so a browser will refuse the connection. - Maat-shaped apps only. A site needs a
bin/server.dartthat honoursAPP_PORT. Custom start commands for other kinds of applications are a follow-up. - No GUI. Everything goes through the
djedcommand.
Verifying on your machine
install and uninstall change state outside this repository — DNS
resolution, the System keychain and /Library/LaunchDaemons — so they
are worth running by hand, watching each sudo prompt, rather than from
an unattended script. Before you do, stop Herd or Valet: both run an
nginx that already holds ports 80 and 443, the same two djed needs,
and install refuses to start while either does.
herd stop # or: valet stopdart compile exe packages/djed_dev/bin/djed.dart -o ~/bin/djeddjed installmkdir -p ~/Sitescp -r examples/todo ~/Sites/todocd ~/Sites/todo && cp .env.example .env && dart run bin/maat.dart migratedjed park ~/Sitesdjed statuscurl -sS https://todo.test/ | head -c 200Then edit ~/Sites/todo/routes/web.dart (a comment is enough), wait a
few seconds, and run the curl and djed status commands again — the
response is unchanged but status now shows a new pid for todo,
because the file watcher restarted it. When you are finished:
djed uninstall --purgeherd start # or: valet startuninstall reverses what install did: it boots the daemon out of
launchd’s system domain and deletes
/Library/LaunchDaemons/com.maat.djed.plist, restores
/etc/resolver/test to whatever Herd or Valet left there (or deletes it
if there was nothing), and untrusts the certificate authority. --purge
also deletes ~/.config/djed, including its logs.