Deployments

The call comes back before your software is on the box.

Ten front doors deploy a release: deploy_spa, deploy_embed, deploy_pwa, deploy_mpa, deploy_api, deploy_cron, deploy_emitter, deploy_preview, deploy_user_group and deploy_mini_app. Each one sets exactly one field, req.data.deploy_type, and hands the request to deploy_app. deploy_app is not declared in cpi_methods, so there is no generic deploy call to make: the type is chosen by which name you called, not by an argument you passed. What that function does first is not provisioning. It clones a base object, sets app_type to the deploy type, sets is_deployment true, attaches the whole release doc as app_release_doc and saves the result into xuda_master, all before the second droplet is asked for. The public name is computed from that document id rather than registered: app_access_url is the id followed by .xuda.app, and no DNS API call happens anywhere in the deploy path. A Cloudflare worker routes everything under xuda.app to a region, and that region reverse-proxies to the box. Then the work splits in two. config.json declares 12 job steps for a link deployment, and all 12 cover the create phase. When the droplet first boots and curls the webhook back, the same job is reopened with a different and longer list: 24 names read from _INSTALL_STEPS, from connecting to server through to activating the server. The bridge between the two phases is a memcached key, install_job_ plus the app id, written when the droplet is created with a 3600 second TTL. If it has expired by first boot the install still runs, just with no progress bar.

Ten front doors, one deploy_app
12 create steps, then 24 install steps
A document before it is a machine
The call comes back before your software is on the box.
Shared or dedicated

Shared versus dedicated is a connection string, and it decides the other three rows.

The difference between a project in the studio and a deployment is not a setting, it is which CouchDB the connection string resolves to in common/xuda_node_common.mjs. A studio project resolves to the region shared projects database. A deployment resolves to its own datacenter, https on 6984 publicly or http on 5984 over the private address, using that datacenter generated admin password, and its database name is computed, not chosen: db_ plus the datacenter id. Read the four columns below down rather than across. What the front door declares is what you can control: deploy_spa takes app_datacenter or create_datacenter, deploy_mini_app takes neither, and deploy_vps takes app_name, app_region and app_server_type with no release_doc_id at all.

Studio project
Mini app
Link deployment
VPS
Where the data sitsThe region shared projects CouchDB, resolved by connection string.db_ plus the datacenter id, the same rule as any other deployment.db_ plus the datacenter id, on that datacenter own CouchDB. Every deployment attached to it reads and writes the one database.None of this runs. deploy_vps writes its own app doc with app_type vps and never calls deploy_app.
Machines this call createsNone. It is a project, not a deployment.One deployment droplet. deploy_mini_app declares no datacenter field at all, so it cannot create one.Up to two: the datacenter droplet from datacenter_image_id, then the deployment droplet from deployment_image_id. Pass data.app_datacenter and the first is skipped.One VM. There is no release_doc_id field on the call, so no build is attached to it.
Firewall the path createsNothing to attach one to.None. mini_app shares a switch case with vps and webserver that breaks without creating a firewall.One inbound rule: tcp 443 from 0.0.0.0/0 and ::/0. Outbound icmp, tcp and udp are open. The datacenter firewall opens 5984 only to droplets tagged datacenter_for_ plus the project.None, deliberately. On the farm path proxmox_apply_firewall returns early with vps: customer-managed firewall.
Monthly floorNothing on the infrastructure line.24 dollars, service_hosting_basic. min_plan free is the plan gate, not the bill.97 dollars the first time: 24 for the deployment server plus 73 for optimized_hosting_2, on top of the 49 dollar Pro membership. user_group adds 35, emitter 75, pwa and preview 15.Whatever the slug costs. deploy_vps declares no min_plan.
12

Declared job steps, and all of them are the create phase

24

Install steps the same job reopens with at first boot

2

Servers a first deployment stands up, datacenter then app

$97

Cheapest legal first deployment per month, before membership

The box

One directory, filled entirely from the outside.

The install is ssh and scp running from the platform region against root at the box address. copy_source_path points at builds/ plus the project plus the build id, and the files are pushed into /var/xuda. There is no image registry, no artifact upload and no pull initiated by the box: it is a machine that gets written to. Each row below has two faces on purpose, what the install copies in and what serves or reads it afterwards, because several of them are not the same thing at all.

