All documents
Handover

Deployment and handover

Where every part of the platform lives, the command that puts it there, and what each piece of the code is for. Written to be followed by somebody who did not build it.

Document
OPS-DEP-1
Version
1.0
Date
1 September 2026
Status
Current
Author
Abad Naseer

1The map

Three products, four repositories, four machines. Everything below was read off the running systems rather than remembered, so where this document and your memory disagree, believe this document and then check the machine.

WhatAddressMachineRegion
SmartMarketmarketz.smartzees.com54.254.25.0ap-southeast-1
SmartServiceservicez.smartzees.com35.91.251.211us-west-2
SmartCommunitylivz.smartzees.com54.188.207.85us-west-2
The landing pagesmartzees.com35.91.251.211shares the Service box
The documentsservicez.smartzees.com/docs/35.91.251.211and mirrored on Market
The open model184.34.122.122i-0c9e7a485d54e4e92us-west-2, another AWS account

The three application machines are Lightsail, in AWS account 952427475294. The model is plain EC2 in the client's own account, 253918336085. That is why it is reached over the public internet and why a security group, not a private network, is the only thing in front of it.

Getting in:

ssh -i ~/Downloads/sailagentecsdevkey.pem ubuntu@35.91.251.211   # Service, landing, docs
ssh -i ~/Downloads/sailagentecsdevkey.pem ubuntu@54.254.25.0    # Market
ssh -i ~/Downloads/sailagentecsdevkey.pem ubuntu@54.188.207.85  # Community

The model machine has its own key and is normally reached from the AWS console instead, through EC2 Instance Connect, so nobody has to be sent a private key.

2The four repositories

Local folderRemoteBranchProduct
plumber-assistantjafgithub/ai-service-chat-se-abadmasterSmartService
AI-Orders-mainjafgithub/aichat-v2-abadmainSmartMarket
community-assistantAbadNaseer/ai-agent-smartcommunitymasterSmartCommunity
smartzees-landingAbadNaseer/ai-agent-landingpage-smartzeesmastersmartzees.com

The last two are staged under a personal account because write access on the organisation copies has not been granted yet. When it is, the remote changes and nothing else does.

Each repository has the same shape: backend/ is FastAPI, frontend/ is Next.js exported to static files, and docs/ holds the written documents. The landing page has only a frontend.

Node is not on the system path on the build machine. Every frontend command below needs this first:

export PATH="$HOME/.local/node/bin:$PATH"

3Deploying each one

The pattern is the same everywhere and it is deliberate. Build locally, rsync to a staging directory in the deploy user's home, then move it into place with sudo. Never build on the server: these machines have under 2GB of memory and a Next.js build will take one down.

3.1SmartService

Backend, at /home/ubuntu/plumber/backend, unit plumber, port 8100.

rsync -az --exclude '__pycache__' --exclude '.venv' --exclude '.env' \
  plumber-assistant/backend/app/ ubuntu@35.91.251.211:/home/ubuntu/plumber/backend/app/
ssh ubuntu@35.91.251.211 'sudo systemctl restart plumber'

Frontend, served from /var/www/serviceagent.

cd plumber-assistant/frontend && npm run build:serviceagent
rsync -az --delete out/ ubuntu@35.91.251.211:/home/ubuntu/deploy-serviceagent/
ssh ubuntu@35.91.251.211 \
  'sudo rsync -a --delete /home/ubuntu/deploy-serviceagent/ /var/www/serviceagent/'

build:serviceagent rather than build: it sets an empty base path and an empty API url, because the app sits at the root of its own subdomain and the API is same origin.

3.2SmartMarket

Backend, at /var/www/ai-order/backend, unit aiorder.

rsync -az --exclude '__pycache__' --exclude '.env' \
  AI-Orders-main/backend/app/ ubuntu@54.254.25.0:/home/ubuntu/deploy-market-app/
ssh ubuntu@54.254.25.0 \
  'sudo rsync -a /home/ubuntu/deploy-market-app/ /var/www/ai-order/backend/app/ && sudo systemctl restart aiorder'

Frontend, served from /var/www/ai-order/frontend-dist.

