Reference

App catalog JSON

The complete field reference for an Openship App definition — the JSON that turns a set of containers into a one-click install, with generated secrets, a curated settings form, and a connection card.

An App is a JSON file that wraps a deployment into a one-click, focused experience. Instead of creating a project, adding a service per container, hand-writing the images and the env that wire them together, generating and matching a database password across two services, and then hunting through raw env tabs to change a setting — you click the app, fill a short form, and deploy.

So an App is not a new runtime. It is metadata over the normal multi-service deploy: a repo-less services project marked isApp. Same engine underneath, much shorter path for the user. This page documents every field you can author. To actually submit one, see Add an app.

Add the $schema line for editor autocomplete and inline validation:

{
  "$schema": "https://openship.io/app.schema.json",
  "id": "it-tools",
  "name": "IT-Tools",
  "description": "A handy collection of developer and sysadmin utilities.",
  "kind": "template",
  "logo": "it-tools",
  "category": "other"
}

It is JSON, not JSONC — no comments, no trailing commas.

Unknown keys are ignored, not rejected

The validator strips keys it does not recognize instead of failing. That is deliberate: it is what lets a newer app definition stay installable on an older Openship, and it is why growth is always additive. The trade-off is that a misspelled field name fails silently — it is dropped, not reported, by both the published schema and the server. So keep the $schema line and let your editor autocomplete field names rather than typing them from memory.

Two kinds

kindWhat it does
templateInstantiates the services defined in the entry (a backend plus its database, a CMS plus its database, …) and deploys them through the compose/services path. This is the common case.
flowProvisioning already has a bespoke wizard (for example the mail stack). The entry just points at that wizard via flowHref; it does not instantiate services.

Top level

Prop

Type

Services

Each entry becomes one container in the created project.

Service name is the hostname

Services reach each other by name on the project network. A service named ghost-db is reachable at the host ghost-db — which is why a database host in environment is just the plain service name, never an IP or a localhost address.

Prop

Type

Service resource defaults

Declare resources for stacks whose components need different sizes. On Cloud, the installer saves each profile as the service's normal resource settings. Self-hosted services remain uncapped unless the operator configures limits. Reopening or retrying a failed install fills missing profiles in one transaction and preserves explicit service and project resource settings, files, environment and secrets. Catalog updates do not resize already deployed apps or change frozen deployment snapshots.

On Cloud, enabled services share a workspace. Its CPU is the rounded-up sum of the service limits, RAM includes 512 MB for Docker and the OS, and disk uses the largest service disk request with an 8-GB floor. Source builds reserve additional resources. Oblien enforces the namespace's per-workspace and total limits; a template cannot raise them or grant credits.

Supabase's profile requests a 4-vCPU, 8-GB RAM, 40-GB disk workspace, following its published system requirements. With the current retail catalog it fits Team's per-workspace limits. Existing allocations, a stricter saved offer or explicit service overrides can still require a different allocation. Deploy an API version with resource-profile support before relying on a catalog update; older installers ignore this optional field.

Building an image

Most services pull a prebuilt image. Some can't: the upstream ships a base image that still needs extra packages and a provisioning entrypoint baked in — Neon's compute node is the canonical case. For those, set build instead of image. It is an inline Docker build context that Openship materializes at deploy time and builds on the deploy host through the normal compose build pipeline. Building subsumes an entrypoint override: the Dockerfile sets its own ENTRYPOINT.

Set exactly one of image or build per service. Neither (nothing to run) and both (ambiguous) are rejected — service "<name>" must set exactly one of image|build.

Prop

Type

COPY paths are prefixed by the service name

This is the rule authors get wrong first. Every buildable service is materialized under a subdir named after the service, inside one shared build context, and docker build runs with that shared root as its context. So COPY/ADD sources are relative to the root, not to the service's own subdir:

# service "compute", with files: [{ "path": "compute.sh", ... }]
COPY compute/compute.sh /shell/compute.sh   # ✅ prefixed with the service name
COPY compute.sh /shell/compute.sh           # ❌ COPY source not found → build fails

A files[].path is relative to the service's own subdir; you COPY it as <service-name>/<path>.

Values and build args

{{config:KEY}} placeholders are resolved in dockerfile and in every files[].content at install time, exactly as in environment values — so a generated secret or an install-step answer can flow into the build.

