Skip to main content

Nexgen V3 local development

The local development environment runs the app services in Docker containers and the two databases directly on the Mac. MySQL and MongoDB are installed with Homebrew and are no longer run in Docker. Several worktrees of the repo can then share one pair of database servers, and the Docker VM stays small.

This page covers the paid app (nexgen) and the trial app (nexgen-free), which share one backend and one frontend codebase.

The setup here is the dev environment, which belongs to the main checkout. To give a git worktree an environment of its own, with separate databases, containers, and ports, see Running multiple local environments. That page builds on this one, so finish this setup first.

How the pieces fit​

  • On the Mac: MySQL 8.4 on port 3306 and MongoDB 8.0 on port 27017, both listening on 127.0.0.1 only and both run by brew services.
  • In Docker: the two backends, the two frontends, the mcp-server, the api service, and Mailpit, which catches outgoing email.
  • The containers reach the databases through the host name host.docker.internal.

The committed docker-compose.yml still defines mysql and mongo containers, on host ports 3307 and 27018. A gitignored docker-compose.override.yml puts them behind a profile so that they are not started.

docker/setup.sh still assumes container databases

docker/setup.sh starts the mysql and mongo containers, and applies its MySQL grants inside the npv3-mysql container. Do not use it for a native database setup. Follow the steps on this page, which run the same operations against the native servers.

Prerequisites​

  • A Mac with Homebrew.
  • A current Docker Desktop. The override file uses the Docker Compose !reset tag, which old Compose versions reject.
  • Git, and access to the Axxya-Systems/nexgen_v3 repository.
  • MySQL 8.4 (Homebrew mysql@8.4).
  • MongoDB 8.0 (Homebrew mongodb-community@8.0), with mongosh and the MongoDB database tools.
  • Optional: the nexgen_v2 repository cloned next to nexgen_v3, to run the V2 apps against the same databases.
brew install --cask docker
brew install mysql@8.4
brew tap mongodb/brew
brew install mongodb-community@8.0 mongosh mongodb-database-tools

mysql@8.4 is keg-only, so add its client to the path:

echo 'export PATH="/opt/homebrew/opt/mysql@8.4/bin:$PATH"' >> ~/.zshrc

Setting up the databases​

1. Configure and start MySQL​

Put these settings in /opt/homebrew/etc/my.cnf before the first start. They match what staging and production run, and the first two keep the V2 PHP 5.6 clients able to connect.

[mysqld]
bind-address = 127.0.0.1
mysqlx-bind-address = 127.0.0.1
port = 3306

mysql_native_password = ON
authentication_policy = *:mysql_native_password

sql-mode = NO_ENGINE_SUBSTITUTION
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

innodb_buffer_pool_size = 1G
brew services start mysql@8.4
brew services start mongodb-community@8.0

2. Create the databases and users​

Clone the repo, then load the two init scripts into the native servers. A fresh Homebrew MySQL has a root user with no password; if a root password has been set since, add -p.

cd ~/git/axxya
git clone git@github.com:Axxya-Systems/nexgen_v3.git
cd nexgen_v3
mysql -u root < docker/init.sql
mongosh docker/init-mongo.js

init.sql creates the MySQL databases and the three users. init-mongo.js creates the MongoDB database, its users, and its indexes. Both are safe to run again.

Database logins​

These are development-only credentials, committed in docker/init.sql and docker/init-mongo.js. They are never used on staging or production.

MySQL, at 127.0.0.1:3306:

  • npuser / nppassword is the app user, with full rights on nexgen_v3 (paid app), nexgen_free_v3 (trial app), and nexgen_test (the isolated database the backend unit tests use).
  • mcp_readonly / mcp_readonly_password is the read-only user for the mcp-server.
  • flask_readonly / flask_readonly_password is the read-only user for the api service.

MongoDB, at 127.0.0.1:27017:

  • nexgen / nexgen on the nexgen_mdb_v3 database, shared by the paid app and the trial app.
  • flask_writer / flask_writer on the same database, for the api service.

To connect from a terminal:

mysql -h 127.0.0.1 -P 3306 -u npuser -pnppassword nexgen_v3
mongosh "mongodb://nexgen:nexgen@127.0.0.1:27017/nexgen_mdb_v3?authSource=nexgen_mdb_v3"

