Files
NextChair/manager
Stefan Lazic 3b07124d7e
CI / ci (push) Canceled after 0s
Edit Business flow
New files:

manager/src/lib/env-file.ts — a shared, surgical .env reader/updater. It only ever touches the specific keys you tell it to (never regenerates the file), so anything hand-edited into a business's .env since provisioning is preserved exactly.
manager/src/app/dashboard/[slug]/edit/ — the edit page, its form component, and its server action.

Modified:

provisioning.ts gained updateBusiness() alongside the existing provisionBusiness(), reusing the same Caddy-site-writing and container-recreation logic (refactored into a shared writeCaddySiteAndReload helper).
stacks.ts/compose-template.ts — extended to track a BUSINESS_NAME field (previously collected on the Add Business form but silently discarded — that gap is now fixed too, so newly created businesses get their name persisted from the start).
The detail page and dashboard list now show the business name, and the detail page has an Edit button.

Per the requirements:

Slug is read-only in the edit form (disabled input, never accepted by the server action — it's bound in from the route, not form data).
Domain uniqueness is validated against every other business (and correctly self-excludes the business being edited).
A domain change rewrites the Caddy site file, reloads Caddy, and force-recreates the container; a name-only change does neither.
No secrets (DATABASE_PASSWORD, AUTH_SECRET, etc.) are ever read into the edit UI — only name/domain/databaseName/status are exposed.
Clear success banners ("Changes saved" vs. "...the container was restarted...") and inline error display, matching the existing Add Business pattern.
2026-09-05 17:05:33 +01:00
..
2026-09-05 13:39:55 +01:00
2026-09-05 17:05:33 +01:00
2026-09-05 13:39:55 +01:00
2026-09-05 13:39:55 +01:00
2026-09-05 13:39:55 +01:00
2026-09-05 13:39:55 +01:00
2026-09-05 13:39:55 +01:00
2026-09-05 13:39:55 +01:00
2026-09-05 13:39:55 +01:00
2026-09-05 13:39:55 +01:00
2026-09-05 13:39:55 +01:00
2026-09-05 13:39:55 +01:00
2026-09-05 13:39:55 +01:00
2026-09-05 13:39:55 +01:00

NextChair Manager

Standalone platform-operator tool for provisioning and managing NextChair business deployments on the shared multi-business VPS setup (see ../stacks/). Not part of the main app — a genuinely separate Next.js app, own package.json, own build, own container.

Why it's a separate app

An earlier attempt at this exact feature was built as routes inside the main app (src/app/manager/...). It was reverted after repeated, never-fully-resolved bugs: the main app's src/proxy.ts, root layout, root page, and global SessionProvider all statically import Prisma / Auth.js / business branding — code a Manager (which has no business database of its own, by definition) kept transitively pulling in, causing crashes in "Manager mode."

This app avoids that class of bug structurally rather than by careful avoidance: it never imports anything from the main app's src/ at all. A few small pieces (bcrypt convention, the signed-cookie primitive) were copied and adapted, not imported — see src/lib/auth.ts and src/lib/session.ts.

What it does

  • Lists provisioned businesses and their running/stopped state.
  • Add Business: collects a name/slug/domain, then generates secrets, creates that business's database + user on the shared MySQL server, writes its stacks/<slug>/compose.yaml + .env + Caddy site file, reloads Caddy, starts the container, and runs its migrations. See src/lib/provisioning.ts for the exact sequence.
  • Per business: start / stop / restart / update (recreate the container against the current shared image tag — not a source rebuild) / view recent logs.
  • Does not duplicate the /install wizard — after provisioning, the operator is linked to https://<domain>/install to finish that business's own setup.

How auth works

A single dedicated platform-operator credential (MANAGER_EMAIL / MANAGER_PASSWORD_HASH, bcrypt cost 12) — not tied to any one business's own Super Administrator, since this tool spans every business's separate database and has no "primary" one of its own. A signed (HMAC, not encrypted) cookie holds the session; see src/lib/session.ts.

The Docker-socket trust boundary

This container has the host's Docker socket mounted (docker.sock) so it can run docker compose/docker exec against the host's dockerd — see src/lib/docker.ts. Root inside this container plus that socket effectively means root on the host. Consequences:

  • It must stay behind the login above at all times — never expose it without that gate, and don't reuse MANAGER_PASSWORD_HASH anywhere else.
  • STACKS_DIR must be bind-mounted at the identical absolute path on both host and container (see .env.example) — a docker compose -f <path> command run from inside this container is executed by the host's dockerd, so the path only means the same file on both sides if the mount doesn't change it. This is the same constraint Dockge itself has.

Setup

cp manager/.env.example stacks/manager/.env   # fill in real values
docker compose -f stacks/manager/compose.yaml --env-file stacks/manager/.env up -d --build

Requires stacks/infra/compose.yaml already running (shared MySQL, Caddy, the next-chair-edge network). Add a Caddy site for this app the same way as any business (see stacks/infra/sites/.gitkeep), pointed at next-chair-manager:3000.

Local development

cd manager
npm install
cp .env.example .env.local
npm run dev

Requires network access to a real MySQL server and docker/docker compose on the machine you run it from for the provisioning/stack actions to actually work — the dashboard's read-only parts (listing stacks) will also just show nothing if STACKS_DIR doesn't point at a real directory yet.