There is no args map. A build ARG the Dockerfile declares is fed from the project's build env: the runtime passes each env var as --build-arg. Declare the value as a normal environment entry (or a configField) and reference it with ARG in the Dockerfile.

Building happens on the deploy host

A large base image builds where it deploys. On a small host a heavy build — Neon's compute node pulls a multi-hundred-MB base — can be slow or memory-bound. That cost is inherent to the app, not to Openship.

Two field systems

The single most common authoring mistake is mixing these up. There are two places to declare env, split by role:

configFieldssettings with installStep: true
ForMachine-generated or derived envHuman inputs the wizard renders
Rendered as a form input?No — resolved server-sideYes — text, select, number, …
Typical usegenerate: "secret" / "jwt", generateGroupa name, a toggle, a chosen option

If you want the user to fill it in, it is an installStep setting — not a configField. Use configFields only for values the operator never types.

configFields

Generated or derived env only. Each entry maps to one env key on one service.

Prop

Type

"configFields": [
  { "key": "MYSQL_ROOT_PASSWORD", "service": "ghost-db", "label": "Database password",
    "generate": "secret", "generateGroup": "ghostdb", "secret": true },
  { "key": "database__connection__password", "service": "ghost", "label": "Ghost DB password",
    "generate": "secret", "generateGroup": "ghostdb", "secret": true }
]

Both fields share generateGroup: "ghostdb", so one generated password is written to both sides and the two always match.

Settings

The curated settings form, grouped. A field with installStep: true is also collected in the install wizard — this is the human-input surface. Everything else is day-2 configuration. Validation is enforced live in the form and server-side on save.

Groups:

Prop

Type

Fields:

Prop

Type

Form layout

The catalog controls grouping and field widths. The dashboard supplies the controls, spacing, colors, and responsive behavior through the same form renderer used for installed app settings. Fields remain defined once in settings; only fields with installStep: true appear at install.

For a compact install such as Convex, add this at the top level:

{
  "installLayout": {
    "settings": "split"
  }
}

split puts the editable project name and install fields in two adjacent cards, stacking them when the available width is too small. Field labels identify each input without an extra card heading. This suits small forms such as Convex's Name and Instance name.

single combines the name and install fields in one card. grouped keeps a separate name card and renders each visible settings group with its label and description. Empty groups are omitted. Without installLayout, the existing separate name card and single vertical settings card remain. When reopening a draft, its project name stays hidden and the remaining settings use the full width.

columns defaults to 1. Set it to 2 for fields that fit side by side; the form stacks them when its available width is too small, including when dashboard navigation takes more room. In grouped mode, a group's own columns overrides the install default. That group setting also applies after installation. Set fullWidth: true on a field such as a long textarea to span both columns. Field order follows the catalog; conditional and advanced visibility still apply.

These optional hints are returned in the catalog API for consumers to render. They do not change environment values, required-field validation, or routing, and older clients can ignore them. Use these supported hints rather than CSS classes or executable layout expressions in catalog JSON.

Connection

The post-install Connection card: the URLs and keys a user needs, plus the handover into another project.

Prop

Type

Each output:

Prop

Type

Output sources

An output source (and each variant source) must match one of these three forms:

FormResolves to
env:<service>:<KEY>That env value on that service
publicUrl:<service> or publicUrl:<service>:<port>The public URL for that service, optionally for a specific route
template:…A composed string, embedding {{env:svc:KEY}} placeholders

Anything else fails validation, and the <service> part must name a declared service.

Prepare

Commands run inside a service container after it starts — never a host shell. Their stdout can be captured and persisted as an env var (this is how an app like Convex gets its admin key).

Must be re-run safe

A prepare step can run again on a later deploy. Write it idempotently, or gate it with once.

Prop

Type

phase: pre-deploy is reserved

The schema accepts "pre-deploy", but the engine does not run it yet. Do not rely on it. For pre-run database initialization use files to write into /docker-entrypoint-initdb.d, combined with dependsOn and a healthcheck.

Endpoints

What the install wizard asks you about — how to reach each port. Omit endpoints entirely and Openship derives one public http endpoint per declared route of each exposed service.