Two things this drawing does not promise. The build snapshot is not hermetic: several of those copies read as scp from the build folder or else scp from the platform live copy, so a box can be assembled partly from whatever the platform region happens to hold at install time rather than wholly from the frozen build. And every ssh and scp in the path runs with StrictHostKeyChecking=no and UserKnownHostsFile=/dev/null, because farm guest addresses are recycled from a pool and a pinned host key would break the next VM to get that address.
The build

A build writes four things, and none of them is an archive.

Nothing in a Xuda build produces a tarball you could carry somewhere else. build_app runs the program checker, increments one integer, writes documents into the project database, and lays down a folder on the platform drive. That folder is what the install later reads over scp: a build is a staging area on the platform, not an artifact handed to you. The checker in front of it is advisory: build_app defaults ignore_errors to true, so the error count lands in the build info document and the release proceeds anyway.

  • The version is an integer, not a hash and not a semver. app_publish_info.app_lastBuild is incremented by one at build_module line 327, and that number is the build_id every document and every path downstream carries. The version field beside it is whatever the caller passed, default 0, so it carries no meaning the platform assigns.
  • The project is written out as documents: one deployed_app doc per studio doc in the project database, each carrying build_id, plus one release doc with stat 3. The id of that release doc is what data.release_doc_id names on a deploy call, which is why a deployment points at a frozen row rather than at whatever is newest.
  • The folder is builds/<project>/<build>/ on the platform drive: a copy of the studio drive as studio_assets, plus deployment/, common/*.mjs, runtime/xuda-sw.js, dist/runtime, dist/images/devices and two prebuilt Nuxt outputs. Cache busting happens here rather than at serve time, and it is a rename: every file under dist/runtime/js carries .<build_id> before its extension.
  • What a build does not do is deploy. build_app sets update_required true on every live deployment of the project and sends a new_version_available notification, and stops there. No box is touched by building: each deployment is updated by its own update_app_version call. The snapshot is not hermetic either, since several install copies read scp <build folder> || scp <platform live copy>.
One build, four writes
app_lastBuild
+1
incremented, never hashed
deployed_app docs
1 per studio doc
each stamped with build_id
release doc
1, stat 3
its id is your data.release_doc_id
Folder on the drive
1
builds/<project>/<build>/, read later over scp
The run

One call, two machines, and four ways it stops.

Ten front doors, one function. deploy_spa, deploy_pwa, deploy_api and seven siblings each set req.data.deploy_type on the request and hand off to deploy_app, which is not declared in cpi_methods, so there is no generic deploy call to make: the type is decided by which name you call. The run is the same every time: it refuses on documents before it spends money, creates at most two droplets, and hands the second to an install phase the deploy job has already stopped describing.

The job

The job you started declares 12 steps. The box answers with 24 more.

config.json declares job_steps per method, and for a link deployment that list is 12 names with a job_timeout of 1800000 ms. It covers the create phase only: two servers, two firewalls, two backup plans. The install is a second phase, later, on the box, when the droplet first boots and curls the webhook home. At that moment the same job is reopened against a different list, the 24 names of _INSTALL_STEPS in deploy_module. Both are printed here in full.

config.json cpi_methods.deploy_spa.job_steps -> deploy_module _INSTALL_STEPS
declared at call time, 12 steps, job_timeout 1800000
01 initiating process
02 creating datacenter server
03 setting datacenter firewall
04 setting datacenter backup plan
05 connecting to datacenter server
06 installing datacenter resources
07 creating spa server
08 setting spa firewall
09 setting spa backup plan
10 connecting to spa server
11 installing spa resources
12 activating the server
the droplet boots, curls the webhook, install_job_<app_id> is read back
the same job is reopened against 24 steps
01 connecting to server
02 initiating directories
03 installing common resources
04 installing build data
05 copying studio drive
06 copying deployment resources
07 installing webserver resources
08 installing modules
09 installing dependencies
10 installing npm packages
11 installing plugins
12 installing plugins setup
13 installing preview resources
14 installing user group resources
15 copying pwa resources
16 creating ecosystem config file
17 creating ini files
18 creating admin ini files
19 creating nginx files
20 creating websocket service
21 creating self signed certificate
22 starting services
23 installing load balancer
24 activating the server
both lists end on the same three words. Only the second one is the box saying it.
One memcached key bridges the two lists: install_job_<app_id>, written when the droplet is created with a 3600 second TTL. If it has expired by first boot, the install still runs, with no progress bar in front of it.
Doing it

Three moves, and the file each one comes from.

A release deployment is not a command with flags. It is a method name, a document id, and a job that changes shape while you watch it.

  1. 01

    Pick the front door, name the release

    There is no generic deploy call. You call deploy_spa, deploy_pwa, deploy_api or one of the other seven names, and that choice is the deploy_type: each sets req.data.deploy_type and hands off to deploy_app, which cpi_methods does not declare. data.release_doc_id is mandatory: it is the id of the release doc your last build wrote with stat 3. cpi/deploy_module/index.mjs lines 418 to 443.

  2. 02

    Watch the job swap lists mid run

    The job reports the 12 create phase steps config.json declares. When the droplet first boots it curls the webhook, the job id is read back from the memcached key install_job_<app_id> written with a 3600 second TTL, and the same job reopens against the 24 name install list. config.json cpi_methods.deploy_spa.job_steps and cpi/deploy_module/index.mjs lines 4186 to 4241. A failed install leaves the app at app_status_code 2 until you call reinstall_<type>_resources.

  3. 03

    Take the activated box

    The install writes /var/xuda/ecosystem.config.js, then runs pm2 delete all, pm2 start and pm2 startup. spa, embed, pwa, mpa and mini_app run one webserver in cluster mode with instances max, while cron, emitter and api each run one fork mode process. The public name was computed from the document id, never registered: <_id>.xuda.app, routed by a Cloudflare worker to a region that reverse proxies to the box. cpi/deploy_module/index.mjs 6795 to 7040, cpi/router_module/index.mjs 1896 to 1992.

Honest limits

Eight things the deploy path does not do, starting with the one everybody assumes.

  • There is no rollback for a server deployment. update_app_version takes one field, app_id, and resolves build_id_current from app_publish_info.app_lastBuild, which is always the newest build there is. Only static websites can go backwards, through set_site_live_version. For a deployment, going back means building forward again.
  • A build does not deploy. build_app sets update_required true on every live deployment of the project and sends a new_version_available notification, then stops. Nothing is pushed to a box by the build itself. Each deployment is updated by its own update_app_version call, one at a time, and a project with six live deployments needs six of them.
  • The build snapshot is not hermetic. Several install copies read as scp from the build folder, or on failure scp from the platform's live copy, so a deployment can be assembled partly from whatever the platform region is holding at install time rather than wholly from the frozen build.
  • The cheapest legal first deployment is $97 a month of infrastructure: service_hosting_basic at $24, which is 1 vCPU, 2 GB and 50 GB of disk, for the deployment server, plus optimized_hosting_2 at $73 for the datacenter, on top of the $49 Pro membership. min_plan free is not free hosting either. deploy_mini_app still requires a service_hosting_ slug, cheapest $24, and it declares no datacenter field at all, so with only its declared fields it works on the account project and returns code -2 anywhere else.
  • Nine methods declare data.create_datacenter.app_region and app_server_type but not app_name, which deploy_app requires when it creates a datacenter inline. Send exactly the declared fields and the call comes back code -5, app_name is empty. Only deploy_datacenter declares that field.
  • deploy_load_balancer is farm-required, yet _render_haproxy_config pools only backends that are neither Proxmox-provisioned nor on a 10., 172.16-31. or 192.168. address. An all-farm backend set renders one comment: no reachable backends yet.
  • A failed install leaves the app at app_status_code 2. install_hosting_resources returns code -1, the webhook marks the job failed but never the app document, and nothing retries past four transient ssh attempts. The fix is a manual reinstall call for that deploy type.
  • The certificate on every box is self-signed: openssl req -x509 for 99999 days, RSA 2048, common name the app id under xuda.app. Every deploy ssh and scp runs with StrictHostKeyChecking=no and UserKnownHostsFile=/dev/null, because farm guest IPs are recycled from a pool.
Before you deploy

The questions the wizard does not answer

Six things read out of deploy_module, build_module and config.json, answered against the shipped code rather than against the intent.

Watch

The same path, with buttons.

If you would rather see the dashboard than read the field names, this is where the deploy type, the server type and the job steps are picked.

A deployment is a document, then a droplet, then an scp.

deploy_app writes the document into xuda_master before it asks for a machine, computes the public name off that document id, derives the database name off the datacenter, then pushes the build folder from the platform region into /var/xuda over ssh and starts it under pm2. Nothing in that path is a registry, an image push or a pull from the box. Which of the ten deploy types you want is a different question from how any of them work, and it is answered on the scenarios page. When you know the shape, start one from the dashboard.