---
title: Apps
url: https://www.tines.com/stories/docs/self-hosted/apps/
updated: 2026-08-26T13:40:25+00:00
---

*[tines.com](https://www.tines.com/llms.txt) › [Docs](https://www.tines.com/stories/docs/llms.txt) › [Self-Hosted](https://www.tines.com/llm/stories/docs/self-hosted.md)*

# Apps

*[View on tines.com](https://www.tines.com/stories/docs/self-hosted/apps/)*

Apps are a customizable, interactive front end for your Tines Stories workflows, and are fully supported on both cloud and self-hosted environments. This page focuses on the latter, so if you wish to learn more about Apps in general, see [this page](https://www.tines.com/stories/docs/apps/).

In self-hosted environments, the build process differs from Tines Stories Cloud and introduces a small number of infrastructure requirements: an up-to-date `tines-command-runner` image, and somewhere for a build to get its npm packages from. That can be the public npm registry, a registry on your own network, or the packages baked into the command runner image, so Apps can also be built in a fully air-gapped deployment.

# How app builds work on self-hosted

Every time an App's source files change, Tines Stories bundles the app on the server: it resolves the App's npm dependencies, compiles the TypeScript and React source with esbuild, and stores the resulting JavaScript bundle for the app to render.

On Tines Stories Cloud, that bundling runs in AWS Lambda. On self-hosted there is no Lambda, so bundling runs in `tines-command-runner`, the same container that already powers Run Script actions. The web server and background workers call tines-command-runner over HTTP on port `4400`, the same connection already required for Run Script.

Each app gets its own build workspace on the `tines-command-runner` container's disk, holding its `node_modules` directory. The first build for an App is a cold build and runs a full `npm install`, which can take quite a bit of time depending on the number of dependencies. Later builds reuse the cached dependencies and typically complete in a few seconds. When an app is deleted, Tines Stories removes its workspace.

The Tines app runtime libraries are sent to the command runner as part of the build itself, so they are never fetched from a registry.

# Requirements

## Keep tines-command-runner aligned with your Tines Stories version

The app build toolchain, Node and esbuild, ships inside the `tines-command-runner` image. The Tines Stories UI exposes the app builder as soon as Apps are enabled for your tenant, based on the entitlement alone. It does not check whether your `tines-command-runner` image can actually build. If you enable apps or upgrade `tines-app` without also re-pulling `tines-command-runner`, users will see the app builder and every build will fail.

Always upgrade `tines-command-runner` to the same version as `tines-app`. This is the same guidance that already applies to other command runner features, see [Upgrading Tines](https://www.tines.com/stories/docs/upgrade-tines/).

No configuration is needed to turn the toolchain on; it is present in all official Tines Stories images. If you build your own custom `tines-command-runner` images, you must pass the `ENABLE_APPS_BUILD=1` Docker build argument, which defaults to off.

Version alignment matters most in air-gapped deployments. An offline build can only resolve the package versions baked into the image it is running on, so upgrading `tines-app` without re-pulling `tines-command-runner` can leave builds unable to find a dependency version they expect.

## Connectivity

App builds add one outbound requirement to tines-command-runner, and only when they install from the public npm registry.

| **Source** | **Destination** | **Port** | **Notes** |
| --- | --- | --- | --- |
| Command runner | registry.npmjs.org | 443 | Needed to build apps from the public npm registry. Not needed if you install from a registry on your own network, or set NO\_NPM\_REGISTRY=true to build from the packages baked into the image. |

No other connectivity changes are needed. Builds travel over the existing web server and background worker connection to the command runner on port `4400`, and published apps are served from your existing tenant domain through your load balancer. For the full picture, see [Connectivity requirements](https://www.tines.com/stories/docs/network-and-connectivity/).

## Resources

Plan for:

- **Disk headroom on the command runner.** Each App keeps its own `node_modules` directory, plus a shared npm cache. Budget a few hundred megabytes per actively built app.
- **Ephemeral storage.** Build workspaces and the npm cache live on the container's local disk and are not persisted across restarts. Losing them is safe: the command runner re-seeds its npm cache from the packages baked into its image every time it starts, and the next build simply repeats the cold `npm install`.
- **Cold builds after a restart or scale-out.** If you run more than one tines-command-runner container, each keeps its own dependency cache, so the same app may build cold more than once. No session stickiness configuration is required for Apps.

# Setting up Apps on self-hosted

1. **Contact us.** Contact your Account Executive or Customer Success Manager, see [Before you begin](https://www.tines.com/stories/docs/before-you-begin/). Apps are gated by a tenant entitlement that cannot be self-enabled.
2. **Confirm your tines-command-runner image matches your Tines Stories version.** See "Keep tines-command-runner aligned with your Tines Stories version" above.
3. **Decide where app builds will get their npm packages.** The public npm registry needs no configuration. If your command runner has no route to it, see "Air-gapped and restricted-egress deployments" below.
4. **Turn apps on for your tenant.** As a tenant owner, go to `https://your-tenant-domain/settings/apps`. You can enable apps tenant-wide or for specific teams. See [Admin settings for Apps](https://www.tines.com/stories/docs/admin-settings-for-apps/).

# Configuring the npm registry

By default, app builds fetch dependencies from the public npm registry. To install from a registry on your own network instead, or to build with no registry at all, set these environment variables on the tines-command-runner container. These sit alongside the Python package index settings documented in [Run Script (tines-command-runner)](https://www.tines.com/stories/docs/run-script-tines-command-runner/).

- `NPM_CONFIG_REGISTRY` - URL of the npm registry used when building apps, for example an internal mirror or the bundled registry described below. When unset, the public npm registry is used. Example: `https://npm.your-company.com/`
- `NPM_TOKEN` - Auth token for a private npm registry. Has no effect unless `NPM_CONFIG_REGISTRY` is also set. The token is resolved from the environment at install time and is never written to disk.
- `NPM_CONFIG_STRICT_SSL` - Set to `false` to disable TLS certificate verification for the npm registry. Prefer adding your registry's CA to the container using `NODE_EXTRA_CA_CERTS`; use this only as a last resort when the registry is behind a self-signed certificate you cannot otherwise trust.
- `NO_NPM_REGISTRY` - Set to `true` to build apps with no registry access at all, resolving dependencies only from the packages baked into the image. Takes precedence over `NPM_CONFIG_REGISTRY`, which is ignored while it is set. Set it on the `tines-app` and `tines-sidekiq` containers too, so that the app builder knows which packages it can use.
- `NO_PROXY` - If tines-command-runner runs behind an egress proxy configured with `HTTP_PROXY` or `HTTPS_PROXY`, add `npm-registry` here when you install from the bundled registry. npm honors proxy settings, and the proxy cannot resolve a hostname on the internal Docker network.

You can also tune build behavior, though the defaults suit most deployments.

- `TCR_APPS_BUILD_TIMEOUT_SECONDS` - Wall-clock limit for a single app build, in seconds. Default: `120`
- `TCR_APPS_WORKSPACE_DIR` - Directory holding per-app build workspaces. Default: `/run/tcr/apps`
- `TCR_APPS_NPM_CACHE_DIR` - Shared npm cache directory, seeded from the packages baked into the image every time the container starts. Default: `/run/tcr/apps-npm-cache`

Persisting the workspace or cache directories on a volume is optional, and only saves the first build after a restart. If you do mount a volume over the npm cache, it must be writable by the container's app user, uid `2000`, because a newly created named volume is owned by root. An unwritable cache is logged as a warning at startup and the container keeps running, but with `NO_NPM_REGISTRY=true` app builds then fail, because there is nothing else to resolve from. A persisted cache is re-seeded automatically when a newer image changes the packages it ships.

# Air-gapped and restricted-egress deployments

Deployments with no route to the public npm registry have two options for app builds. They are alternatives rather than complements: `NO_NPM_REGISTRY` takes precedence, and `NPM_CONFIG_REGISTRY` is ignored while it is set.

> **NOTE:** Apps can be built in air-gapped deployments. Choose between offline builds from the packages baked into the tines-command-runner image, and an npm registry on your own network, including the one bundled with self-hosted Tines Stories.

| **Option** | **What it needs** | **Packages available to app builds** |
| --- | --- | --- |
| The public npm registry (default) | Outbound access from the command runner to registry.npmjs.org on port 443 | Everything on npm |
| A registry on your own network, including the one bundled with self-hosted Tines Stories | NPM\_CONFIG\_REGISTRY pointed at it | Everything that registry carries |
| No registry at all | NO\_NPM\_REGISTRY=true | The packages baked into the command runner image |

## Option 1: build offline from the packages in the image

Set `NO_NPM_REGISTRY=true` on `tines-command-runner`, and on the `tines-app` and `tines-sidekiq` containers.

Builds then install offline. Nothing is fetched, and dependencies resolve entirely from an npm cache baked into the `tines-command-runner` image, which the container copies into its cache directory every time it starts.

Setting the variable on all three containers is important. The command runner's copy decides how `npm install` runs. Setting it on `tines-app` and `tines-sidekiq` is what tells the app builder what packages it can use, so it can avoid trying to use dependencies which will never resolve.

### Which packages are preloaded

The image ships the dependencies every app needs, which are React, React Router, and `@tines/apps` . It also includes some commonly used packages:

- `@tanstack/react-table`
- `clsx`
- `date-fns`
- `fuse.js`
- `lodash`
- `nanoid`
- `papaparse`
- `recharts`
- `uuid`
- `zod`

Their own dependencies are included too. Because an offline install cannot resolve a version range, only the exact versions in the image can be installed. You can see the list at `/opt/apps-build/preloaded-packages.json` inside the container, and it is also shown in the Tines UI, see "Checking your configuration" below. 

### When an App needs a package that is not preloaded

The build fails with an error that names the package, lists what is available, and points at the alternatives, for example:

> Package chart.js@4.4.0 is not available in this air-gapped Tines deployment, which can only install packages preloaded into the tines-command-runner image. Available packages: ... Remove it from package.json, or ask your administrator to configure an internal npm registry (NPM_CONFIG_REGISTRY).

The app builder reads that error and corrects itself, so there is nothing to allowlist or maintain on the Tines Stories side.

## Option 2: install from an npm registry

Point `tines-command-runner` at a registry with `NPM_CONFIG_REGISTRY`, adding `NPM_TOKEN` if it requires authentication. Every package available in the registry will be available to Apps. This is a good option if you already run Artifactory, Nexus, or another mirror, or if you want to use the mirror bundled with Tines.

### The bundled npm registry

Self-hosted Tines Stories ships a [verdaccio](https://verdaccio.org) registry as the `npm-registry` service, the npm counterpart of the `pypi-server` service used by Run Script. It listens on port `4873` on the internal Docker network only, is never published to the host, and stores packages in the `npm-packages` volume. To install from it, uncomment this line on the tines-command-runner service in your compose or stack file:

```yaml
- NPM_CONFIG_REGISTRY=http://npm-registry:4873/
```

Installing and publishing are both anonymous, so no `NPM_TOKEN` is needed.

**It is seeded for you.** The registry is pre-seeded with the same set of default dependencies that are baked into the image, mentioned above. The registry is seeded when the container is setup, or upgraded. You can manually run, or re-run, the script which seeds the registry:

```bash
docker compose run --rm --no-deps tines-command-runner /usr/local/bin/publish_npm_seed http://npm-registry:4873/
```

**Publishing other packages.** Apps can only access the packages that are available on the registry. The build of an App will fail if it requests a package which does not exist on the registry. Additional packages can be added by following the steps below. 

Note that publishing a package means publishing its dependencies too: `npm pack` packs a single package, and npm resolves the rest from the registry at install time. 

```bash
mkdir npm-sideload && cd npm-sideload
npm init -y
npm install <package>@<version>
mkdir tarballs
npm ls --all --parseable | tail -n +2 | sort -u | xargs -n1 npm pack --pack-destination ./tarballs
```

Transfer the `tarballs` directory to your Tines Stories host, then publish each tarball using the tines-command-runner image, which carries npm and can reach the registry on the internal network:

```bash
docker compose run --rm --no-deps -v "$PWD/tarballs:/tarballs:ro" tines-command-runner sh -c 'export PATH=/opt/apps-build/bin:$PATH; echo "//npm-registry:4873/:_authToken=publish" > /tmp/npmrc; for tarball in /tarballs/*.tgz; do npm publish "$tarball" --registry http://npm-registry:4873/ --userconfig /tmp/npmrc --cache /tmp/npm-cache --provenance=false --tag latest || echo "not published: $tarball"; done'
```

npm refuses to publish without credentials for the target host, even though the bundled registry accepts anonymous publishes, which is what the placeholder token is for; its value is never checked. `--provenance=false` and `--tag latest` override settings that some public packages ship, which would otherwise fail the publish or leave the package without a `latest` version. A version already on the registry is refused rather than replaced, so re-running the command after adding a tarball is safe.

On Docker Swarm there is no compose file to run against. Replace `docker compose run --rm --no-deps` in the commands above with `docker run --rm --network tines_tines-net` against the tines-command-runner image, on a manager node, substituting your stack name if it is not `tines`.

Things to note about the bundled registry:

- The `npm-packages` volume grows with everything published to it, including the versions each upgrade adds. Like `python-packages`, it has no size limit.
- `verdaccio-config.yaml` ships in the release ZIP and is mounted into the service. Keep it alongside your compose or stack file: if it is missing when the service starts, Docker creates a directory in its place, and `upgrade.sh` stops with an error explaining how to restore it.
- The service runs whether or not you install from it. Setting `NPM_CONFIG_REGISTRY` is what opts a deployment in.

## Checking your configuration

You can see information on your configuration in the `tenant health` section of the Tines UI. For Apps, you can see whether `NO_NPM_REGISTRY` is set, the registry it installs from when that is not the public one, along with that registry's reachability and latency, and how many packages are baked into the image. Hover over the package count to see their names and versions.

# How published apps are served

On Tines Stories Cloud, published apps are served from a dedicated `*.tines.app` domain. On self-hosted they are served from your own tenant domain, at `https://your-tenant-domain/apps/<app>`.

Nothing extra is needed to support this. Apps reach your users over the same load balancer, TLS certificate, and hostname as the rest of Tines Stories, so no additional DNS records or certificates are required. Viewers must be signed in to your Tines Stories tenant, and access follows the app's visibility setting.

# Troubleshooting

| **Symptom** | **Likely cause** |
| --- | --- |
| The build fails with a message that the Node build toolchain is not installed | The tines-command-runner image predates app build support, or was custom-built without the toolchain. Re-pull it at the same version as tines-app. |
| The app builder is visible but every build fails | Apps were enabled before tines-command-runner was upgraded. Align the image version with tines-app. |
| Builds fail with npm network or 404 errors | The command runner cannot reach the npm registry. Check egress to registry.npmjs.org on port 443, or your NPM\_CONFIG\_REGISTRY setting. If you install from the bundled registry through an egress proxy, add NO\_PROXY=npm-registry. |
| Builds fail with a TLS or certificate error | Your registry uses a certificate the container does not trust. Add its CA to the container using NODE\_EXTRA\_CA\_CERTS. |
| A build fails saying a package is not available in this air-gapped deployment | The app asked for a package that is not baked into the image. Let the app builder choose a preloaded alternative, or install from a registry using NPM\_CONFIG\_REGISTRY. |
| A build fails saying a package is not published to the internal npm registry | Publish the package and its dependencies to the registry, or remove it from the app's package.json. If the message names React or the Tines app runtime, the registry was never seeded, so run publish\_npm\_seed. |
| The app builder keeps suggesting packages that cannot be installed | NO\_NPM\_REGISTRY is set on tines-command-runner but not on tines-app and tines-sidekiq, so the app builder does not know the deployment is air-gapped. |
| The command runner logs a warning that the npm cache seed failed | The npm cache directory is not writable by the container's app user, uid 2000. Fix the volume's ownership, or remove the volume and use the container's own disk. |
| Builds time out | Cold builds can take up to two minutes. If builds consistently time out, check command runner CPU and disk, and consider raising TCR\_APPS\_BUILD\_TIMEOUT\_SECONDS. |
