Run the stack locally
By the end you’ll have a portal → backend → middleware → crm-mock round-trip running locally, with the
SPA pointed at your local backend. You need 5 of the 11 repos: backend, crms-middleware, frontend,
backend-pdf-service (only for sales-offer PDFs), and crm-mock (stands in for Dynamics + the Logic Apps).
Prereqs + the ports
Section titled “Prereqs + the ports”Docker + Docker Compose; Node ≥ 20 + Yarn 1 (frontend) / npm (pdf-service). Each dev compose publishes on a deliberately non-clashing host port (DBs are off 3306):
| Service | Host port |
|---|---|
| backend nginx | 8080 |
| backend MySQL / Mailhog / Redis / MinIO | 33006 / 8025 / 63790 / 9000–9001 |
| crms-middleware nginx / MySQL | 8081 / 33007 |
| crm-mock nginx | 8089 |
| backend-pdf-service | 3001 |
| frontend (Nx: shopper / broker) | 4200 / 4201 |
The SPAs talk only to backend (:8080); backend → crms-middleware (:8081) → crm-mock (:8089);
backend → backend-pdf-service (:3001) for PDFs.
Bring it up, in order
Section titled “Bring it up, in order”-
crm-mockfirst (so the CRM endpoints exist) — Laravel 12 / SQLite:Terminal window cp .env.example .envcomposer install && php artisan key:generatetouch database/database.sqlite && php artisan migratecomposer run dev # or: docker-compose -f docker/docker-compose-dev.yml --env-file .env up -dIts
oauth2/tokenjust returns a literalfake-token— no real Azure needed. nginx on :8089. -
crms-middleware:Terminal window cp .env.example .envdocker-compose -f docker/docker-compose-dev.yml --env-file .env up -d # note .yml (README says .yaml — typo)docker-compose exec app composer installdocker-compose exec app php artisan migrate && docker-compose exec app php artisan db:seeddocker-compose exec app php artisan passport:keys # passport:keys, NOT passport:installThen point its CRM config at the mock and create the OAuth client (next two sections).
-
backend:Terminal window cp .env.example .envdocker-compose -f docker/docker-compose-dev.yml --env-file .env up -ddocker-compose exec app composer installdocker-compose exec app php artisan key:generatedocker-compose exec app php artisan migratedocker-compose exec app php artisan db:seeddocker-compose exec app php artisan passport:keysnginx on :8080. Set
CRMS_MIDDLEWARE_{BASE_URL,AUTH_URL,API_URL}→http://host.docker.internal:8081/…. -
backend-pdf-service(only for PDFs):npm install && npm run dev— on :3001. -
frontend:cp .env.example .env→ edit the endpoints (below) →yarn install→yarn start(shopper:4200) /yarn start:broker(:4201) / …
Point the SPAs at your local backend
Section titled “Point the SPAs at your local backend”frontend/.env.example ships every endpoint as the placeholder https://api.example.com/api — forget to
change it and the SPA compiles but every request 404s. Set the API bases (note the double underscore)
NX__API_ENDPOINT, NX__BROKER_API_ENDPOINT, NX__SALESMAN_API_ENDPOINT,
NX__SHOPPER_ANALYTICS_API_ENDPOINT, NX__PRE_DELIVERY_API_ENDPOINT → http://localhost:8080/api. The
per-persona path prefix is separate: NX__BROKER_ENDPOINT_PREFIX=/broker,
NX__SHOPPER_ENDPOINT_PREFIX=/shopper, NX__SALESMAN_ENDPOINT_PREFIX=/sales-man, etc.
Wire the middleware to crm-mock
Section titled “Wire the middleware to crm-mock”All the CRM vars are empty in crms-middleware/.env.example, so this is a manual step:
{EGYPT,OMAN,MONTENEGRO}_CRM_BASE_URL→http://host.docker.internal:8089{EGYPT,OMAN,MONTENEGRO}_CRM_AUTH_URL→http://host.docker.internal:8089/api/oauth2/token- Oman broker-CRM mock:
OMAN_CRM_BROKER_CRM_MOCK_ENABLED=true+OMAN_CRM_BROKER_CRM_MOCK_BASE_URL=http://host.docker.internal:8089(flips the Oman pre-reg clients to the mock viaswapOmanBrokerCrmToMockIfEnabled).
Passport clients (service-to-service)
Section titled “Passport clients (service-to-service)”Sanctum (end-user tokens) needs nothing beyond migrate. Passport (the client_credentials legs)
needs keys + two clients:
- On middleware:
passport:client --client→ name itportals-backend. Copy its id/secret into backendCRMS_MIDDLEWARE_CLIENT_ID/CRMS_MIDDLEWARE_CLIENT_SECRET. - On backend:
passport:client --client→ acrms-middlewareclient (used by the middleware to call the backend’s/api/crms-middleware/*webhooks).
The scopes (middleware-webhook / portals-api / crm-webhook) are applied at the token endpoint, not
pinned on the client — so plain client_credentials clients are enough. See the
auth model.
Queues — you don’t need a worker locally
Section titled “Queues — you don’t need a worker locally”Verify (smoke test)
Section titled “Verify (smoke test)”- Services up:
curl http://localhost:8080·:8081·:8089/api/oauth2/token(→{"access_token":"fake-token",…}) ·:3001(pdf has a/healthprobe). - Backend works end-to-end:
curl http://localhost:8080/api/currencies(a public route) should return JSON — proving nginx → PHP → MySQL. - A CRM round-trip through the mock: exercise a mocked path — a units-listing read or the Oman
broker pre-registration register — and confirm the middleware authenticates (
fake-token) and returns a mapped body. Don’t use a lead submit (the mock has no lead-write endpoint).
Foot-guns to expect
Section titled “Foot-guns to expect”- Networking isn’t codified — use
host.docker.internal. Each dev compose uses its own bridge network (orascom-dev,crms-middleware-dev,crm-mock-dev,orascom-pdf) and the declared external network names don’t line up across repos. Reach every service byhttp://host.docker.internal:<port>. On Linux add--add-host=host.docker.internal:host-gateway. PDF_SERVICE_URLdrift:.env.examplesayslocalhost:3001,CLAUDE.mdsayspdf-service:3001. From a dockerised backend usehttp://host.docker.internal:3001.- Port 3001 clashes with other local stacks — override
PORT. (DB ports are safe at 33006/33007.) CURRENY_LAYERis a deliberate typo — don’t “fix” it; renaming breaks every populated env.- Docs drift: both Laravel READMEs say Laravel 9 / PHP 8.1, but the backend is Laravel 10 / PHP 8.2
(
composer.jsonwins). The middleware README compose path says.yaml; the file is.yml.
