LinkStack — an open-source, self-hosted Linktree alternative — packaged as an OpenHost app with one-click owner SSO and public link-page passthrough.
- The full LinkStack admin panel, auto-logged-in for the OpenHost instance owner. No LinkStack username/password to manage.
- Public link pages reachable by anyone without OpenHost login — exactly what a
Linktree-style page is for. The shareable URL is
/p/<handle>(e.g./p/admin), plus vCards (/vcard/...), themed pages, and outbound click links (/going/...). See "Public-page passthrough" below for why the site root/is deliberately gated and/@handleis not the public URL here. - SQLite storage, persisted across restarts in the app's OpenHost data dir.
This app uses an adapted trusted-header SSO pattern (Pattern A).
- When the OpenHost router verifies the owner's
zone_authcookie it stamps the upstream request withX-OpenHost-Is-Owner: true. - An in-container Python auth-proxy (
auth_proxy.py, listening on the OpenHost-routed:8080) sanitises that header — stripping any client-supplied copy and re-adding it only when the router set it — then forwards to Apache/LinkStack on:8081. It also rewritesHostfromX-Forwarded-Hostand forcesX-Forwarded-Proto: https. - A LinkStack-native middleware (
app/Http/Middleware/OpenHostSso.php) reads the trusted header and, on an owner navigation to an admin/studio path with no existing session, logs the visitor in as the seededadminuser via Laravel'sAuth::login()— the same call LinkStack's own installer/skiproute makes.start.shregisters this middleware in the kernel's$middlewarePriorityahead ofAuthenticate, so the auto-login runs before theauthgate evaluates the session.
No password is ever generated, transmitted, or written to disk. The owner header alone drives the auto-login, so nothing in the app's data dir is a usable credential. Anonymous visitors (no owner header) are never auto-logged-in, which is what keeps public link pages working.
LinkStack's AdminSeeder ships the admin with the well-known default password
12345678, and the /login form is publicly reachable. To close that hole,
start.sh rotates every user's password to a fresh random value (bcrypt hash
only, never plaintext) on each boot and disables self-registration. The owner
never needs the password — SSO is the only login path.
A user's public link page is served at /p/<handle> (e.g. /p/admin).
LinkStack normally serves pages at /@<handle>, but the OpenHost router's
public-path matcher only matches slash-terminated prefixes — it cannot match
/@handle from a /@ entry. start.sh therefore sets LinkStack's
custom_url_prefix to p/ so the canonical public URL (/p/<handle>) lives
under a matchable prefix. The site root / is intentionally NOT public: listing
/ in public_paths would make the entire app (including /dashboard) public
and stop the router from ever stamping the owner header, breaking SSO.
The public-share path prefixes are declared in two places that must stay in sync:
routing.public_pathsinopenhost.toml(the OpenHost router allow-list).PUBLIC_PREFIXESinapp/Http/Middleware/OpenHostSso.php(so the owner is not auto-logged-in while viewing a public page).
start.sh relocates LinkStack's writable state — database/ (SQLite),
storage/, config/, uploaded themes/, and assets/img/ (avatars and
backgrounds) — plus .env into $OPENHOST_APP_DATA_DIR and symlinks them back
into /htdocs. First boot generates an APP_KEY, configures SQLite, runs the
migrations + seeders, and marks the install complete via its own
.openhost_installed marker (independent of LinkStack's shipped ISINSTALLED).
Apache runs as the unprivileged apache user on port 8081 (it cannot bind
:80/:443); the SSL vhost is disabled since OpenHost terminates TLS upstream.
Dockerfile base off linkstackorg/linkstack + python3
openhost.toml OpenHost manifest
start.sh first-boot install + supervisor
auth_proxy.py SSO sidecar on :8080
files/OpenHostSso.php LinkStack auto-login middleware
LinkStack is AGPL-3.0. This packaging is provided under the same terms; see the upstream project for details.