# Publishing (https://docs.hub.ki/custom-apps/publishing)



The platform loads apps from a **source**: a location it reads app folders from.
You publish by placing your app folder in a source. A global admin registers the
source once and syncs it; an organization admin then decides who can use each
app. The admin steps are described in
[Administration](https://docs.hub.ki/custom-apps/administration).

## Package the folder
Each app is one folder containing its manifest, artifact, and optional icon:

```
apps/
└── notes/
    ├── manifest.json
    ├── app.html
    └── icon.svg
```

The folder name is arbitrary — the platform identifies an app by the `appId` in
its `manifest.json`. Remember the artifact must be a
[single file](https://docs.hub.ki/custom-apps#constraints) of at most 10 MB.

An app version is the pair `appId` + `version`. To keep several versions
available at the same time (an organization can be
[pinned to one](https://docs.hub.ki/custom-apps/administration#grant-it-to-organizations)),
keep one folder per version:

```
apps/
├── notes-1.0.0/
└── notes-1.1.0/
```

The same `appId` + `version` must not appear in two folders; both are skipped.
An `appId` belongs to the first source that published it — another source
declaring the same `appId` is ignored.

## S3 sources
An S3 source is a bucket plus a **prefix**. Every folder below the prefix that
contains a `manifest.json` becomes an app, at any depth:

```
s3://my-bucket/apps/notes/manifest.json      →  prefix "apps"
s3://my-bucket/apps/notes/app.html
```

Upload with any S3 client, for example the AWS CLI:

```sh
aws s3 sync publish/ s3://my-bucket/apps/ --delete
# S3-compatible storage: add --endpoint-url https://s3.example.com
```

Requirements:

* A **prefix is mandatory**; the bucket root cannot be a source.
* For an external bucket the admin enters the endpoint, region, bucket and an
  access key. The key needs to **list** the bucket and **read** objects under
  the prefix (`s3:ListBucket`, `s3:GetObject`); it never writes.
* The endpoint must be an **`https://` URL on a publicly resolvable host**.
  Private addresses, `localhost` and plain `http://` are rejected.
* At most **10,000 objects** may live under the prefix; beyond that the sync
  refuses to run rather than publish a partial catalog. Keep build leftovers out
  of the prefix.

A source can also read from the platform's own storage (no endpoint or
credentials), which is how first-party apps are shipped.

## GitHub sources
Reading app folders from a GitHub repository, with a webhook that re-syncs on
push, is implemented on the platform but **not yet selectable** in the admin
interface ("Coming soon"). Use an S3 source until it is enabled.

## Syncing
Nothing happens on upload. A source is read when a global admin chooses
**Sync now**; S3 sources are **not** synced on a schedule. Each sync:

1. lists the folders and validates every `manifest.json`
   ([rules](https://docs.hub.ki/custom-apps/manifest#validation)),
2. copies the artifact and icon of each valid app into the platform,
3. publishes the valid apps, and **unpublishes apps and versions that are no
   longer found** — together with the scope approvals and version pins that
   organizations had set for a removed app.

An app that fails validation is skipped. If it was published before, skipping it
means it is no longer found, so **a broken manifest takes a live app offline**
on the next sync. As a safeguard, a sync that finds no valid app at all, while
the source currently publishes some, is refused entirely.

The admin interface reports only how many apps were skipped, not why. Check a
manifest against the [validation rules](https://docs.hub.ki/custom-apps/manifest#validation)
before uploading; the most common causes are listed there.

## Updating an app
Ship changes by bumping `version` in the manifest, replacing the artifact, and
asking for a re-sync. Organizations that follow **Latest** move to the highest
version immediately; organizations pinned to a version stay on it as long as
that version's folder is still in the source. If a pinned version disappears
from the source, the app stops being available to that organization until its
pin is changed.

Two things to know before you bump:

* The [key/value store](https://docs.hub.ki/custom-apps/sdk#store) is scoped to the app
  **version**. Users start with an empty store on every new version — migrate
  anything that must survive into the [workspace](https://docs.hub.ki/custom-apps/sdk#workspace).
* Re-syncing an unchanged `version` with a different artifact overwrites that
  version in place. That is convenient while testing, but give real releases a
  new `version` so pinned organizations get what they pinned.

Users do not need to reinstall after an update unless the major `sdkVersion`
changes.
