Appearance
Deploy & Promote a Change
The whole non-production fleet is deployed by a single Render Blueprint that lives in luke-platform/render.yaml. There is no per-service dashboard clicking and no hand-rolled CI/CD: you merge to the branch that maps to an environment, and Render redeploys that environment's service from its own repo (repo: + branch:).
This guide is the practical checklist for shipping and promoting a change. For the shape of the system it deploys into, see Deployment Topology and Platform & Deployment.
As of July 2026.
uatis defined in the blueprint but shipped commented-out — uncomment its block to enable it.
Environments & branches
Each environment is a full stack — engine (Docker), consumer-auth gateway (Docker), file-proxy (Docker), agents (Python), consumer-ui (static) and core-ui (static) — all pulled from their own repos and sharing one Postgres instance (luke-nonprod-db), isolated by schema. The branch a service repo is deployed from is what decides where a change lands.
| Environment | Branch | Purpose |
|---|---|---|
| dev | develop | Integration / daily development. Merging here auto-deploys dev. |
| qa | qa | Stabilisation and test. Promote by merging develop → qa. |
| uat | uat | User-acceptance (currently disabled). Promote by merging qa → uat. |
Deploy from develop, not main
Service repos deploy from develop (and qa/uat) — never main. A change merged only to a service's main does not ship: no environment is wired to it. Always open PRs against develop (or qa).
The one exception is the luke-email headless library, whose deploy/publish branch ismain. Treat it as the odd one out, not the rule.
Promotion flow
A change rides the same one-way escalator through the environments. You do not deploy an environment directly; you merge into its branch and Render does the rest.
The rule of thumb: promotion is a merge. develop → qa → uat, never skipping a rung and never editing an environment out of band.
Deploy a service
Java engines (luke-core-engine, luke-auth-engine, luke-file-proxy) and the Python luke-agents service are all type: web services built by Render.
- Open a PR into the service repo's
developbranch and get it green. - Merge. Render detects the push and rebuilds that environment's service:
- Java services build from
./Dockerfileand expose a health check at/actuator/health. Render waits for it to pass before routing traffic. luke-agentsbuilds withpip install -r requirements.txtand runs the FastAPI app. It keeps its transcripts in its own schema (luke_agents_<env>), separate from the engine/capability schemas, so a redeploy does not touch process data.
- Java services build from
- Watch the deploy in the Render dashboard until it reports live and the health check is green.
- Promote when ready: merge
develop→qa(thenqa→uat). Each merge redeploys only that environment.
Config changes ship the same way
Environment variables are declared in render.yaml. Changing a service's config means editing the blueprint in luke-platform and syncing it — it is not a code deploy of the service repo. Secrets marked sync: false (WorkOS keys, etc.) are set once in the dashboard.
Deploy a UI
The UIs (luke-consumer-ui, luke-core-ui) are static sites: Render runs npm ci && npm run build and publishes ./dist. Merge to develop and the static site rebuilds and redeploys. Two things about UI deploys are easy to get wrong.
Vendored headless libraries
The apps consume the headless libraries — @lukeflow/form-*, @lukeflow/email-*, @lukeflow/sign-*, @lukeflow/list-* — as vendored built dist, not as live workspace source. A library change is therefore a three-step rebuild, in order:
- Rebuild the package in its own monorepo (produces fresh
dist). - Re-vendor that
distinto the consuming app. - Rebuild/push the app so the new vendored bundle is in
./dist.
Skipping the re-vendor step means the app ships the old library — the build is green but the change is invisible. See the headless libraries reference.
The /embed bundle lives in the engine — not the UI
The public /embed page is served by the engine
The public /embed page is a standalone bundle copied into the engine at static/embed-assets. It is not served by luke-consumer-ui. A consumer-ui push does not update it.
To ship an /embed change:
- Build the embed bundle (
build:embed). - Copy it into the engine at
static/embed-assets. - Commit and push the engine (
luke-core-engine) todevelop.
If you only push consumer-ui, the live /embed page stays on the old bundle.
Shared database rule
All environments share the single luke-nonprod-db Postgres instance. They stay isolated because each engine sets its schema-name (DB_SCHEMA = platform_dev / platform_qa / platform_uat) and the engine creates that schema on first connect. The agents service does the same with its own schema (luke_agents_<env>).
Schema-name + table-prefix isolation is a hard invariant
A shared-instance engine must set its schema-name and table-prefix. Without it, the second environment to boot crashes while creating act_id_user (the tables already exist in the shared schema). This is not optional tuning — it is what keeps dev/qa/uat from colliding on one database. See Multi-Tenancy.
Never point a new environment at an existing schema, and never drop the table-prefix "to keep it simple" — a first-boot collision is the immediate result.
Rollback
Rollbacks are a Render operation, not a git-revert scramble:
- Redeploy a previous build. In the Render dashboard, pick the last-known-good deploy for the affected service and redeploy it. This is the fastest path back.
- Revert the merge. Revert the offending commit on the environment branch (
develop/qa/uat); the revert push triggers a clean redeploy. - For
/embedor vendored-library regressions, remember the rollback must include the step you shipped: re-push the engine with the previousstatic/embed-assets, or re-vendor the previous librarydist— reverting the app alone will not restore either.
For the authoritative, service-by-service rollback and incident procedures, follow the runbooks in luke-platform (README.md and the security/ program) and see Platform & Deployment.