sudo ./holistic setup # system prep + all services
sudo ./holistic setup dashboard # single service
sudo ./holistic user add <name> # new user (Linux + Samba + groups)
sudo ./holistic user passwd <name> # change password (Linux + Samba)
sudo ./holistic user delete <name> # remove user
sudo ./holistic invite new # mint a dashboard registration code
sudo ./holistic update # git pull + re-run setup
./holistic list # services in install order
./holistic status # check install stateAfter holistic setup, the dashboard is at https://holistic.local. Users sign in with
their Linux account; registration needs an invite new code.
| Platform | Path |
|---|---|
| Windows | \\<server-ip>\<username> or \\<server-ip>\family |
| macOS | smb://<server-ip>/<username> or smb://<server-ip>/family |
| Linux | smb://<server-ip>/<username> (GNOME) or mount via cifs-utils (below) |
sudo apt install cifs-utils
sudo mkdir -p /mnt/holistic
sudo mount -t cifs //<server-ip>/<username> /mnt/holistic \
-o user=<username>,uid=$(id -u),gid=$(id -g),iocharset=utf8GNOME/Nautilus: gio mount smb://<server-ip>/<username> (or Files → Other Locations).
Persistent mount — keep the password in a root-only credentials file:
sudo install -m600 /dev/stdin /etc/holistic-smb.cred <<'CRED'
username=<username>
password=<your-password>
CRED
# /etc/fstab line:
# //<server-ip>/<username> /mnt/holistic cifs credentials=/etc/holistic-smb.cred,uid=1000,gid=1000,_netdev 0 0
sudo mount -aCreate services/<name>/install.sh, add <name> to services/manifest.
Runs as root, must be idempotent, must print [<name>] installed and started.
holistic is portable: it has no hardcoded hostname and runs same-origin behind Caddy on
whatever domain you point at it. The domain it is currently served on is resolved at
runtime from the (Caddy-set) forwarded headers — GET /api/instance is the single source
of truth:
origin/hostare per-request (LANholistic.local, the public tunnel domain, devlocalhost— whatever the client used). Use these for absolute links/redirects.mailDomainis the stable, canonical domain for addresses likeuser@<mailDomain>(e.g. a future email service). It is learned once from the first public access and persisted to/var/lib/holistic/instance.json; LAN/loopback/IP hosts are ignored, so it is""until a real domain is seen.
How a service consumes it:
- UI plugin: read
props.instance(InstanceInfoin@holistic/ui) — e.g.`${user.username}@${instance.mailDomain}`. IfmailDomainis"", prompt the operator to setHOLISTIC_MAIL_DOMAIN. - Service daemon: read
/var/lib/holistic/instance.json(shared state, likejwt-secret/permissions.d) or call/api/instance.
Overrides (env on the dashboard unit; empty = auto): HOLISTIC_MAIL_DOMAIN pins the
canonical mail domain — set it when you want the apex (example.com) instead of the
served host (holistic.example.com), or a different MX domain. HOLISTIC_PUBLIC_ORIGIN
pins the full scheme://host (rare).
Admin = the sudo group and can do everything. To grant non-admin users
fine-grained rights, a service declares them through a holistic-wide standard;
the privleg service then lets admins toggle them per user.
A service drops a manifest at install time to /etc/holistic/permissions.d/<id>.json
(like its Caddy and sudoers drop-ins). Each right is backed 1:1 by a Linux group
named hp_*; the service creates the groups (groupadd -f) and enforces the
right itself with isAdmin || group ∈ user.groups:
{
"service": "hostek", "version": 1,
"categories": [{
"id": "system", "label": "System",
"permissions": [{
"id": "power", "label": "Power control",
"description": "Turn the server on/off, toggle headless",
"group": "hp_hostek_power", "default": false, "dangerous": true
}]
}]
}Rules: groups must match ^hp_[a-z0-9][a-z0-9_-]{0,27}$ (the hp_ prefix is a
security boundary — privleg only ever touches hp_* groups) and each group backs
exactly one right. Pick default so a host without privleg behaves exactly as
today: default:false for an action that is admin-only now (group stays empty ⇒
admin-only; privleg grants it per user), or default:true for an action open to
everyone now (provisioning grants it to all users ⇒ unchanged; privleg revokes it
per user). Enforce with isAdmin || group ∈ user.groups either way. The TypeScript
shapes are PermissionManifest/PermissionCategory/PermissionDecl in
@holistic/ui. Validate with holistic perms validate.
The exact mirror of the rights standard, for settings instead of rights. All configuration — anything an admin sets — is bundled in the dashboard's single Configuration tab, never in a service's own tab: a service tab belongs to the user's experience and must not drown in knobs.
A service declares its settings and their defaults in a manifest dropped at install time
to /etc/holistic/config.d/<id>.json (like its permissions drop-in):
{
"service": "hosuto", "version": 1,
"categories": [{
"id": "limits", "label": "Limits",
"settings": [
{ "id": "maxServersPerUser", "label": "Servers per member", "type": "int", "default": 3 },
{ "id": "loader", "label": "Default loader", "type": "enum",
"default": "paper", "options": ["vanilla", "fabric", "paper"] }
]
}]
}An admin edits the values there; the dashboard validates them against the manifest and
writes /var/lib/holistic/config/<id>.json atomically (mode 0640, group holistic).
Every service daemon simply reads that file live — no restart, no RPC — and falls back
to its declared default for anything absent, so a host that has never been configured
behaves exactly as before.
Rules: the service id must match ^[a-z][a-z0-9]{2,19}$ and equal the file stem (it is the
same id as the plugin and the permissions manifest); setting ids match
^[a-z][a-zA-Z0-9_]*$ and are unique per service (the daemon reads them by bare id);
type is one of string, int, bool, enum, secret; an enum needs a non-empty
options[] containing its default; dangerous: true makes the editor confirm before
saving. Validate with holistic config validate.
holistic CLI entry point
services/manifest install order
services/<name>/install.sh per-service setup
services/<name>/ui/ optional dashboard UI (a @holistic/ui plugin)
frontend/ dashboard SPA + @holistic/ui SDK
{ "origin": "https://example.com", "host": "example.com", "mailDomain": "example.com" }