cd AI-Orders-main/ai-order && npm run build:deploy
rsync -az --delete out/ ubuntu@54.254.25.0:/home/ubuntu/deploy-market/
ssh ubuntu@54.254.25.0 \
  'sudo rsync -a --delete /home/ubuntu/deploy-market/ /var/www/ai-order/frontend-dist/'

The web root is frontend-dist, not frontend. There is a frontend directory beside it that is not served. Deploying into it looks like a successful deploy that changes nothing, and it has cost an afternoon before.

3.3SmartCommunity

Backend, at /home/ubuntu/community/backend, unit community, port 8200.

rsync -az --exclude '__pycache__' --exclude '.venv' --exclude '.env' \
  community-assistant/backend/app/ ubuntu@54.188.207.85:/home/ubuntu/community/backend/app/
ssh ubuntu@54.188.207.85 'sudo systemctl restart community'

Frontend, served from /var/www/community.

cd community-assistant/frontend && npm run build
rsync -az --delete out/ ubuntu@54.188.207.85:/home/ubuntu/deploy-community/
ssh ubuntu@54.188.207.85 \
  'sudo rsync -a --delete /home/ubuntu/deploy-community/ /var/www/community/ && sudo chown -R www-data:www-data /var/www/community'

Port 8200 rather than 8100 is deliberate. This machine was cloned from the Service Assistant, and a stray request meant for that product must not find something listening here and be answered by it.

3.4The landing page

Static only, served from /var/www/smartzees on the Service machine.

cd smartzees-landing/app && npm run build
rsync -az --delete out/ ubuntu@35.91.251.211:/home/ubuntu/deploy-smartzees/
ssh ubuntu@35.91.251.211 \
  'sudo rsync -a --delete /home/ubuntu/deploy-smartzees/ /var/www/smartzees/'

3.5The documents

Served from /var/www/serviceagent-docs, which is outside the site root on purpose: the frontend deploy above uses --delete and would otherwise remove them.

python3 docs/_house/hub.py            # rebuilds docs/index.html from its list
python3 docs/deployment/build.py      # this document
rsync -az docs// ubuntu@35.91.251.211:/home/ubuntu/deploy-docs//
ssh ubuntu@35.91.251.211 \
  'sudo rsync -a /home/ubuntu/deploy-docs/ /var/www/serviceagent-docs/'

Generated document sets (source.md plus the render_*.py scripts) deploy their build/web/ directory. Hand written ones (srs, lab, deployment, the two specifications) deploy the index.html beside the builder.

4What the code files do

Only the ones somebody unfamiliar would otherwise have to read to understand. The rest are named for what they do.

Shared by all three products

FileWhat it is for
services/llm.py (Market: services/ai.py)The router. Everything wanting a sentence written asks this, and it decides whether our own model or Gemini answers, and falls back when the first cannot
services/gemini_service.pyGoogle's model. Also does speech to text and text to speech
services/ollama_service.pyOur own model over HTTP
services/tracing.pyMeasures each stage of a request and keeps the last 50, which is what the live diagram reads
api/ai_public.pyTwo read only routes with no token: which engine is switched on, and the recent traces. No question or reply text ever appears here
services/rag.pyThe embedding model, all-MiniLM-L6-v2, 384 dimensions, loaded once

SmartService only

FileWhat it is for
services/ai_runtime.pyThe engine switch itself, a small JSON file. This machine owns it for the whole platform
services/gpu_instance.pyFinds, starts and stops the model machine. Needs boto3 and AWS keys
api/ai_admin.pyThe admin panel's engine controls, including start and stop
services/conversation.pyTurns what somebody said into a search, a booking step or a parking request
services/catalog_index.py, phrase_index.pyThe service catalogue, held in memory so a search is a matrix multiply
services/parking.py, api/parking.pyVisitor passes and the QR code the gate reads
services/booking_service.py, booking_emails.pyAppointments, and the confirmations that go with them

SmartCommunity only

