Skip to main content

Nexgen V3 deployment

The full deployment procedure lives in the repository README. This page is a short reference for the current live production versions.

Live production versions​

The six live production version strings:

  • nexgen-backend
  • nexgen-frontend
  • nexgen-free-backend
  • nexgen-free-frontend
  • mcp-server
  • api

These are the production service URLs:

  • app.nutritionistpro.com (nexgen)
  • trial.nutritionistpro.com (nexgen-free)
  • mcp.nutritionistpro.com (mcp-server)
  • api.nutritionistpro.com (api)
  • dev.nutritionistpro.com (api, the customer API base URL https://dev.nutritionistpro.com/v3/customer; staging uses dev.staging.axxya.com)

The api service's production block is registered in copilot/api/manifest.yml but only staging is deployed today. Deploying it to production needs the dev.nutritionistpro.com DNS record added by hand in Cloudflare, CUSTOMER_API_URL set on nexgen-backend so the Settings page shows the base URL, and the flask_readonly MySQL user granted UPDATE (last_used_at) on personal_access_tokens so the API Key card's Last used value updates. The api/README.md in the nexgen_v3 repo has the full procedure.

Steps production does not run on its own​

Staging and test run docker/entrypoint.sh when a backend container starts, which runs php artisan migrate and create_indexes.php. Production skips both, so every PR whose post-deploy steps mention a migration or a new MongoDB index needs a hand run after the deploy. Look for a "Post-deploy steps" section in the PR body.

MongoDB indexes​

create_indexes.php creates every index the code expects and skips the ones that exist. Run it once for the shared nexgen_mdb database, from the nexgen-backend container, since nexgen-free uses the same MongoDB:

copilot svc exec --app nexgen-v3 --env prod --name nexgen-backend --command "sh -c 'su -s /bin/sh www-data -c \"php /var/www/html/create_indexes.php\"'"

Check that the first output line says db=nexgen_mdb. Nexgen V3 databases describes the databases themselves. DocumentDB builds one index per collection at a time, so repeat the command a few minutes apart until it reports no Existing index build in progress. The bson_append_array() warnings are driver noise. Indexes on the foods collection take a while because the collection is large, so run those outside business hours.

Two reasons the run matters beyond speed:

  • A search that hints an index fails outright if the index is missing. The food searches hint FoodID_1, FoodNameLC_1, owner_1, and UniqueCode_1 on foods, so those four must exist before a backend that hints them is deployed. From mongosh on the bastion, db.foods.getIndexes().map(i => i.name) lists them.
  • The manufacturer search only uses ManufacturerID_1 once it exists. Until the run, it behaves as it did before the index was added, which is slow but correct.
  • The source code search only runs once FoodCode_1 exists on foods. An all-digit search term matches FoodCode (the FNDDS, NDB, or CNF code) through a hinted bucket, and the bucket is skipped entirely while the index is missing, because an unindexed equality walks the whole library on DocumentDB. Until the run, digit searches find foods by ID and unique code only. After the run, time one digit-term search on staging and then production before relying on it; a local MongoDB picks the index unhinted, so local timings say nothing about DocumentDB.

To confirm a specific index, for example the compound one the dependent recipe recalculation uses:

db.recipes.getIndexes().map(i => i.name)

Expected on recipes: _id_, FoodID_1, owner_1, and owner_1_RecipeFoods.RecipeFoodID_1.

Food source codes stored as numbers​

foods.FoodCode is stored as a string on most documents and as an integer on about 14,000 older ones. Every read treats the two forms as one value, so nothing breaks while both exist, but the command below rewrites the numeric codes as strings so the library is consistent. It is a dry run unless --confirm is passed, it is idempotent, it never touches a string, null, empty, or fractional value, and it is only ever run by hand. One run serves nexgen and nexgen-free because they share nexgen_mdb. Run the dry run first and check the count it reports:

copilot svc exec --app nexgen-v3 --env prod --name nexgen-backend --command "sh -c 'su -s /bin/sh www-data -c \"php /var/www/html/artisan nexgen:normalise-food-codes\"'"
copilot svc exec --app nexgen-v3 --env prod --name nexgen-backend --command "sh -c 'su -s /bin/sh www-data -c \"php /var/www/html/artisan nexgen:normalise-food-codes --confirm\"'"

--food-id=<FoodID> limits the run to one food, --limit=<n> caps the number rewritten, and --json prints the result as JSON. The NutKat publish path now stores strings, so numeric codes do not come back.

MySQL migrations​

Run the migrations on both backends. Each has its own MySQL database, and migrations are written to be additive and idempotent:

copilot svc exec --app nexgen-v3 --env prod --name nexgen-backend --command "sh -c 'su -s /bin/sh www-data -c \"cd /var/www/html && php artisan migrate --force\"'"
copilot svc exec --app nexgen-v3 --env prod --name nexgen-free-backend --command "sh -c 'su -s /bin/sh www-data -c \"cd /var/www/html && php artisan migrate --force\"'"

php artisan migrate:status in the same container shows what is pending. Run commands as www-data: a root-run artisan command can leave cache files the web user cannot write.

After the run​

Check the next Local Runner Full Tests workflow is green. npm run test:perf-staging from the repo root runs the DocumentDB performance suite against staging and is the quickest way to confirm a search is within budget after an index change.

Configuration variables​

These are read from the environment and can be changed without a code change. None of them is set in a Copilot manifest today, so every environment runs on the default unless a manifest says otherwise.

  • API_RATE_LIMIT - requests per minute per client IP for the api route group, which is the Laravel API the Angular app calls. The default is 1000. A zero or junk value is treated as 1, so a bad setting never locks the API out completely. The Local Runner workflow sets it to 100000 because a whole E2E suite arrives from one address and shares one bucket; a 429 on POST /api/v1/validate-user shows up as a logged-in page with only Dashboard and Help in the sidebar. Do not set it in a manifest without a reason.
  • SHOP_POSTAL_CODE_OPTIONAL_COUNTRIES - a comma-separated list of country names added to the countries where the checkout and billing forms do not require a postal code. See Pricing.
  • CUSTOMER_API_URL - the customer API base URL shown on the Settings page API Key card. Unset in production until the api service is deployed there.
  • ALLOW_EDITS - the trial edit limit per record on nexgen-free-backend, raised from 3 to 10 in September 2026. This one is set in the nexgen-free-backend manifest.

Notes​

  • Frontend versions come from each backend's version_front field (both frontends share source and are on the same build). Backend and frontend diverge (pr463 vs pr465), meaning the frontends got a later one-sided deploy than the backends.
  • The api (Flask) service reports main-0000000, which is an unstamped or placeholder APP_VERSION, so its build tag is not traceable to a PR or SHA the way the others are. Worth fixing if you want real provenance on that service.