# Systemd user unit template for a hosted prototype surface

One unit per hosted surface: `${HOME}/.config/systemd/user/${BRAND}-prototype-<surface>.service`.

Two flavours: dev-server mode (frameworks with hot-reload) and static file-server mode (raw HTML or built `dist/`).

## Dev-server mode (vite, next, sveltekit, astro)

```ini
[Unit]
Description=${BRAND} prototype surface <surface> — <framework> dev server
After=network.target

[Service]
Type=simple
WorkingDirectory=<surface-dir>
Environment="PORT=<port>"
Environment="NODE_ENV=development"
ExecStart=/usr/bin/env npm run dev -- <framework-port-flag> <port> --host 127.0.0.1
Restart=on-failure
RestartSec=3
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=default.target
```

`<framework-port-flag>` is the per-framework override from [`scaffold-frameworks.md`](scaffold-frameworks.md): `--port` for Vite/SvelteKit/Astro, `-p` for Next.

## Static file-server mode

For the `static` framework (raw HTML rendered from `LANDING.md`):

```ini
[Unit]
Description=${BRAND} prototype surface <surface> — static file server
After=network.target

[Service]
Type=simple
WorkingDirectory=<surface-dir>
ExecStart=/usr/local/bin/cloudflared file-server --port <port> --address 127.0.0.1 <surface-dir>
Restart=on-failure
RestartSec=3
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=default.target
```

If `<surface-dir>/dist/` exists (the founder added a build step later), point `ExecStart`'s last arg at `<surface-dir>/dist` instead of `<surface-dir>`. The skill detects this automatically with `[ -d <surface-dir>/dist ] && SERVE_DIR=<surface-dir>/dist || SERVE_DIR=<surface-dir>`.

## Why a user unit, not a root unit

The brand cloudflared connector already runs as a `systemctl --user` unit ([`platform/plugins/cloudflare/references/reset-guide.md`](../../../../../platform/plugins/cloudflare/references/reset-guide.md)). Keeping prototype units in the same scope means:

- Same `journalctl --user` namespace — the operator reads one log namespace, not two.
- No `sudo` required to install, enable, or restart.
- `loginctl enable-linger <user>` (already set by the installer) survives reboot — units come back without an SSH session.

Root units would also work, but split the operator's mental model and require `sudo` for every restart. Not worth the tradeoff for a per-prototype surface.

## Enabling and starting

```bash
systemctl --user daemon-reload
systemctl --user enable --now "${BRAND}-prototype-<surface>.service"

# Verify it stayed up.
systemctl --user is-active "${BRAND}-prototype-<surface>.service"

# If not active, dump the journal and surface verbatim.
if [ "$(systemctl --user is-active "${BRAND}-prototype-<surface>.service")" != "active" ]; then
  journalctl --user -u "${BRAND}-prototype-<surface>.service" -n 50 --no-pager >&2
  exit 1
fi
```

## Re-run after edit

If the skill regenerates the unit file (e.g. the port changed, or the static surface gained a `dist/` directory), the sequence is:

```bash
systemctl --user daemon-reload
systemctl --user restart "${BRAND}-prototype-<surface>.service"
```

`daemon-reload` is required after any unit-file edit; `restart` picks up the new `ExecStart`.

## Why `Restart=on-failure`, not `always`

Dev servers exit 0 in legitimate cases (the operator killed `npm run dev` from another shell to apply a config edit). `on-failure` respects those exits; `always` would fight them.

## Logging

All output (stdout + stderr from the dev server / file server) goes to the user journal. Read with:

```bash
journalctl --user -u "${BRAND}-prototype-<surface>.service" -f
```

No file-based log rotation needed — systemd handles journal rotation.