FileWhat it is for
services/community_chat.pyThe whole pipeline: scope to one association, retrieve, answer or refuse
services/docs_index.py209 passages in memory, and the 0.30 threshold under which no model is called at all
services/doc_library.py, doc_chunker.pyWhich documents exist, and cutting a PDF into passages
services/platform_switch.pyReads the engine switch from SmartService. This agent never writes it
api/documents.pyUpload, list, withdraw, download. The office screen talks to this

SmartMarket only

FileWhat it is for
services/catalog_index.pyAbout 25,600 products in memory. This is the 787x search improvement
sync_to_remote.pyDrains sync_outbox into the client's own database. See section 6
services/shopping/Sourcing items the shop does not stock, through a provider interface
services/order_service.pyTotals, tax and tips. Tax is computed on the goods and the tip on the goods before tax

5The model, and the switch

One switch decides which engine answers, for all three products, and it lives on servicez.smartzees.com/admin under AI runtime. The other two read it over HTTP and never write it.

Settings on the Service machine, in .env:

GPU_INSTANCE_ID=i-0c9e7a485d54e4e92
AWS_REGION=us-west-2
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
OLLAMA_MODEL=llama3.2:3b
OLLAMA_TIMEOUT_SECONDS=45

The other two only need OLLAMA_URL and OLLAMA_MODEL, because they never start or stop anything.

OLLAMA_URL and GPU_INSTANCE_ID are two different modes and the first one wins. Set both on the Service machine and the panel will show a healthy engine it cannot stop. It also needs boto3 installed in its virtual environment, or every AWS call fails and the panel says the machine is unknown.

6Rules that are not optional

RuleWhy
Exactly one machine may run aiorder-sync, and it is 54.254.25.0. The same for plumber-sync on 35.91.251.211.These push rows into the client's live database. Cloned machines come up with the sync running, and two machines writing the same rows has happened three times
Stop aiorder-sync before any test that creates a customer or an order.Test data reaches the client's production system otherwise. Start it again afterwards
Never widen port 11434 beyond the three application addresses.Ollama has no authentication of any kind. The security group is the whole of its protection
Never build on a server.Under 2GB of memory. A Next.js build will take the machine down
The model machine's shutdown behaviour must stay stop.It switches itself off when idle. Set to terminate, the idle timer deletes it

Checking the first rule takes one command and belongs in every handover:

for h in 35.91.251.211 54.254.25.0 54.188.207.85; do
  echo -n "$h "
  ssh ubuntu@$h 'systemctl is-active plumber-sync aiorder-sync | tr "\n" " "'
  echo
done

7Checking a deploy worked

Not "it returned 200", which a stale page also does. Ask it something.

curl -s -X POST https://servicez.smartzees.com/api/v1/chat \
  -H 'Content-Type: application/json' \
  -d '{{"message":"my boiler is leaking","session_id":"check"}}'

curl -s -X POST https://marketz.smartzees.com/api/v1/chat \
  -H 'Content-Type: application/json' \
  -d '{{"message":"milk","session_id":"check"}}'

curl -s -X POST https://livz.smartzees.com/api/v1/chat \
  -H 'Content-Type: application/json' \
  -d '{{"message":"quiet hours","session_id":"check","community":"serenity"}}'

A good answer from the third names a document. If it does not, the index did not load, whatever the status code said.

And the platform switch, which the other two agents depend on:

curl -s https://servicez.smartzees.com/api/v1/ai/provider

8Leftovers on the machines

Written down because they look important and are not. Every one is a copy left behind when a machine was cloned.

MachineLeftoverStatus
54.188.207.85/var/www/serviceagent, /var/www/ai-orderNot served. From the clone this machine was made from
54.254.25.0/var/www/plumber, /var/www/plumber.prevNot served
35.91.251.211/var/www/ai-orderServes the documents mirror only, not the shop
Both app machinesplumber_assistant and ai_order databases where the product does not runUnused. Left rather than dropped, because dropping a database to reclaim nothing is a bad trade

None of these are served by nginx. If you are editing something and nothing changes, check you are on the right machine before you check anything else.

Written by Abad Naseer. Every measured number in this document is cited from the running system or from the code, and every target says that it is a target. Where the two disagree, the document says so rather than choosing the flattering one.