Packaging And Updates
Introduction
An app moves between systems as a zip file. You export it from the admin panel of the system where it runs, and install that zip on another system. A system can also install an app from a zip of its repository, or from the DoCloud App Library.
Installing and updating are the same operation. When the app is already installed, the package's files replace the installed ones and the manifests are merged, so the installation keeps its own state.
All of this happens in the admin panel under Apps, which has three tabs: Installed Apps, DoCloud App Store and Upload App. Every step needs the PHP zip extension (see Requirements below).
How to export an app
Open Apps > Installed Apps, open the app and click Download App. The admin panel calls system_admin_app/export_app, which:
- Copies
api/apps/<app>/,apps/<app>/andapi/admin/apps/<app>/into a temporary folder. The backend folder is required. The other two are copied when they exist. - Copies the SQL file named in the manifest's
<run><sql>, if there is one, from the backend folder intodatabase/. - Strips old cache-busting parameters (
?ver=,&lpt=,?v=) from the JS imports in the copy. The installed files aren't changed. - Generates
config.xmlfrom the manifest (see below). - Zips the result to
downloads/exports/<app>.zipand returns its URL, which the browser downloads.
The zip looks like this:
myapp.zip
config.xml
backend/
myapp/
myapp.xml
myappController.class.php
...
frontend/
myapp/
app-config.json
...
adminpanel/
myapp/
app-config.json
...
database/
myapp_seed.sql
Everything in the three folders is included, hidden files too. Remove notes, test data and local files from them before you export.
The generated config.xml
config.xml is what the install step reads first. It is a projection of the manifest, backend/<app>/<app>.xml:
| Element | Content |
|---|---|
<info> | Copied whole from the manifest. |
<user_permissions> | Only the permissions with auto_update="true", as a flat list without <category> groups. |
<admin_panel_permissions> | The same, for admin permissions. |
<user_notifications> | Only the notifications with default_enabled="true", as a flat list. |
<run> | Copied whole, or empty. |
<prerequisites> | Copied whole, or empty. |
<paths> | Always frontend, backend, database and adminpanel. |
For the helloworld app it looks like this (the <info> block is shortened):
<?xml version="1.0" encoding="UTF-8"?>
<app>
<info>
<app_name>helloworld</app_name>
<display_name>Hello World</display_name>
<app_version>1.2.0</app_version>
<api_version>0.0.21</api_version>
<status>active</status>
<release_date>2026-10-07</release_date>
</info>
<user_permissions>
<permission display_name="Get hello world data" name="get_hello_data" auto_update="true"/>
<permission display_name="View hello world app" name="view" auto_update="true"/>
</user_permissions>
<admin_panel_permissions>
<permission display_name="view setting page" name="view" auto_update="true"/>
<permission display_name="Get hello world data" name="get_hello_data" auto_update="true"/>
</admin_panel_permissions>
<user_notifications>
<notification name="test" display_name="test notification" description="test notification" default_enabled="true">
<email active="false"/>
<push active="false"/>
<sms active="false"/>
</notification>
</user_notifications>
<run/>
<prerequisites/>
<paths>
<frontend>frontend</frontend>
<backend>backend</backend>
<database>database</database>
<adminpanel>adminpanel</adminpanel>
</paths>
</app>
Tables, options, roles and the rest of the manifest aren't in config.xml. The install reads them from the packaged backend/<app>/<app>.xml.
How to install an app
Installing has two steps. First the system receives the zip, extracts it to temp/apps/extracted/<zip name>/ and checks it. Then the admin confirms and the system installs it with system_admin_app/update_app.
From an export
Open Apps > Upload App and choose the zip. Only .zip files are accepted. The panel shows the app's details and an Install button.
The check (check_app_zip) reports:
- API compatible: the system's
api_versionis the same as or newer than the app's<api_version>. - App exists: the app is already installed, so this is an update.
- Update compatible: the package's
<app_version>is the same as or newer than the installed one.
From a repository zip
Since 0.0.30 you can install a zip of the app's repository, such as GitHub's Download ZIP, without exporting it first. The repository needs the dev workspace layout (see Dev Workspace), with the manifest at backend/<app>/<app>.xml.
On upload the system normalises the archive:
- A wrapping folder is fine. If the zip holds one folder (ignoring
__MACOSX) and that folder containsconfig.xmlor abackend/folder, its contents are moved up. This also coverszip -r myapp.zip myapp/. config.xmlis created if missing. If the app ships aconfig.xmlnext to its backend code (backend/<app>/config.xml), that file is used. Otherwise one is generated from the manifest, exactly as an export does.- Old cache-busting parameters are stripped from the JS imports in
frontend/andadminpanel/. - The SQL file is mirrored. The file named in
<run><sql>is copied betweendatabase/andbackend/<app>/, whichever is missing, so the app installs and later exports the same way.
An archive that already has config.xml at its root is installed as it is.
From the DoCloud App Library
Open Apps > DoCloud App Store, pick an app and a version. The system downloads the zip from DoCloud (install_app_from_docloud), normalises it as above and shows the install popup. Here Install is disabled unless the app is compatible.
For an installed app, the app's update list in Installed Apps shows the versions in the library. Get the update takes the same route.
To publish your app to the library, export it and add the zip in DoCloud. See Docloud.
How to choose the install location
On a development system, the install screens offer an Install location. It appears when <system_environment> is development and dev/ exists, or when the app already lives in dev/.
- Standard directories: the files go to
apps/<app>/,api/apps/<app>/andapi/admin/apps/<app>/. - dev/ workspace: the files go to
dev/<app>/frontend/,dev/<app>/backend/anddev/<app>/adminpanel/, and the framework links them in straight away. Only the parts the package contains get a folder.
An installed app is always updated where it already lives:
- an app in
dev/<app>/can't be updated into the standard directories, - an app in the standard directories can't be installed into
dev/, and - an app linked from somewhere other than
dev/<app>/can't be installed intodev/.
To move an app between locations, remove it and install it again. update_app takes install_location (standard or dev). Without it, the app's current location is used, and standard for a new app. This needs DoFramework 0.0.41 or later.
An app installed into dev/ isn't a git repository. Run git init in dev/<app>/ if you want to work on it.
What an install does
update_app runs these steps, in this order:
- If the app is installed, merges the packaged manifest and both
app-config.jsonfiles with the installed ones (next section). - Copies the package's
backend/,adminpanel/andfrontend/folders over the target folders. Files are added and overwritten, never deleted, so a file that a new release dropped stays on the system. - For a
dev/install, creates the links. - Creates each
<createTables>table that doesn't exist yet (CREATE TABLE IF NOT EXISTS). Existing tables aren't changed. An app with migrations runs its pending migrations here instead (see Database Migrations). - Seeds the app's
<app_options>. - Runs the
<run>SQL file from the package'sdatabase/folder, and the<run>script. - Sets the app's entry in the admin panel permission set to the
auto_updateadmin permissions inconfig.xml. - Updates the frontend cache.
An install doesn't do everything a reinitialise does. It doesn't add new columns to existing tables, provision <roles>, or add the auto_update user permissions to the system_admin role. Reinitialise the app after every install and update, then log out of the admin panel and back in.
How an update merges the manifests
Since 0.0.33, the package owns the app's manifest and the installation owns only a little state. Before the files are copied, the installed files are rewritten:
api/apps/<app>/<app>.xml
- The packaged manifest is the base, so new tables, options, notifications,
<app_permissions>,<run>entries and permissions all arrive with the update. <info><status>is taken from the installed manifest, so an app disabled on this system stays disabled.- In
<user_permissions>and<admin_panel_permissions>, an installed permission is kept when the package doesn't ship a permission of that name and it doesn't haveauto_update="true". It goes back into the category of the same name, which is created if needed. - An installed
auto_update="true"permission that the package dropped is removed. - If the packaged manifest has no permission block, or an empty one, the installed block is kept whole. A broken package can't wipe the installation's permissions.
apps/<app>/app-config.json and api/admin/apps/<app>/app-config.json
- The packaged file replaces the installed one, so new resources, menus and widgets ship.
statusis taken from the installed file, when both files have it.
Since 0.0.41 a system update merges the system apps' manifests the same way.
Reinitialise or update?
| Install or update | Reinitialise | |
|---|---|---|
| Copies files, merges manifests | Yes | No |
| Creates missing tables | Yes | Yes |
| Adds missing columns, changes columns, adds keys | No | Yes |
Seeds <app_options> | Yes | Yes |
Runs <run> | Yes | Yes |
Adds auto_update user permissions to system_admin | No | Yes |
| Admin panel permissions | Replaces the app's entry with the package's list | Adds the auto_update ones |
Provisions <roles> | No | Yes |
Reinitialise when you changed the manifest on the system itself, for example while developing in dev/. Update when you have a new package. After an update, reinitialise too. Reinitialise is Apps > Installed Apps, open the app, Reinitialize, or AppManager::initialize_app('myapp'). See App Manager for what it does in detail.
How to set the version fields
Each release has its version in three places. Change all of them together:
| File | Fields |
|---|---|
api/apps/<app>/<app>.xml | <info><app_version> and <info><release_date> |
apps/<app>/app-config.json | version and release_date |
api/admin/apps/<app>/app-config.json | version and release_date |
Nothing checks that they agree for your app (./bump-version checks only the framework's own system apps), but different parts of the system read different files. The admin panel's app list and the install check use the manifest. The browser gets the app-config.json values.
<info>
<app_name>myapp</app_name>
<app_version>1.2.0</app_version>
<api_version>0.0.44</api_version>
<release_date>2026-10-05</release_date>
</info>
"version": "1.2.0",
"release_date": "2026-10-05",
<api_version>is the oldest framework version the app works with. Installing on an older system is reported as not compatible.- Versions are compared number by number, split on
.. Use the samex.y.zform everywhere.
How to remove an app
Open Apps > Installed Apps, open the app and click the delete icon. The confirmation has one option, Remove database records and tables.
- Unticked: the app's files are deleted. Its tables and data stay in the database, and are used again if you reinstall the app.
- Ticked: each table in
<createTables>(<app>_<name>) is dropped as well. This can't be undone. An app with migrations is reset instead, and its migrations without adown()leave their tables in place (see Database Migrations).
Removal also runs the meta cleanup in the manifest's <uninstallConfiguration>. It doesn't remove the app's grants from roles or from the admin panel permission set.
For an app in the standard directories, removal deletes apps/<app>/, api/apps/<app>/ and api/admin/apps/<app>/. Export the app first if you have no other copy. An app that lives in dev/<app>/ is unlinked and its folder is moved to dev/.removed/ instead (see Dev Workspace).
Requirements
- PHP zip extension (
ext-zip). Uploading, installing from the library and exporting all useZipArchive. Without it, upload and library installs answer "The PHP zip extension is not installed on this server, so archives cannot be created or extracted." An export answers "Error creating zip." and writes a critical entry to the log. allow_url_fopen. Library installs download the zip withfile_get_contents().- Write access for the web server to
temp/,downloads/, the three app folders and, fordev/installs,dev/.
Admin actions
These are system_admin_app actions. Each one needs an admin-panel session that holds the action. They answer with HTTP 200 and report failure in response.success and response.statusMsg. See Architecture for the response format.
check_app_zip
Description:
The check_app_zip action receives an uploaded zip, extracts and normalises it, and reports what it holds and whether it can be installed.
Syntax:
const form = new FormData();
form.append("app_zip", file_input.files[0]);
const res = await fetch(XP.getApiUrl() + "/system_admin_app/check_app_zip", {
method: "POST",
credentials: "include",
body: form,
});
Parameters:
app_zip: The uploaded.zipfile.
Return Value:
Object:data.infois the<info>block plusapp_size,temp_app_dir(pass it toupdate_app) and, for an installed app,current_app_version.data.compatibilityhasapi_compatible,app_exist,app_update_compatibleandtotal_compatible.data.install_locationhasdev_availableandcurrent(standard,dev,linkedornull).data.pathanddata.prerequisitescome fromconfig.xml.
install_app_from_docloud
Description:
The install_app_from_docloud action downloads a zip from a URL, extracts and normalises it, and reports on it like check_app_zip. The admin panel passes the URL of a version in the DoCloud App Library.
Syntax:
const form = new FormData();
form.append("app_url", version.app_url);
const res = await fetch(XP.getApiUrl() + "/system_admin_app/install_app_from_docloud", {
method: "POST",
credentials: "include",
body: form,
});
Parameters:
app_url: URL of a.zipfile.
Return Value:
Object: The samedataascheck_app_zip, withoutapp_size.
update_app
Description:
The update_app action installs or updates an app from a zip that check_app_zip or install_app_from_docloud extracted. It runs the steps in What an install does.
Syntax:
const form = new FormData();
form.append("app_name", info.app_name);
form.append("app_temp_dir", info.temp_app_dir);
form.append("install_location", "standard");
const res = await fetch(XP.getApiUrl() + "/system_admin_app/update_app", {
method: "POST",
credentials: "include",
body: form,
});
Parameters:
app_name: The app name from the package's<info>.app_temp_dir: Thetemp_app_dirvalue from the check.install_location: Optional.standardordev. Defaults to where the app is installed now, orstandard.
Return Value:
Object:datahasinstall_location,backend_copied,frontend_copied,admin_panel_copied,database_executed,app_options_executed, andsql_executedandscript_executedwhen the app has a<run>block.
export_app
Description:
The export_app action packages an installed app as a zip, as described in How to export an app.
Syntax:
const form = new FormData();
form.append("app_name", "myapp");
const res = await fetch(XP.getApiUrl() + "/system_admin_app/export_app", {
method: "POST",
credentials: "include",
body: form,
});
Parameters:
app_name: The installed app's name.
Return Value:
Object:data.download_urlis the zip's URL anddata.file_nameis<app>.zip.
reinit_app
Description:
The reinit_app action reinitialises an installed app with AppManager::initialize_app().
Syntax:
const form = new FormData();
form.append("app_name", "myapp");
const res = await fetch(XP.getApiUrl() + "/system_admin_app/reinit_app", {
method: "POST",
credentials: "include",
body: form,
});
Parameters:
app_name: The installed app's name.
Return Value:
Object:datais the result ofinitialize_app().
remove_app
Description:
The remove_app action removes an installed app, as described in How to remove an app.
Syntax:
const form = new FormData();
form.append("app_name", "myapp");
form.append("remove_database_tables", "true");
const res = await fetch(XP.getApiUrl() + "/system_admin_app/remove_app", {
method: "POST",
credentials: "include",
body: form,
});
Parameters:
app_name: The app's name. Only letters, digits and_are accepted.remove_database_tables: Optional. The stringtruedrops the app's tables. Anything else keeps them.
Return Value:
Object: For an app indev/,data.dev_repo_moved_tois the folder it was moved to, such asdev/.removed/myapp-20261005-101500/.