The local database names end in _v3. Staging and production use nexgen, nexgen_free, and nexgen_mdb.

Setting up the Docker containers​

1. Create the environment files​

These files are gitignored, so a fresh clone does not have them, and Docker Compose will not start without them.

cp nexgen/backend/.env.example nexgen/backend/.env
cp nexgen/backend/.env.example docker/nexgen-free.env

In both files, point the backends at the native databases:

DB_HOST=host.docker.internal
MONGO_DB_HOST=host.docker.internal

Then, in docker/nexgen-free.env only, apply the trial differences: DB_DATABASE=nexgen_free_v3, TRIAL_DOMAIN=true, its own APP_NAME, and the trial create-limit variables (INGREDIENT_CREATE, RECIPE_CREATE, LABEL_CREATE, and the others), set high for development. The MongoDB database stays nexgen_mdb_v3. Copying this file from another developer is the quickest route.

2. Create the override file​

Create docker-compose.override.yml in the repo root. Docker Compose merges it over docker-compose.yml automatically, and it is gitignored.

services:
mysql:
profiles: ["docker-dbs"]
mongo:
profiles: ["docker-dbs"]

nexgen-backend:
depends_on: !reset []
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
JWT_PRIVATE_KEY: "<base64 private key>"
nexgen-free-backend:
depends_on: !reset []
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
JWT_PRIVATE_KEY: "<base64 private key>"
mcp-server:
depends_on: !reset []
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
MYSQL_HOST: "host.docker.internal"
MONGO_URL: "mongodb://nexgen:nexgen@host.docker.internal:27017/nexgen_mdb_v3?authSource=nexgen_mdb_v3"
api:
depends_on: !reset []
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
JWT_PUBLIC_KEY: "<base64 public key>"
MYSQL_HOST: "host.docker.internal"
MONGO_URL: "mongodb://flask_writer:flask_writer@host.docker.internal:27017/nexgen_mdb_v3?authSource=nexgen_mdb_v3"

The backends sign a token with the private key and the api service verifies it with the public key. Without the keys, the AI features return an error and everything else works. To generate a pair:

openssl genpkey -algorithm RSA -out jwt-private.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -in jwt-private.pem -pubout -out jwt-public.pem
base64 -i jwt-private.pem | tr -d '\n'
base64 -i jwt-public.pem | tr -d '\n'

Recipe import also needs ANTHROPIC_API_KEY, set in a gitignored .env file in the repo root.

3. Build and start the containers​

Name the services, because docker-compose.yml also defines services that are no longer run locally.

docker compose build nexgen-backend nexgen-free-backend nexgen-frontend nexgen-free-frontend mcp-server api
docker compose up -d nexgen-backend nexgen-free-backend nexgen-frontend nexgen-free-frontend mcp-server api mailpit

4. Install dependencies and prepare storage​

The two backend containers mount the same source folder, so one composer install serves both.

docker exec npv3-nexgen-backend composer install --no-interaction --prefer-dist
for c in npv3-nexgen-backend npv3-nexgen-free-backend; do docker exec "$c" bash -c 'mkdir -p storage/logs storage/framework/sessions storage/framework/views storage/framework/cache bootstrap/cache && chown -R www-data:www-data storage bootstrap/cache && chmod -R 775 storage bootstrap/cache'; done
docker exec npv3-nexgen-backend php artisan key:generate --force
docker exec npv3-nexgen-free-backend php artisan key:generate --force

5. Run the migrations and seeders​

docker exec npv3-nexgen-backend php artisan migrate --force
docker exec npv3-nexgen-free-backend php artisan migrate --force

The mcp-server and the api service each update one column, on a table that exists only after the migrations. Grant that on the native MySQL:

mysql -u root -e "GRANT UPDATE (last_used_at) ON nexgen_v3.personal_access_tokens TO 'mcp_readonly'@'%'; GRANT UPDATE (last_used_at) ON nexgen_free_v3.personal_access_tokens TO 'mcp_readonly'@'%'; GRANT UPDATE (last_used_at) ON nexgen_v3.personal_access_tokens TO 'flask_readonly'@'%'; FLUSH PRIVILEGES;"

Then seed both databases:

docker exec npv3-nexgen-backend php artisan db:seed --force
docker exec npv3-nexgen-free-backend php artisan db:seed --force

