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. Seesrc/lib/provisioning.tsfor 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
/installwizard — after provisioning, the operator is linked tohttps://<domain>/installto 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_HASHanywhere else. STACKS_DIRmust be bind-mounted at the identical absolute path on both host and container (see.env.example) — adocker 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.