Stefan Lazic 94aa1b2f26
CI / ci (push) Canceled after 0s
OPT and Improved Installation Wizzard
1. Summary of changes
Password auth → email OTP, everywhere it existed for the tenant/business app: sign-in, customer self-registration (/register), staff-invitation acceptance, and the installer's administrator step. Flow is always: email → 6-digit code emailed → code entered → session created, via one shared service (otp.service.ts) reused by every entry point.
NextChair Manager's own auth was not touched — this only affects tenant-side (business admin/staff/customer) authentication, per the spec's explicit instruction.
Installer improvements: Step 2 (Administrator) now asks only Name + Email + optional Phone, verified inline via the same OTP widget (no redirect). Step 3 (Business Details) pre-fills Business Name from the Manager-provisioned BUSINESS_NAME, shows the booking URL (NEXT_PUBLIC_APP_URL) as a read-only info box instead of asking for it, and has a distinctly-labeled "Business Website (optional)" field with copy clarifying it's not the booking URL.
Installation cannot complete with an unverified admin email — enforced server-side (finalizeOtp runs inside the same DB transaction as user/business creation), not just trusted from client state. A mid-wizard refresh still can't create duplicate admins or partial records — that property was preserved by design, not incidentally.
2. Prisma/database changes
New EmailOtp model (email as PK — one row per address, so a new request naturally invalidates the previous code): codeHash, purpose, expiresAt, attempts, verifiedAt, consumedAt, timestamps. Only a bcrypt hash of the code is ever stored.
User.passwordHash — unchanged schema (already nullable), just annotated as deprecated. No destructive migration; historical rows are untouched.
Migration: prisma/migrations/20260905190022_email_otp_authentication — adds EmailOtp only. Generated and applied against throwaway containers only; the real next-chair-mysql production database was never touched (confirmed not even running on this host during this session).
3. New environment variables
None required for production. One new variable, E2E_TEST_MODE, exists purely for the Playwright suite (set only in playwright.config.ts's webServer.env) — it unlocks a /api/test/otp-code debug endpoint and bypasses OTP rate limiting so the same seeded users can sign in dozens of times across one test run. It must never be set on a real deployment; nothing in the app sets it anywhere else.

4. Email delivery configuration
Nothing new to configure. OTP emails reuse the existing EmailProvider abstraction and EMAIL_PROVIDER env var exactly as configured today — console in dev (prints to server logs), brevo in production via the same SMTP credentials already documented. The OTP email is a new template (otp-email.ts) sent directly (bypassing the async notification/cron pipeline, since a 10-minute-expiry code can't wait for a delivery retry loop).

5. Migration considerations for existing users/installations
Existing users lose password sign-in — the only way to authenticate now is email OTP. There is no in-app fallback; this is an intentional, sharp cutover. If existing customers/staff/admins need advance notice, that's a business/communications decision outside this change.
No account data is deleted or migrated — passwordHash rows are simply never read for auth anymore.
prisma/seed.ts / scripts/ensure-super-admin.ts still set a passwordHash for seeded dev users — harmless and vestigial now, not cleaned up (flagging it rather than touching seed data silently).
Installations already completed are unaffected; only new installer runs go through the OTP admin-verification step.
6. Files changed
New:

src/lib/auth/otp.service.ts — the core requestOtp/verifyOtp/finalizeOtp service
src/lib/auth/__tests__/otp.service.integration.test.ts
src/services/notifications/templates/otp-email.ts
src/features/auth/components/otp-code-step.tsx — shared code-entry UI
src/features/auth/actions/request-sign-in-otp.ts
src/app/api/test/otp-code/route.ts — e2e-only, gated on E2E_TEST_MODE
prisma/migrations/20260905190022_email_otp_authentication/
Modified — auth core: prisma/schema.prisma, src/auth.ts, src/features/auth/schemas.ts

Modified — sign-in/register/invitations: sign-in-form.tsx, register.ts, register-form.tsx, accept-invitation.ts, accept-invitation-form.tsx, staff-invitation.service.ts

Modified — installer: installer.service.ts, schema.ts, install.ts, install-wizard.tsx, install/page.tsx, installer.service.integration.test.ts

Modified — email / e2e infra: console-email-provider.ts, playwright.config.ts, e2e/helpers.ts, and the four e2e/*.spec.ts files (dropped the now-unused password argument from every signIn() call).

7. Verification performed
npx tsc --noEmit, npm run lint, npm test (276 unit) — all clean.
npm run test:integration (207 tests, throwaway DB) — includes 11 new OTP-service tests and a rewritten installer suite; a genuine race condition in requestOtp (concurrent requests for a brand-new email double-hitting the upsert's create branch) was caught and fixed here.
Full npm run test:e2e against a fresh throwaway database: 36/37 passed. The one failure (owner-journeys "opening hours conflict" test) is a pre-existing, auth-unrelated ordering flake — appointment-count ambiguity from earlier tests' bookings — not something this change touches or introduced.
Live manual verification in-browser: existing seeded customer sign-in via OTP; new customer registration via OTP (session created, landed on Home); installer wizard end-to-end through Step 3, confirming email-verification gating, Business Name pre-population from BUSINESS_NAME, and the read-only Booking URL / distinct Business Website fields.
All throwaway Docker containers/DBs used for this work have been removed; the real production database was never touched.
2026-09-05 21:03:36 +01:00
2026-08-02 17:47:23 +01:00
2026-08-02 17:47:23 +01:00
2026-08-07 17:30:12 +01:00
2026-08-02 17:47:23 +01:00
2026-08-05 09:25:59 +01:00
2026-09-05 21:03:36 +01:00
2026-09-05 17:41:38 +01:00
2026-09-05 09:32:05 +01:00
2026-09-05 09:32:34 +01:00
2026-09-05 21:03:36 +01:00
2026-09-05 14:03:15 +01:00
2026-09-05 09:32:05 +01:00
2026-09-05 13:39:55 +01:00
2026-09-05 17:05:33 +01:00
2026-09-05 09:32:05 +01:00
2026-09-05 13:39:55 +01:00
2026-09-05 13:39:55 +01:00
2026-09-05 09:32:05 +01:00
2026-09-05 09:32:05 +01:00
2026-09-05 09:32:05 +01:00
2026-09-05 09:32:30 +01:00
2026-09-05 13:39:55 +01:00
2026-09-05 09:32:05 +01:00
2026-09-05 09:32:05 +01:00
2026-09-05 09:32:05 +01:00
2026-09-05 09:32:05 +01:00
2026-09-05 09:32:05 +01:00
2026-09-05 13:39:55 +01:00
2026-08-02 17:47:23 +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

Booking app as an alternative to Booksy

VPS deployment (multi-business)

One-time setup on a fresh VPS with Docker and the Compose plugin already installed. Run everything from the repo's checkout directory.

# 1. Clone the repo at the path you intend to keep it at — STACKS_DIR
#    below must be this exact absolute path, since containers shell
#    `docker compose` back out to the HOST's dockerd via a mounted
#    socket (see stacks/infra/compose.yaml and manager/README.md for why).
git clone <your-repo-url> /opt/nextchair
cd /opt/nextchair

# 2. Shared Docker network every stack attaches to.
docker network create next-chair-edge

# 3. Shared infrastructure: MySQL, Caddy (reverse proxy + automatic TLS),
#    Dockge (stack management UI, http://<vps-ip>:5001).
cp .env.infra.example stacks/infra/.env
# edit stacks/infra/.env — MYSQL_ROOT_PASSWORD, ACME_EMAIL, and set
# STACKS_DIR to this exact clone path, e.g. /opt/nextchair/stacks
docker compose -f stacks/infra/compose.yaml --env-file stacks/infra/.env up -d

# 4. Build the shared business image (re-run this after every code
#    update — see stacks/blake-heritage/compose.yaml's own note).
docker build -t nextchair-app:latest .

# 5. The management UI (manager/) — this is what you'll use to add
#    businesses from here on, not the by-hand steps in stacks/infra/
#    compose.yaml's header (those exist only as a fallback/reference).
cp manager/.env.example stacks/manager/.env
# edit stacks/manager/.env — MANAGER_EMAIL/PASSWORD_HASH, MANAGER_SESSION_SECRET,
# MYSQL_ROOT_PASSWORD (same value as step 3), and STACKS_DIR (same as step 3)
docker compose -f stacks/manager/compose.yaml --env-file stacks/manager/.env up -d --build

Then, per business, point its domain's DNS at the VPS and use the management UI (https://<your-manager-domain> — add a Caddy site for it the same way as any business, see stacks/infra/sites/.gitkeep) to add it: name, slug, domain — it creates the database, generates secrets, starts the container, and runs migrations for you. Finish setup by visiting https://<business-domain>/install.

To ship a code update to every business: git pull, rebuild the image (step 4), then use each business's Update button in the management UI (or docker compose up -d --force-recreate in its stacks/<slug>/ directory) to recreate it against the new image.

S
Description
Booking app as an alternative to Booksy
Readme
2.6 MiB
0 Stars 1 Watchers 0 Forks
Languages
TypeScript 89.4%
HTML 9.6%
CSS 0.3%
JavaScript 0.3%
Dockerfile 0.2%
Other 0.2%