Apps let you install small web applications directly into your Cortex instance. An app is a self-contained bundle — installed from a zip by an administrator — that Cortex serves under /apps/{id}/, runs inside a locked-down sandbox, and grants scoped, proxied access to your Cortex API. Apps turn Cortex from a knowledge base into a small platform: a custom dashboard, a domain-specific search UI, an integration with another tool, or a purpose-built workflow can live right next to your documents and graph.
This page covers apps hosted inside your instance. The ecosystem also includes standalone apps — full applications with their own deployment and users that connect via the REST API with scoped keys; the flagship is Cortex Chat, the multi-tenant chat frontend.
Building an app for Cortex
This page is about installing apps into your instance. To build one, start from the cortex-app-template — it ships the manifest schema, a validator, and a working example bundle. For step-by-step guidance, point your AI coding assistant at the builder skills on cortexskills.org: SKILL.md files that teach any agent how to scaffold, configure, and package a Cortex app.
Overview
Apps are off by default. The whole subsystem is gated behind a single flag, ENABLE_APPS. When it is off, every app route returns 404 and the Apps section of the admin UI hides itself entirely — there are zero traces of the feature until you deliberately turn it on.
Once enabled, an administrator installs apps from Settings → Apps. Each installed app:
- is served as a static bundle under
/apps/{id}/, embedded in a sandboxed iframe; - gets its own dedicated, scoped API key, minted at install time and never exposed to the browser;
- can reach the Cortex API only through a proxy that enforces an endpoint allowlist declared in the app's manifest;
- appears in a launcher grid at
/appsfor logged-in users.
Apps come in classes. Static apps run entirely in the browser and talk only to Cortex. Platform apps add server-side capabilities (see Platform capabilities). A third class, service apps (apps that ship their own container), is defined by the manifest but is not hosted by Cortex — those run as separate services and are out of scope for this page.
How it works
- An administrator uploads an app package (a zip containing a manifest, an icon, and a static bundle) from Settings → Apps.
- Cortex validates the manifest, mints a dedicated scoped API key for the app, and stores the bundle on disk.
- If the app declares configuration (API tokens, base URLs, etc.), the admin fills it in through a setup wizard — the same wizard pattern used by Agent Skills, with secret values encrypted at rest.
- The admin enables the app. Enabled apps appear in the launcher grid at
/apps. - A user opens the app. Cortex serves it inside a sandboxed iframe and hands it a short-lived app token — never the real API key.
- When the app calls the Cortex API, the request goes through a proxy that checks it against the manifest's endpoint allowlist and attaches the minted key server-side.
Installing an app
- Go to Settings → Apps (visible only when
ENABLE_APPS=true). - Upload the app's
.zippackage. (Packages are size-capped byAPP_MAX_PACKAGE_MB, and the zip is checked for path traversal and zip-bomb expansion before anything is written to disk.) - Cortex validates the manifest. If anything is wrong, install fails with a clear list of every issue found — nothing is half-installed.
- During install, choose the app's collection scope with the collection picker: grant access to all collections, or restrict the app's minted key to specific collections. This, together with the manifest's key scope (
readorread_write), determines exactly what the app can touch. - On success the app is installed but disabled. Configure it if needed, then enable it.
Reinstalling (upgrading) an app preserves its minted key, saved configuration, and any share-link grants, so an upgrade doesn't break existing links or require reconfiguration. Deleting an app revokes its API key.
Installing from the registry
Instead of uploading a zip, admins can install straight from the public Cortex Registry — browse it on the web at registry.cortex.eco (catalog source: mocaOS/cortex-registry, a git-native, PR-moderated repo). Inside your instance, open Settings → Apps → Browse Registry to see the same catalog, with each app's declared key scope, endpoint allowlist, and platform capabilities visible before you commit to anything.
Installing from the registry is checksum-verified end to end: the catalog
pins every release artifact by sha256, the registry's CI re-downloads and
re-verifies artifacts on every listing change, and your instance verifies
the download against the pinned digest again before a single byte is
unpacked. A moved, re-uploaded, or tampered release asset fails closed.
Updates surface in the same panel (Update v0.3.0 → v0.3.1) and preserve the
app's key, configuration, share links, storage, and scheduled tasks.
The catalog location is APP_REGISTRY_URL (defaults to the official
registry; set it empty to hide registry browsing entirely, or point it at
your own fork for a curated internal catalog).
Publishing your own app is a PR — see the registry README for the release flow.
First-party apps
The registry currently lists eight first-party apps. All are platform-class (capabilities http + storage + tasks; YT Transcriber additionally llm), all request a read_write key — scope it to a dedicated collection per source at install. Each uses storage for delta cursors and per-item dedup, so re-runs transfer only new or changed content, and each can run on a schedule.
| App | What it does | You provide |
|---|---|---|
| Paperless Sync | Mirrors a paperless-ngx archive into Cortex — full original documents, on demand or scheduled | PAPERLESS_BASE_URL, PAPERLESS_TOKEN |
| Dropbox Sync | Syncs selected Dropbox folders; OAuth PKCE, no app secret anywhere | DROPBOX_APP_KEY (a scoped Dropbox app) |
| Google Drive Sync | Syncs Drive folders shared with a service account — no OAuth consent screens; short-lived tokens minted server-side | GDRIVE_SA_KEY (JSON key, stored encrypted), GDRIVE_SA_EMAIL |
| OneDrive Sync | Syncs OneDrive folders via Microsoft Graph delta; device-code sign-in, no client secret | ENTRA_CLIENT_ID (free Entra app registration, public client flows enabled) |
| SharePoint Sync | Syncs SharePoint document libraries with app-only Sites.Selected access — per-site grants, no user sign-in | ENTRA_TENANT_ID, ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET |
| Nextcloud Sync | Syncs Nextcloud folders via WebDAV | NEXTCLOUD_BASE_URL, NEXTCLOUD_USER, NEXTCLOUD_APP_PASSWORD |
| WebDAV Sync | Syncs folders from any WebDAV server — Synology, QNAP, Koofr, pCloud, GMX/WEB.DE, MagentaCLOUD, HiDrive, Seafile, Hetzner Storage Box, … | WEBDAV_URL, WEBDAV_USERNAME, WEBDAV_PASSWORD |
| YT Transcriber | Turns YouTube videos and whole channels into clean transcripts in the knowledge graph — transcription via Venice, refinement via the instance's own LLM | VENICE_API_KEY |
Every listing shows the app's endpoint allowlist, external hosts, and config schema in the Browse Registry panel before install; secrets are encrypted at rest and only ever used server-side.
Configuring an app
If an app declares configuration variables in its manifest, it shows up needing setup. Open its configuration to fill in each value through a wizard-style form:
- Text values (base URLs, identifiers) are stored in plain form.
- Secret values (API tokens, keys) are masked in the UI and encrypted at rest (Fernet, when
ENCRYPTION_KEYis set) — exactly like skill config. Submitting the mask placeholder preserves the existing secret.
Non-secret config values can be read back by the app at runtime; secret values never cross that boundary — they are only ever used server-side.
Launching an app
Enabled apps appear in the launcher grid at /apps. Opening one loads it in a sandboxed host at /apps/launch/{id}, which performs the token handshake: the app requests a token, the host hands it a short-lived one, and the app renews it as needed. All of this happens inside the sandbox — the surrounding Cortex UI and cookies are never exposed to the app's code.
Share links
Some apps are meant to be used by people who are not Cortex users. For apps that allow it, an administrator can mint revocable share links from the app's management screen. A share link looks like /a/{id}?g=... and opens the app in a minimal, cookie-less shell — no Cortex login required.
A share visitor can use the app, and nothing else:
- the link exchanges its grant for a short-lived app token that validates only at the app's proxy;
- the visitor can never reach the rest of Cortex — not the admin UI, not the main API, not other apps;
- revoking the share link immediately kills any tokens already issued from it;
- links can be given a role (viewer or editor) and an optional expiry.
This makes apps a safe way to expose a single, bounded slice of your knowledge base to the outside world.
Platform capabilities
A platform app (type: "platform") can declare server-side capabilities beyond browser-plus-Cortex. The headline capability — and the recommended way to integrate other software — is http.
http — the no-CORS integration pattern
An app with the http capability can make calls to external APIs that execute server-side, inside Cortex, with credentials injected from the app's configuration. This is the same authentication model as skills: the app's config carries auth_header templates, and Cortex fills them in with the stored (secret) values when it makes the outbound call.
Two properties make this the preferred integration pattern:
- No CORS setup on the target. Because the call originates from Cortex's server, not the browser, the external service needs no CORS configuration — a common blocker for browser-only integrations simply disappears.
- Credentials never reach the browser. API tokens for the external service live in encrypted app config and are attached server-side. The app's client code never sees them.
Outbound targets are restricted to the hosts declared in the app's manifest (literal hostnames, or references resolved from the app's config), and requests are additionally guarded against SSRF: loopback and cloud-metadata addresses are blocked, and redirects are not followed. Responses are size-capped, and the call honours APP_HTTP_TIMEOUT.
Apps that talk to more than one external host should scope each credential with auth_host on the config variable — the header is then injected only on calls to that host, so one service's key is never sent to another (the app template's validator warns when a multi-host app leaves a credential unscoped).
config read
Every app can read back its own non-secret configuration values at runtime. Secret-typed values are excluded from this — they exist only for server-side auth injection.
storage — per-app key/value store
An app with the storage capability gets its own persistent key/value store, backed by a per-app SQLite file inside the apps volume. Values are JSON documents; keys are path-like identifiers (transcripts/abc, sync/cursor). The store supports get/put/delete, prefix listing, and is what makes apps stateful across sessions and devices — no more localStorage-only state. Quotas apply per value (APP_STORAGE_MAX_VALUE_KB) and per app (APP_STORAGE_MAX_MB). Storage is strictly namespaced: no app can ever read another app's data, and uninstalling an app deletes its store.
tasks — background work that survives a closed tab
Apps ship no server code, so background work is expressed as a declarative step-queue: the app submits a JSON program of steps — external http calls, allowlisted cortex API calls, llm completions, store reads/writes, string template rendering, and conditional item skips — and Cortex executes it server-side in its existing async loop. Close the tab; the work keeps going.
What the engine provides:
- Setup → items → finally structure. Setup steps run once (e.g. list new documents from an external system, paginated). The item list can fan out from a setup result, with optional built-in deduplication against the app's storage (
skipIfStored). Per-item steps run through a bounded worker pool with per-item error isolation — one bad item never aborts the run. Finally steps run once at the end (the place to persist a sync cursor). - Schedules. A task with
"schedule": {"everyMinutes": N}re-runs on its own — no browser, no cron container. This is what turns an integration app into a true sync daemon: e.g. a paperless-ngx app that pulls newly added documents into Cortex every hour. - Control. Tasks can be paused, resumed, cancelled, retried (failed items only), or triggered early; state and per-item statuses are persisted, and runs interrupted by a server restart resume on boot.
- LLM policies. The
llmstep supports automatic chunking of long inputs and built-in output validation (length-ratio + word-overlap guards with retry-once-then-keep-original), so transcript-cleanup-style workloads are safe against silent LLM truncation without any app-side code.
Every step passes the same security gates as the interactive paths: http steps are host-allowlisted and SSRF-guarded with server-side secret injection, cortex steps are checked against the manifest's endpoint allowlist and use the app's minted key, and llm steps run through the instance's configured model — metered against the instance quota like any other completion.
llm — instance-model completions
The llm capability lets task steps call the instance's configured extraction-tier model (falling back to the main model when no dedicated extraction model is set) — task steps are bulk text work, where the extraction workhorse beats a slow reasoning chat model. Calls are counted toward the instance's unit metering (MAX_QUERIES_PER_MONTH), traced by Langfuse when observability is enabled, and capped per task run (APP_TASK_LLM_CALLS_PER_RUN).
App classes
| Class | Where it runs | What it can reach |
|---|---|---|
| static | Browser (sandboxed iframe), served by Cortex | The Cortex API, through the proxy allowlist |
| platform | Browser + server-side capabilities in Cortex | Cortex API, plus http (allowlisted external hosts), storage, tasks (scheduled step-queues), llm, and config read |
| service | Its own container (not hosted by Cortex) | Out of scope for in-instance hosting |
Security model
Apps are built to be safe to install and, for share links, safe to expose to strangers.
- No real API key in the browser. Install mints a dedicated, scoped key (
readorread_write, optionally collection-restricted). Its plaintext stays server-side (encrypted whenENCRYPTION_KEYis set) because the proxy must present it upstream. The browser only ever holds a short-lived app token. - Short-lived app tokens. Tokens expire after
APP_TOKEN_TTL_SECONDS(default 15 minutes) and are renewed via the sandbox handshake. They validate only at the app proxy, never on the main Cortex API. - Sandboxed iframe. Apps are embedded with a restrictive sandbox and never
allow-same-origin, so an app runs in an opaque origin with no access to Cortex cookies or the surrounding page. - Proxy allowlist. Cortex API calls are forwarded only if their path matches the manifest's declared endpoints; everything else is rejected. Streaming (SSE) responses — needed by chat-style apps — pass through unbuffered.
- Per-app Content-Security-Policy. Every served file carries a CSP that limits network access to Cortex and the app's declared external hosts.
Configuration
The whole subsystem is controlled by these environment variables. Only ENABLE_APPS is required to turn it on; the rest have sensible defaults.
| Variable | Default | Description |
|---|---|---|
ENABLE_APPS | false | Master switch. Off = every app route returns 404 and the admin UI section hides itself. |
APPS_DIR | .agents/apps | Directory holding installed app bundles. Persist via a Docker volume (apps_data) in production. |
APP_MAX_PACKAGE_MB | 50 | Maximum uploaded package (zip) size. Uncompressed contents are capped at 4× this (zip-bomb guard). |
APP_TOKEN_TTL_SECONDS | 900 | Lifetime of the short-lived app tokens handed to sandboxed apps; apps renew via the handshake. |
APP_PROXY_UPSTREAM | http://127.0.0.1:8000 | Where the app proxy forwards allowlisted /apps/{id}/api/cortex/* calls (self-loopback, so auth/metering/rate-limit paths match external API traffic). |
APP_HTTP_TIMEOUT | 30 | Timeout in seconds for the platform http capability (server-side external calls on behalf of platform apps). |
APP_STORAGE_MAX_MB | 50 | Per-app quota for the storage capability. |
APP_STORAGE_MAX_VALUE_KB | 1024 | Maximum size of a single stored value. |
APP_TASK_MAX_ITEMS | 2000 | Maximum items a single platform task may hold. |
APP_TASK_MAX_CONCURRENCY | 4 | Upper bound on a task's per-run item worker pool. |
APP_TASKS_GLOBAL_CONCURRENCY | 8 | Global cap on concurrently executing task items across all apps. |
APP_TASK_MIN_SCHEDULE_MINUTES | 15 | Floor for a scheduled task's everyMinutes interval. |
APP_TASK_LLM_CALLS_PER_RUN | 500 | Cap on llm-step completions in one task run (chunked steps count each chunk). |
APP_TASK_STEP_OUTPUT_MAX_KB | 2048 | A step whose output exceeds this fails — large artifacts belong in storage. |
APP_TASK_MAX_PER_APP | 50 | Maximum stored task records per app (finished one-shots age out first). |
APP_REGISTRY_URL | official registry | The catalog behind Browse Registry (an index.json). Point at a fork for a curated catalog; empty hides the panel. |
Docker volume persistence
Installed apps live on the filesystem under APPS_DIR. In Docker deployments, mount it as a named volume so apps survive container restarts:
Code