014 — Deployment Architecture¶
The platform deploys through three pipelines:
| # | Pipeline | Delivers | Trigger |
|---|---|---|---|
| 1 | Odoo.sh | The Odoo backend, all five environments | ERP Team merges between branches |
| 2 | Cloudflare Workers | The production portal at b2b.samtia.com |
Automatic — a push tomain on GitHub |
| 3 | Docker → private registry | The four internal portal environments (staging / pre-prod / training) | Manual — a developer runs the build script |
Only pipeline 2 is continuous. Pipelines 1 and 3 are deliberately gated by a human.
Runtime Topology¶
Production¶
flowchart TB
U(["Users"]) -->|HTTPS| CFP["b2b.samtia.com<br/>Cloudflare Worker (OpenNext)"]
U -->|HTTPS| ODOOP["www.samtia.com"]
CFP -->|"REST + WebSocket<br/>NEXT_PUBLIC_APP_API_BASE_URL"| ODOOP
ODOOP --> SHP["Odoo.sh production build<br/>workers · bus · cron"]
SHP --> DBP[("PostgreSQL")]
SHP --> FSP[("Filestore")]
SHP --> SMTPP[["SMTP"]]
Non-production (staging / pre-prod / training)¶
flowchart TB
U(["Users"]) -->|HTTPS| CDNP["Portal domain<br/>e.g. staging-b2b.adv-photonx.com"]
U -->|HTTPS| ODOOD["Odoo domain<br/>e.g. staging-b2b-odoo.adv-photonx.com"]
CDNP --> CT["Next.js container<br/>standalone build<br/>internal server, port 30xx"]
CT -->|"REST + WebSocket<br/>NEXT_PUBLIC_APP_API_BASE_URL"| ODOOD
ODOOD --> SH["Odoo.sh build<br/>workers · bus · cron"]
SH --> DB[("PostgreSQL")]
SH --> FS[("Filestore")]
SH --> SMTP[["SMTP"]]
The portal is stateless in both shapes — session state lives in the Odoo cookie and in the
browser — so the runtime can be a Worker or a container with no other change. Everything
environment-specific arrives through NEXT_PUBLIC_* build-time variables.
Backend Pipeline — Odoo.sh¶
Repository relationship¶
flowchart LR
B2B["B2B submodule repo<br/>addons_lp_ecommerce<br/>branch: main"] -->|"submodule pointer<br/>updated by ERP Team"| ERP["ERP main repo<br/>connected to Odoo.sh"]
ERP --> SB["staging-b2b"]
ERP --> SE["staging-erp"]
SB --> PP["pre-prod"]
SE --> PP
PP --> TR["training"]
TR --> PR["production"]
Golden rule: code flows one way only —
staging-b2b / staging-erp → pre-prod → training → production.
All merges on Odoo.sh are performed exclusively by the ERP Team. The B2B team never pushes
to an Odoo.sh branch.
Environments¶
| Branch | Purpose | Storefront portal | Odoo ERP Back Office |
|---|---|---|---|
staging-b2b |
Validate B2B submodule changes | staging-b2b.adv-photonx.com |
staging-b2b-odoo.adv-photonx.com |
staging-erp |
Validate ERP / ERP Back Office changes | — (ERP Back Office only) | adv-photonix-erp-34374833.dev.odoo.com |
pre-prod |
Integrate both streams; joint testing | pre-prod.adv-photonx.com |
pre-prod-odoo.adv-photonx.com |
training |
UAT and end-user training | training-b2b.adv-photonx.com |
training-b2b-odoo.adv-photonx.com |
production |
Live | b2b.samtia.com |
www.samtia.com |
Production runs on its own samtia.com domain pair; every non-production environment uses an
adv-photonx.com sub-domain. staging-erp has no portal and is still reached at its raw
Odoo.sh build URL.
Release sequence¶
sequenceDiagram
participant B2B as B2B Team
participant ERP as ERP Team
participant TR as Training Team
B2B->>B2B: push to submodule main
B2B->>ERP: request promotion
ERP->>ERP: update submodule pointer → staging-b2b
B2B->>B2B: test storefront & portals on staging-b2b
B2B->>ERP: upgrade request
ERP->>ERP: staging-b2b → pre-prod
ERP->>ERP: staging-erp → pre-prod
B2B-->>ERP: joint testing on pre-prod
Note over B2B,ERP: BOTH teams must confirm
ERP->>ERP: pre-prod → training
TR->>TR: UAT on training
TR->>ERP: go / no-go
ERP->>ERP: training → production (LIVE)
Guardrails¶
| Rule | Meaning |
|---|---|
| One-way syncing | Never merge backwards. A fix restarts from the source repo. |
| Single merge authority | Only the ERP Team merges or moves submodule pointers. |
| Test-then-promote | A branch is promoted only after the responsible team confirms and raises an upgrade request. |
| Isolation before integration | The B2B and ERP streams meet for the first time inpre-prod — never earlier. |
| Protected production | Production receives code only fromtraining. |
Team responsibilities¶
| Team | Owns | Environments |
|---|---|---|
| B2B E-commerce | Submodule development, storefront & portal testing, upgrade requests | staging-b2b, pre-prod |
| ERP | ERP repo, Odoo.sh,all merges and submodule updates, deployments | staging-erp, pre-prod, training, production |
| Training | UAT, business-scenario validation, final go/no-go | training |
Production Portal Pipeline — Cloudflare Workers (CI/CD)¶
The live portal is continuously deployed. There is no manual build step and no image artefact.
flowchart LR
DEV["Developer merges<br/>to main"] --> GH["GitHub<br/>LaplaceSoftware/next_ecommerce"]
GH -->|"push webhook"| CFB["Cloudflare Workers Build"]
CFB --> P1["prebuild → release-history.json"]
P1 --> P2["opennextjs-cloudflare build<br/>Next.js → .open-next/"]
P2 --> P3["upload worker + static assets"]
P3 --> LIVE["b2b.samtia.com<br/>Worker 'next-ecommerce'"]
LIVE -->|"REST + WebSocket"| ODOO["www.samtia.com"]
| Aspect | Detail |
|---|---|
| Trigger | A push tomain on the GitHub remote — every merge redeploys production |
| Builder | Cloudflare Workers Builds (Cloudflare's own git integration; the repo containsno GitHub Actions workflow) |
| Adapter | @opennextjs/cloudflare — npm run build:cloudflare runs opennextjs-cloudflare build |
| Worker | Namednext-ecommerce, entry .open-next/worker.js, static assets bound as ASSETS from .open-next/assets |
| Runtime flags | nodejs_compat, plus a WORKER_SELF_REFERENCE service binding back to itself |
| Configuration | wrangler.jsonc and open-next.config.ts at the repo root |
| Build variables | .env.production — API base URL https://www.samtia.com, NEXT_PUBLIC_APP_VERSION, timezone Asia/Riyadh, mock data off |
| Local rehearsal | npm run preview:cloudflare builds and serves the Worker locally before merging |
Two remotes, two roles¶
| Remote | Address | Role |
|---|---|---|
github |
LaplaceSoftware/next_ecommerce |
Deployment trigger. Cloudflare watches main here |
origin |
Azure DevOpsTechnicalUnitedSA/ERP-Odoo/next_ecommerce |
Code review. Pull requests are raised and merged here |
The consequence is the single most important operational fact about the portal: merging a
PR on Azure DevOps does not deploy anything. Production goes live only when main is pushed to
the GitHub remote, and it does so immediately, without review or approval gate. Push to GitHub
main deliberately, and only after the change has passed the staging environments below.
Non-Production Portal Pipeline — Docker¶
The four internal environments still build and publish images by hand. They are not connected to Cloudflare.
Environments¶
| Env | Folder | Internal URL | Public sub-domain | Odoo API | Image tag |
|---|---|---|---|---|---|
| training | docker/training/ |
192.168.2.32:3080 |
— | b2b-training-odoo.adv-photonx.com |
training |
| pre-prod | docker/pre-prod/ |
192.168.2.32:3081 |
pre-prod.adv-photonx.com |
pre-prod-odoo.adv-photonx.com |
pre-prod |
| staging-b2b | docker/staging-b2b/ |
192.168.2.32:3082 |
staging-b2b.adv-photonx.com |
staging-b2b-odoo.adv-photonx.com |
staging-b2b |
| training-b2b | docker/training-b2b/ |
192.168.2.32:3083 |
training-b2b.adv-photonx.com |
training-b2b-odoo.adv-photonx.com |
training-b2b |
Each environment folder is self-contained: its own .env.<env>, Dockerfile,
docker-compose.yml, build_upload_amd64.sh and gitignored SSH defaults. All four push to the
same private registry on the server (127.0.0.1:5000/next-ecommerce-portal:<tag>).
The repo-root
Dockerfile,Dockerfile.devanddocker-compose.ymlare for local development and manual production runs — they are not part of this pipeline.
Build and publish¶
flowchart LR
A["bump NEXT_PUBLIC_APP_VERSION<br/>in docker/<env>/docker-compose.yml"] --> B["build_upload_amd64.sh"]
B --> C["buildx → linux/amd64 image<br/>tagged <env> and <env>-<version>"]
C --> D["save both tags to<br/>docker/build_releases/<env>/<env>_<version>_amd64.tar"]
D --> E["scp tar to the server"]
E --> F["--deploy: load, retag,<br/>push both tags to 127.0.0.1:5000"]
F --> G["redeploy the stack<br/>(manual — Portainer / compose)"]
Two tags per build:
| Tag | Role |
|---|---|
next-ecommerce-portal:<env> |
Floating tag the running stack pulls; unchanged by version bumps |
next-ecommerce-portal:<env>-<version> |
Permanent versioned tag, read automatically from that environment's compose file |
Local tars are kept per version under docker/build_releases/<env>/, so earlier release
artefacts are never overwritten and a previous build can be re-deployed with --no-build.
The script does not restart the running container. Redeploying the stack is a separate, deliberate step.
Build-time behaviour¶
prebuild/prebuild:dockerregeneratepublic/release-history.jsonfrom the per-release JSON files, projecting only{version, date, highlights}. This runs in both pipelines, so/release-notesis correct on Cloudflare and in the containers alike.- The Dockerfile produces a Next.js standalone output; the Cloudflare path produces an OpenNext Worker bundle instead.
- The admin layout forces dynamic rendering so neither build attempts to statically generate session-dependent admin pages.
Coordinating the Pipelines¶
flowchart TB
subgraph REL["A platform release = a pair"]
BE["Odoo.sh build<br/>submodule commit"]
FE["Portal build<br/>Worker (prod) or image tag (non-prod)"]
end
REL --> RN["_docs/releases/<version>.json<br/>records headCommit (frontend)<br/>+ headCommitBackend (module)<br/>+ environments"]
Because the halves version independently, the portal's release file is the record that ties them together. When a change spans both, deploy the backend first — the portal tolerates unknown backend fields far better than the reverse.
The ordering risk introduced by CI/CD¶
Production is now the fastest path in the platform while its backend counterpart is the slowest — Odoo.sh production is reached only after staging, integration and UAT, and every merge is performed by the ERP Team.
flowchart LR
A["Portal change merged<br/>+ pushed to GitHub main"] -->|"minutes"| B["Live on b2b.samtia.com"]
C["Backend change<br/>on the submodule"] -->|"staging-b2b → pre-prod<br/>→ training → production"| D["Live on www.samtia.com"]
B -.->|"portal may call an endpoint<br/>production Odoo does not have yet"| D
Rule: never push a portal change to GitHub main while it depends on a backend change that
has not yet reached Odoo.sh production. The Cloudflare pipeline has no gate that will catch
this for you.
Environment name mapping (a known trap)¶
| Odoo.sh branch | Portal env folder | Note |
|---|---|---|
staging-b2b |
docker/staging-b2b/ |
aligned |
pre-prod |
docker/pre-prod/ |
aligned |
training |
docker/training-b2b/ and docker/training/ |
two portal environments point at different Odoo hosts; checkNEXT_PUBLIC_APP_API_BASE_URL before deploying |
production |
none — nodocker/production/ folder exists |
The live portal atb2b.samtia.com is a Cloudflare Worker, deployed by CI/CD from GitHub main, configured by .env.production |
staging-erp |
— | ERP Back Office only, no portal |
Deployment Checklist¶
Backend
- [ ] Change merged to the submodule
main - [ ] ERP Team updated the pointer on
staging-b2b - [ ] Module upgrade completed on the build (new models/fields applied)
- [ ] Post-deploy configuration verified (see 013)
Frontend — non-production (Docker, manual)
- [ ]
NEXT_PUBLIC_APP_VERSIONbumped in the targetdocker/<env>/docker-compose.yml - [ ]
_docs/releases/<version>.jsonwritten, with both head commits and business-facing highlights - [ ]
build_upload_amd64.sh --deployrun for each target environment - [ ] Stack redeployed and the running version confirmed
- [ ]
/release-notesshows the new entry
Frontend — production (Cloudflare, automatic)
- [ ] The change has been validated on
staging-b2bandpre-prod - [ ] Any backend change it depends on has already reached Odoo.sh
production - [ ]
NEXT_PUBLIC_APP_VERSIONbumped in.env.production - [ ]
npm run preview:cloudflarerun locally — the Worker build differs from the container build - [ ] Push
mainto the GitHub remote — this is the deploy action - [ ] Cloudflare build succeeded;
b2b.samtia.comserves the new version and/release-notesshows it