6. Start the V2 apps (optional)​

The V2 apps use the same databases.

cd ~/git/axxya/nexgen_v2
docker compose up -d

Local URLs​

  • http://localhost:4200 is the paid app frontend (nexgen-frontend).
  • http://localhost:4201 is the trial app frontend (nexgen-free-frontend).
  • http://localhost:8010 is the paid app backend (nexgen-backend).
  • http://localhost:8011 is the trial app backend (nexgen-free-backend).
  • http://localhost:5050 is the api service. The customer API base URL is http://localhost:5050/v3/customer.
  • http://localhost:3001 is the mcp-server.
  • http://localhost:8025 is Mailpit, which shows every email the V3 apps send.
  • http://localhost:8001, http://localhost:8002, and http://localhost:8004 are the V2 nexgen, nexgen-free, and nexgen-api services. The V2 Mailpit is at http://localhost:8026.

The seeders create these logins, among others:

  • admintest@axxya.com / adminpass@word1, an admin user.
  • test@axxya.com / testpass@word1, a client user.
  • dev@axxya.com / devpass@word1, a client user.

The frontends take a few minutes to compile after the containers start. docker logs -f npv3-nexgen-frontend shows the progress.

Day-to-day commands​

docker compose up -d nexgen-backend nexgen-free-backend nexgen-frontend nexgen-free-frontend mcp-server api mailpit
docker compose stop
docker compose ps
brew services list
brew services restart mysql@8.4
brew services restart mongodb-community@8.0
Use stop, not down

docker compose down removes the containers. Use docker compose stop to turn the stack off.

After pulling changes, run the migrations again on both backends, and the seeders if the pull changed them. After a seeder changes roles or permissions, clear the view cache with docker exec npv3-nexgen-backend php artisan view:clear, then log out and back in.

The Laravel log is at storage/logs/ inside each backend container, which is nexgen/backend/storage/logs/ on the Mac.

Running the database containers again​

The mysql and mongo containers and their volumes stay defined. To run them on purpose, for example to compare against MongoDB 5.0:

docker compose --profile docker-dbs up -d mysql mongo
docker compose stop mysql mongo

They listen on host ports 3307 and 27018, so they do not collide with the native servers. The backends keep using whichever host their environment files name.

Troubleshooting​

  • A backend cannot connect to MySQL or MongoDB. Check brew services list shows both servers as started, and that DB_HOST and MONGO_DB_HOST are host.docker.internal in both environment files. A changed environment file needs docker compose up -d <service> to recreate the container.

  • V2 fails with "authentication method unknown to the client". The two authentication lines are missing from my.cnf, or MySQL was not restarted after adding them.

  • A page still behaves as it did before you pulled a change to routes, service providers or Blade views. The running backend keeps its compiled routes, config and views, and PHP's opcode cache, until they are cleared and the container restarts. Run this for both backends:

    docker compose exec nexgen-backend sh -c 'php artisan route:clear && php artisan config:clear && php artisan view:clear'
    docker compose exec nexgen-free-backend sh -c 'php artisan route:clear && php artisan config:clear && php artisan view:clear'
    docker compose restart nexgen-backend nexgen-free-backend

    Normal CI deployments use a clean checkout and do not generate route caches, so a deployed backend loads the new routes when its container starts. A manual ./deploy.sh or copilot svc deploy builds the image from your working tree; the root .dockerignore leaves out bootstrap/cache/routes-*.php, bootstrap/cache/config.php and compiled views, so a cache generated locally does not reach the image.

  • A backend test fails now and then with Duplicate entry '<id>' for key 'users.PRIMARY' or 'clients.PRIMARY'. Some PHPUnit fixture helpers pick a random id from a few hundred values. If one test calls such a helper twice, the two calls can draw the same id, about once in a few hundred runs, and a rerun passes. The helper must draw again while the id is already taken:

    do {
    $userId = 999200 + random_int(0, 500);
    } while (DB::table('users')->where('id', $userId)->exists());

    The same error on every run, or in a file whose tests each create one user, points instead at rows left behind by another test file. The tester skill in nexgen_v3 covers that case.

  • A query is fast locally and slow on staging. A local MongoDB does not reproduce DocumentDB's query planner. See Nexgen V3 databases.