Defaults follow the app's intended use on every deployment target:

  • Public HTTP endpoints, such as dashboards and APIs, start with Domain routing.
  • Raw TCP endpoints, such as databases, start Internal only unless their scope is public.
  • An explicit defaultMode overrides these defaults within allowedModes. Use it for choices such as a port-only API. Local or internal HTTP endpoints default to port-only access.

A Cloud connection only selects the domain provider: a managed free domain when connected, or a custom domain when disconnected. It does not switch a public UI to port-only routing. Custom domains still need a hostname and DNS setup. Users can choose another allowed mode; saved draft choices take precedence over fresh-install defaults.

Prop

Type

Provides and requires

The connection graph between projects. provides advertises a connectable bundle; requires declares a connection this app needs from another project — the install wizard then offers a same-org source picker and wires it in one shot. Same-org only, and always user-confirmed.

Prop

Type

Host requirements

minResources describes the host capacity recommended for the app. Declare it when upstream documents a meaningful requirement, such as 8 GB for a multi-service analytics stack. It does not set container limits.

{
  "minResources": { "memoryMb": 8192, "cpuCores": 4 }
}

On self-hosted destinations, the installer and deployment preflight compare these recommendations with the destination's Docker daemon (NCPU, MemTotal). A shortfall shows a warning; it never prevents a first installation or redeployment. An unavailable capacity reading means unknown, and no shortfall is inferred. A 10% tolerance accounts for memory reserved by the operating system on machines sold with round RAM sizes.

On Cloud, the installer checks the runtime configuration, including service overrides, against the organization's plan. It offers an upgrade when the allocation does not fit. Advanced setup remains available to adjust the configuration. Source builds use the remaining shared pool capacity reported by Oblien, with optional saved build caps; image-only templates need no source builder. Deployment checks again before queuing work, and Oblien enforces live capacity atomically when allocating or resizing a workspace.

GET /apps/catalog/{id}/host-fit exposes this preview. Pass projectId to include an existing draft's saved resource settings. The preview does not modify the project, allocate resources, or grant a deployment allowance.

Files

Generated config files bind-mounted into a container at deploy — for apps that need a config file, not just env (an init .sql, for example). Supports placeholders.

Prop

Type

Self-hosted and desktop only

files are not applied on Openship Cloud.

Management

Overrides how the installed app is managed.

ValueEffect
{ "kind": "schema" }Render the curated settings form from settings.
{ "kind": "custom", "href": "/emails" }Send the user to a bespoke surface instead.

Omit it and Openship derives the behavior: schema when settings exist, otherwise the raw project tabs.

Placeholders

Resolved at install. Usable in environment values, files[].content, and connection outputs:

PlaceholderResolves to
{{publicUrl:<service>}}That service public URL. Add :<port> for a specific route.
{{config:<KEY>}}A generated config value by key — for example a generate: "secret".

Localized strings

Several display-only fields accept either a plain string or an inline per-locale map:

{ "title": { "en": "Creating the admin key", "ar": "إنشاء مفتاح المدير" } }

This applies to prepare[].title, prepare[].description, requires[].label, connection.firstLogin.username / password / note, connection.guide.intro / useHint, connection.outputs[].sourceLabel, and variants[].label.

Validation

Beyond field types, these referential checks run at the gate — for both curated and custom apps — so a dangling reference fails immediately instead of at deploy time:

  • Every service reference resolves to a declared service — in configFields, prepare, endpoints, files, settings[].fields, and connection.outputs[].service.
  • Every connection.outputs[].source and variants[].source matches env:<service>:<KEY>, publicUrl:<service>[:<port>], or template:….
  • Every provides[].outputRefs entry names a real connection.outputs[].id.
  • Every jwtSecretGroup matches a declared generateGroup.
  • flowHref starts with / — it can never be an external URL.
  • category is one of the seven allowed values.

Stability and versioning

The catalog JSON is a stable, versioned public API. Growth is additive and backward-compatible: new optional fields and new enum members default to prior behavior, and a field is never repurposed or removed within a schema version.

Prop

Type

One file per app — never author multiple versions. The repo always holds the single latest file. An older instance simply keeps the copy bundled in its own build. When an instance is older than an app minEngine:

  • the app is bundled there → it keeps serving the bundled copy, and may note that an update is available;
  • the app is brand new → the catalog shows a guided Requires Openship ≥ X card and install is refused, client and server. Never a silent disappearance.

See also

On this page