Stefan Lazic 8fddde6d4b
CI / ci (push) Canceled after 0s
Redirect loop fix
The redirect loop was caused by the Manager cookie configuration:
1. secure was based on NODE_ENV=production, so HTTP deployments silently dropped the cookie.
2. Path=/manager excluded /api/manager/*, preventing authenticated Manager API requests.
2026-09-04 22:48:22 +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-09-04 20:32:06 +01:00
2026-09-04 20:54:58 +01:00
2026-08-05 09:25:59 +01:00
2026-08-07 17:30:12 +01:00
2026-08-07 17:30:12 +01:00
2026-08-07 17:30:12 +01:00
2026-09-04 20:54:58 +01:00
2026-09-04 22:48:22 +01:00
2026-09-04 20:54:58 +01:00
2026-08-02 17:47:23 +01:00
2026-08-04 09:57:58 +01:00
2026-09-04 20:32:06 +01:00
2026-08-07 17:30:12 +01:00
2026-09-04 20:32:06 +01:00
2026-09-04 20:54:58 +01:00
2026-08-02 17:47:23 +01:00
2026-08-02 17:47:23 +01:00
2026-08-05 09:25:59 +01:00
2026-09-04 21:22:33 +01:00
2026-08-02 17:47:23 +01:00
2026-08-07 13:38:11 +01:00
2026-08-07 13:38:11 +01:00
2026-08-07 17:39:31 +01:00
2026-08-07 17:30:12 +01:00
2026-08-02 17:47:23 +01:00
2026-08-07 17:30:12 +01:00
2026-09-04 20:54:58 +01:00
2026-08-02 17:47:23 +01:00
2026-08-02 17:47:23 +01:00
2026-08-02 21:39:24 +01:00
2026-08-02 18:36:39 +01:00

+# NextChair

NextChair is a white-label, mobile-first booking platform for barbershops. It combines customer booking, staff scheduling, owner administration, notifications, loyalty, media, reviews, and optional Google integrations in one Next.js application.

Technology

  • Next.js 16 with the App Router and React 19
  • TypeScript 6
  • MySQL 8.0, accessed through Prisma 7 and the MariaDB driver adapter
  • Auth.js / NextAuth v5 for authentication
  • Tailwind CSS 4, Radix UI, Lucide, and Font Awesome
  • Vitest for unit and integration tests
  • Playwright for browser tests
  • Docker Compose for a production-style local deployment

The application is intentionally configured with discrete database settings: DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER, and DATABASE_PASSWORD. It does not use DATABASE_URL at runtime.

Deployment Model

The intended deployment model is one repeatable Docker deployment for both a local/self-hosted Debian server and production:

  • One Dockge-managed Caddy stack owns ports 80 and 443 and routes each domain.
  • One Dockge-managed MySQL 8 stack hosts a separate database and user per business.
  • Each business is a separate Dockge stack with its own media volume and env file.
  • Business containers publish no host ports and share only the proxy and database networks.
  • Each business is installed independently through /install after its stack starts.

deploy/business-compose.yaml.template and scripts/create-business.sh are the canonical business deployment path. The local Docker file remains useful for a single isolated development instance.

Requirements

For local Node development:

  • Node.js 22 or later
  • npm
  • A reachable MySQL 8.0 database

For the primary VPS deployment:

  • A Linux VPS with Docker Engine and Docker Compose
  • A domain per business, with DNS pointing at the VPS
  • Ports 80 and 443 available for Caddy and Let's Encrypt
  • Dockge configured to scan the directory containing the generated stacks

For local development:

  • Docker Desktop with Docker Compose

Local Docker Desktop (Isolated Business Instance)

This is a disposable local approximation of one business from the VPS deployment. It starts one MySQL container and one NextChair container, applies migrations automatically, and leaves the database empty until you either complete the installer or explicitly run the demo seed. It is not the shared multi-business topology; use the VPS setup above for the canonical deployment.

  1. Install dependencies if you also plan to run commands on the host:

    npm ci
    
  2. Create the Docker environment file:

    cp .env.local.example .env.local
    
  3. Replace the placeholder values in .env.local. At minimum, set strong values for:

    • MYSQL_PASSWORD
    • MYSQL_ROOT_PASSWORD
    • DATABASE_PASSWORD
    • AUTH_SECRET
    • INSTALLATION_TOKEN
    • CRON_SECRET

    DATABASE_NAME, DATABASE_USER, and DATABASE_PASSWORD must match the corresponding MySQL container settings. Inside Docker, DATABASE_HOST must remain next-chair-mysql, not localhost.

    Generate suitable secrets with:

    openssl rand -hex 32
    npx auth secret
    
  4. Build and start the stack:

    docker compose -p next-chair -f docker-compose.local.yaml --env-file .env.local up --build -d
    
  5. Open http://localhost:3000. If the database has not been installed, the application redirects to /install. Enter the INSTALLATION_TOKEN and complete the wizard to create the first business and owner account.

  6. Follow the application logs when needed:

    docker compose -p next-chair -f docker-compose.local.yaml --env-file .env.local logs -f next-chair
    

The application health endpoint is http://localhost:3000/api/health. It returns HTTP 200 when the database is reachable, even before the installation wizard has been completed.

Docker lifecycle commands

# Stop containers but keep database and media volumes
docker compose -p next-chair -f docker-compose.local.yaml --env-file .env.local down

# Start again without rebuilding
docker compose -p next-chair -f docker-compose.local.yaml --env-file .env.local up -d

# Run the development/demo fixture explicitly
docker compose -p next-chair -f docker-compose.local.yaml --env-file .env.local run --rm next-chair-seed

# Remove containers and all local database/media data
docker compose -p next-chair -f docker-compose.local.yaml --env-file .env.local down -v

The seed service is opt-in. Normal up never seeds the database. down -v is destructive for this local deployment: it removes the named MySQL and media volumes.

Local Node Development

Use this path when you already have MySQL running outside Docker or want fast Next.js source reloads.

  1. Install dependencies:

    npm ci
    
  2. Create .env.local from .env.example and configure a local MySQL 8 database. Set DATABASE_HOST=localhost and make sure the database and user already exist.

  3. Generate the Prisma client and apply development migrations:

    npm run prisma:generate
    npm run prisma:migrate
    
  4. Start the development server:

    npm run dev
    

    Open http://localhost:3000.

  5. Choose one database bootstrap path:

    • For a real, empty installation, do not seed. Use /install with INSTALLATION_TOKEN.

    • For development fixtures, run:

      npm run prisma:seed
      

      The seed is idempotent and creates demo business, staff, services, customers, appointments, and related records. It marks the database as installed so the application can be used immediately. Do not run it alongside a real first-run installation.

The direct Node path and the local Docker path should use different .env.local values: Docker connects to next-chair-mysql, while a host-run Next.js process normally connects to localhost. Business stacks use DATABASE_HOST=nextchair-mysql on the shared database network.

Environment Configuration

The available templates are:

  • .env.example: host-run development configuration
  • .env.local.example: Docker Desktop configuration
  • .env.infra.example: shared MySQL root settings
  • stacks/caddy/.env.example: shared Caddy ACME email
  • .env.business.example: generated business environment reference

Required for a basic running installation:

  • MySQL settings: DATABASE_*
  • AUTH_SECRET
  • NEXT_PUBLIC_APP_URL
  • INSTALLATION_TOKEN
  • CRON_SECRET
  • AUTH_TRUST_HOST=true when serving through a non-localhost host or reverse proxy

Optional integrations are disabled safely when left unconfigured:

  • Google sign-in: AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET
  • Google Calendar sync: GOOGLE_CALENDAR_*, CALENDAR_TOKEN_ENCRYPTION_KEY
  • Google Reviews: GOOGLE_PLACES_API_KEY
  • SMTP email: set EMAIL_PROVIDER=brevo and provide the EMAIL_SMTP_* and EMAIL_FROM_ADDRESS values. The default console provider logs notifications instead of sending them.
  • Web Push: set PUSH_PROVIDER=web-push and provide the VAPID keys. Generate them with npx web-push generate-vapid-keys.
  • Platform super administrator: SUPER_ADMIN_EMAIL, SUPER_ADMIN_PASSWORD, SUPER_ADMIN_FIRST_NAME, and SUPER_ADMIN_LAST_NAME

Do not commit .env.local, production environment files, OAuth secrets, SMTP credentials, VAPID private keys, or encryption keys.

Application Flow

The application has a deliberate first-run gate:

  1. A fresh database is migrated by Prisma.
  2. Requests are redirected to /install, except for the installer and required static/PWA assets.
  3. The installer validates INSTALLATION_TOKEN, then creates the business, primary location, settings, hours, booking rules, loyalty configuration, starter services, owner role, and first administrator in one transaction.
  4. After installation, /staff and /owner require authentication and permissions. Customer browsing and booking routes remain available according to the business booking rules.

The installation token is checked server-side and rate-limited. It is never stored in the database. The installer is one-way for a deployment: to start over locally, remove the local database volume or use a separate database rather than attempting to reinstall over existing data.

Database and Prisma

Useful commands:

npm run prisma:generate   # Generate the client in generated/prisma
npm run prisma:migrate    # Create/apply a development migration
npm run prisma:studio     # Open Prisma Studio
npm run prisma:seed       # Load idempotent development fixtures

For a deployed database, the container entrypoint runs:

npx prisma migrate deploy
npx tsx scripts/ensure-super-admin.ts

Migrations are applied on every application container start and are not destructive. The optional seed is separate because demo data is not an appropriate production bootstrap mechanism.

Quality Checks and Tests

Run the fast checks first:

npm run typecheck
npm run lint
npm test

Additional commands:

npm run test:watch
npm run test:integration
npm run test:e2e
npm run build

Unit tests do not require a database. Integration tests require a disposable MySQL 8 database configured through the DATABASE_* variables. They apply migrations but the caller is responsible for providing the database.

The Playwright suite starts a development server on port 3100, applies migrations, loads the demo seed, and provisions test credentials. Point it at a throwaway database, never a database containing real business data:

DATABASE_HOST=localhost \
DATABASE_PORT=3306 \
DATABASE_NAME=nextchair_e2e \
DATABASE_USER=nextchair \
DATABASE_PASSWORD='replace-me' \
npm run test:e2e

Playwright uses one Chromium project, one worker, a 45-second test timeout, screenshots on failure, and retains traces on failure. The database should be reset or recreated between isolated test runs when fixtures from previous runs could affect results.

Docker Server Deployment

This is the same topology for a Debian self-hosted server and a production server. The only intentional deployment difference is the image tag: build nextchair:latest locally, or use a registry tag such as registry.example.com/nextchair:2026-09-04 in production.

1. Prepare the server

Copy this repository's deployment files to a stable directory, for example /opt/nextchair, and configure Dockge to use /opt/nextchair/stacks as its stack directory. The tracked infra and caddy directories are shared stacks; every generated business directory sits beside them.

Build or pull the application image once:

# Self-hosted/local image
docker build -t nextchair:latest .

# Production image example
docker pull registry.example.com/nextchair:2026-09-04

Create the shared networks and start the two infrastructure stacks:

cp .env.infra.example .env.infra
cp stacks/caddy/.env.example stacks/caddy/.env
# Edit both env files before starting them.
docker network create nextchair-db
docker network create nextchair-proxy
docker compose -f stacks/infra/compose.yaml up -d
docker compose -f stacks/caddy/compose.yaml up -d

Start the Manager stack as well, after copying its environment template and setting a strong platform-admin password and auth secret:

cp stacks/manager/.env.example stacks/manager/.env
docker compose -f stacks/manager/compose.yaml up -d

Add a Manager-only Caddy route in stacks/caddy/sites/manager.caddy, replacing the example hostname with the private/admin hostname you control:

manager.example.com {
    reverse_proxy nextchair-manager:3000
}

Reload Caddy after adding the route, then open /manager. The Manager has its own administrator session and does not use any business customer, staff, or owner account.

The MySQL port is loopback-only by default. Use an SSH tunnel for database administration rather than opening port 3306 publicly.

2. Provision a business

Run this after MySQL is healthy. The script creates the database and least scope database user, generates secrets, writes a Dockge-ready stack directory, and writes one Caddy site file:

NEXTCHAIR_STACK_ROOT=/opt/nextchair/stacks \
NEXTCHAIR_CADDY_SITES_DIR=/opt/nextchair/stacks/caddy/sites \
./scripts/create-business.sh blake-heritage blakeheritage.com nextchair:latest

For production, pass the exact image tag to use. The script never overwrites an existing business slug. In Dockge, the new directory appears as its own stack, so start, stop, restart, recreate, update, and logs are scoped to that business. The generated .env is mode 600 and is ignored by git.

After DNS points at the server, start the generated stack from Dockge. Caddy will obtain the certificate and route the domain to the app container over the private nextchair-proxy network. Reload Caddy after adding or removing a route:

docker compose -f stacks/caddy/compose.yaml exec nextchair-caddy caddy reload --config /etc/caddy/Caddyfile

Repeat the provisioning command for each business. A business's database, media volume, env file, app container, and Caddy route are all named from its slug. The application migration entrypoint runs on every start, while the first-run business setup remains the app-level /install flow.

Repository Layout

src/app/                 Next.js routes, layouts, and API handlers
src/features/            Feature workflows, server actions, schemas, components
src/services/            Domain services and external-provider integrations
src/lib/                  Shared infrastructure, auth, database, security, and utilities
prisma/                  Schema, migrations, and development seed
e2e/                     Playwright browser journeys and test setup
docker/                  Container entrypoint
docs/                    Phase implementation notes and historical decisions
deploy/                   Reusable business stack template
stacks/                   Dockge stacks: infra, caddy, and businesses
scripts/                  Database/business provisioning utilities
templates/               UI reference templates

The phase documents in docs/ contain implementation history and deeper design rationale. They are useful when changing installation, scheduling, notification, calendar, media, or deployment behavior, but this README is the intended starting point for running the project.

Common Troubleshooting

The app redirects to /install. This is expected for a fresh or unseeded database. Set INSTALLATION_TOKEN and complete the wizard, or explicitly run the development seed.

The app cannot connect to MySQL in the local stack. Use DATABASE_HOST=next-chair-mysql, not localhost, and ensure the local credentials match the local MySQL container.

The app cannot connect to MySQL in a business stack. Use DATABASE_HOST=nextchair-mysql, confirm nextchair-mysql is attached to nextchair-db, and confirm the generated DATABASE_NAME, DATABASE_USER, and DATABASE_PASSWORD match the database created by scripts/create-business.sh.

The app cannot connect to MySQL when run with npm run dev. Use DATABASE_HOST=localhost or the hostname of your external MySQL server, and verify that the database and user already exist.

Port 3000 is already in use. Set APP_HOST_PORT in .env.local for Docker, or start Next.js with npm run dev -- --port 3001 for host development. Update NEXT_PUBLIC_APP_URL when callback URLs need to match the new port.

Emails or push notifications are not delivered. The default providers are console-only. Configure the relevant provider and credentials before expecting external delivery.

A reset is needed during local development. Use a separate database or docker compose ... down -v for the local Docker stack. This removes local database and media data and should never be used against production volumes.

License

The package currently declares the ISC license in package.json. Confirm the intended distribution license before publishing the application or its assets.

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%