Apps
Deploy static sites, internal tools and backends (data, functions, realtime) from your project to stable URLs, with releases, machines, volumes, access control and Kortix sign-in built in.
An App is a deployment that one project owns. It has one stable URL, an access policy and numbered releases. Use Apps to host a site, an internal tool or a backend for the people in your project: Kortix signs them in before your code runs.
What an App is
An App is either static or a server. A static App is files that Kortix serves itself, with no machine. A server App runs machines that Kortix builds from your source or image.
| Part | What it is |
|---|---|
| Release | vN: an artifact (files, a build or an OCI image) plus its resolved config. A release never changes. A failed release never replaces live traffic. A rollback is a new release. |
| Process group | A named command. A server App without processes has one group, app. |
| Machine | Runs one process group of the live release. Its machine_id stays the same across releases. Each release replaces its root disk. |
| Service | A named HTTP port of a process group. Each service has its own host and its own access. |
| Volume | A persistent disk that one machine mounts. It outlives every release and every machine. |
| Label | A free-form tag, for example backend. No behavior depends on labels. |
A database, a queue or any other backend is a server App with a volume. A template writes a ready-made App for you: see Template: Convex backend.
Every App lists what it supports in capabilities, for example machines or
volumes. A call that needs a missing capability answers
409 app_capability_unsupported.
Turn Apps on
Apps are off until a platform admin turns them on for a project. A project
member cannot turn them on: the write answers 403 feature_operator_only.
- Kortix Cloud: email support@kortix.com with the project id.
- Self-hosted: a platform admin (an email in
KORTIX_PLATFORM_ADMIN_EMAILS) who is a project member runskortix projects features enable apps. For any other project, the admin sendsPUT /v1/admin/api/projects/<project_id>/featureswith{"feature": "apps", "enabled": true}and their sign-in JWT. The instance also needsKORTIX_APPS_BASE_DOMAINand a wildcard DNS record: see Self-hosting.
While Apps is off:
- Every Apps route answers
403withcode: "feature_disabled",feature: "apps"and the messageApps is not enabled for this project. Contact Kortix to enable it. kortix apps <subcommand>exits1before it calls the API.- The project sidebar has no Apps entry.
/projects/<id>/appsshows a gate screen.
Check the state with kortix projects features, or in Customize → Settings →
Feature flags. Both show apps only while it is on, as a locked row: origin
kortix in the CLI, Managed by Kortix in the web app.
Deploy your first App
Build the site
npm run buildDeploy the output directory
kortix apps deploy dist --slug my-site --type static --spaThe command creates the App, uploads the files, and waits until the URL is
ready. It prints the URL. Without --slug, the slug is the directory name.
Open it
A new App is private: only you can open it. Open the URL in a browser
where you are signed in to Kortix, or print a 5-minute sign-in link:
kortix apps access-link my-siteShip the next release
npm run build && kortix apps deploy dist --app my-siteUse --app (or a kortix.yaml block) for every later release. Without
it, the CLI creates a new App, and a slug that exists answers 409.
The web app lists every App you may open at Apps in the project sidebar
(/projects/<id>/apps). The page shows live state, a preview and the access
controls. An App’s detail lists its releases. A server App adds a Machines
tab, and an App with a volume adds a Volumes tab. ?app=<app_id> opens one
App’s detail. You create and deploy Apps
from the CLI, the SDK or an agent session.
Sources
| Source | You deploy | Kortix |
|---|---|---|
static |
Files: HTML, a built SPA, a dist/ |
Serves the files. No machine, no cold start. |
bundle |
A package source | Runs the install and build commands, then serves the output directory from a machine |
dockerfile |
A directory with a Dockerfile | Builds the image, then runs --command on --port |
oci_image |
A public image reference (--image) |
Runs --command on --port |
- Pick
staticfor every UI you can build ahead of time. Build locally and deploy the output. A Next.js site withoutput: 'export'deploysout/asstatic. - Pick
dockerfilefor server rendering, native packages or any service. dockerfileandoci_imageneed--command(a JSON argv array is unambiguous) and--port, orhttp_service.internal_portinkortix.yaml. A worker-only App (processesand no service) needs no port.
For a directory, the CLI picks the source when you omit --type: a
Dockerfile makes it dockerfile, else a package.json makes it bundle,
else static. The defaults:
| Setting | static |
bundle |
|---|---|---|
| Install command | — | corepack enable && pnpm install --frozen-lockfile |
| Build command | — | pnpm build |
| Output directory | — | dist |
SPA fallback (--spa) |
off | on |
| Readiness path | / |
/ |
What the CLI uploads:
- It honors
.gitignore,.dockerignoreand.kortixignore. - It never uploads
.git/,.kortix/or.env*. It leaves outnode_modulesunless you pass--include-node-modules. - A build-time value, such as
VITE_*in.env.production, reaches a static App only through a build you run before the deploy.
Limits:
| Limit | Value |
|---|---|
| Archive, compressed | 50 MiB |
| Archive, extracted | 1 GiB |
| Files in an archive | 100,000 |
| Files in a static App | 20,000, at most 50 MiB each. A larger site fails with invalid_site. |
A static App runs no code, so it ignores env and secrets. A deploy that sets
them records an environment_ignored event.
A deployment moves through queued, validating, building, provisioning,
checking and ready, or ends failed or cancelled. kortix apps deploy
waits up to --wait-seconds (default 1200). --no-wait returns once the
deployment is queued.
Configure in kortix.yaml
An apps.<slug> block in a v2 kortix.yaml holds an App’s deploy settings and
release config. Commit it, and every deploy uses the same settings.
apps:
docs:
path: docs/dist
type: static
spa: true
api:
path: services/api
type: dockerfile
command: ["node", "server.js"]
labels: [backend]
display: { preview: none } # an icon card, not a live thumbnail
always_on: true
uses: [docs]
processes:
app: "" # "" runs the block's command
worker: "node worker.js" # no service names it: a worker
http_service:
internal_port: 3000
access: app # public | app | admins
processes: [app]
checks: [{ type: http, path: /health }]
services:
- { name: admin, internal_port: 3001, access: admins }
mounts:
- { source: data, destination: /data, initial_size_gb: 10, processes: [app] }
vm:
- { cpu: 1, memory_gb: 2 }
- { cpu: 1, memory_gb: 1, processes: [worker] }
deploy:
release_command: "node migrate.js"
env:
PUBLIC_BASE_URL: https://example.com
secrets:
API_TOKEN: api_token # runtime name: project secret identifier
| Field | Meaning |
|---|---|
path, type, image, dockerfile, command, root, output_dir, install_command, build_command, spa |
The source. type is static, bundle, dockerfile or oci_image. command is the image’s argv. Paths are relative to kortix.yaml. |
port, readiness_path |
Shorthand for http_service.internal_port and one HTTP check |
always_on, idle_timeout_seconds, resources |
Run mode, and the App’s default machine size (cpu, memory_gb, disk_gb) |
env, secrets |
Runtime values, and runtime names mapped to project secret identifiers. The deploy reads both from this block only. |
uses |
The Apps this App uses. The list replaces the App’s list on every deploy. An App cannot use itself. |
labels |
At most 10, each lowercase letters, digits and - (^[a-z0-9][a-z0-9-]{0,30}$) |
display |
Where Kortix shows the App. Presentation only. See Where an App shows. |
processes, http_service, services, mounts, vm, deploy |
The release config. See the next sections. |
Rules:
kortix apps deploywith no arguments deploys every block: each App after the Apps it uses, then by name.kortix apps deploy <path>uses the block whosepathis that path.--manifest-app <slug>names a block. A flag you pass wins over the block.kortix validateapplies the same rules as the API. The API refuses a bad config with400 app_config_invalidandissues: [{ path, message }].- Any other key is an error.
backendsis an error: list used Apps inuses.kindis ignored, with a warning. - A static App ignores the run mode, the machine size,
env,secretsand the release config.
Where an App shows
display decides where an App appears in the Apps tab. It never changes how
the App runs or who may open it.
| Key | Default | Effect |
|---|---|---|
listed |
true |
false takes the App out of the grid. |
preview |
live |
live frames the App on its card. none shows an icon. Use none for an App whose root page is not for people, such as an API or a backend. |
parent |
none | parent: <slug> makes the App a tab in that App’s detail, with no card of its own. The tab label is the App’s name without the <parent>- prefix: backend-dashboard becomes Dashboard. A tab whose parent does not exist stays a card. |
An App with preview: none opens on its first tab. Every deploy writes the
block’s display; an absent display means the defaults. kortix apps show
prints it on the shown row. PATCH /v1/projects/<project_id>/apps/<app_id>
with display replaces it whole, and {} restores the defaults. An unknown
key, a preview other than live or none, or a malformed parent answers
400.
The full kortix.yaml reference is kortix.yaml reference.
Processes, services and checks
These settings apply to server Apps.
- Process groups.
processesmaps a group name to a shell command.""runs the App’scommand. A group name is lowercase letters, digits,-or_, and starts with a letter. - Primary service.
http_serviceanswers on the App’s own host. Its fields:internal_port,access,auto_stop_machines(off,stop,suspend;suspendacts likestop),auto_start_machines,min_machines_running(0–100),processes,checks. - More services.
serviceslists at most 10 more HTTP services, each{ name, internal_port, access, processes, checks }. A service answers on<app host>--<name>.httpis the primary service’s name and is reserved. - Workers. A group that no service names is a worker. It runs and never idle-stops.
- Ports.
internal_portis 1–65535. Ports7331and8080belong to the App supervisor and are refused. Two services cannot share a port. - HTTP only. A service with
protocol: tcpis refused. Apps have no TCP services and no private network between Apps.
Service access gates the service’s host:
access |
Who gets through |
|---|---|
public |
Anyone. The service checks sign-in itself. |
app (default) |
Whoever the App’s access policy admits |
admins |
Whoever the App’s policy admits and who holds project.app.admin. Others get 403 app_service_admins_only. |
Checks run inside the machine. A release goes live only when every check of every new machine passes.
| Check field | Default | Bounds |
|---|---|---|
type |
http when a path is set, else tcp |
http, tcp |
path |
/ |
Starts with / |
interval_s |
15 | 1–3600 |
timeout_s |
5 | 1–600 |
grace_period_s |
10 | 0–3600 |
Deploy strategy (deploy.strategy):
| Strategy | What happens | Default when |
|---|---|---|
bluegreen |
New machines start and pass their checks, then traffic moves. No downtime. | The release has no mount |
rolling |
One machine at a time. A volume moves from the old machine to the new one. Seconds of downtime. | The release has a mount |
immediate |
Every machine is replaced at once, without waiting for checks | — |
canary |
One new machine first. When it is healthy, the rest roll. | — |
A release with a mount refuses bluegreen and canary: they would put two
machines on one volume. A release that never gets healthy is abandoned, and
traffic stays on the previous release.
deploy.release_command (at most 2000 characters) runs once per release,
before any machine changes. It runs in a temporary machine with the new image,
env and secrets, and no volume. A non-zero exit fails the release with
release_command_failed, and nothing changes. Use it for a migration of a
database outside the App.
Run mode, machines and scale
A server App runs in one of two modes:
| Mode | Behavior | Use it for |
|---|---|---|
Always on (--always-on) |
Runs 24/7. Kortix checks every 5 minutes and restarts it when it stops. | WebSockets, background loops, databases |
On demand (--on-demand) |
Stops after idle_timeout_seconds without requests. The next request wakes it. |
Apps that only answer requests |
A new App is always on. Change the mode with kortix apps set <app> --always-on
or --on-demand, or with always_on in kortix.yaml. The change applies
within 5 minutes, without a redeploy. A static App has no runtime and ignores
the run mode.
Machine size:
| Setting | Default | Bounds |
|---|---|---|
cpu |
1 | 1–32 |
memory_gb |
2 | 1–128 |
disk_gb |
10 | 1–500 |
idle_timeout_seconds |
300 | 120–86400. On demand only. |
A value out of range answers 400 app_machine_out_of_range. Kortix never
clamps it. The App’s size is the default for every process group. On Platinum,
a vm entry in the release sizes one group. On Daytona and E2B, every machine
uses the App’s size. A size change applies with the next release.
kortix apps machines api # id, group, state, release, size, volume
kortix apps machines restart api <machine> # also start, stop, destroy
kortix apps scale count api 3 --process worker # 0-20 machines per group
kortix apps scale show api
kortix apps exec api -- node scripts/report.js # run one command in a machine
- A group runs 1 machine by default.
scale countsets 0–20 per group and applies in the background. A new machine of a group with a mount gets its own new, empty volume. - Destroying a machine keeps its volume. The group runs one machine fewer.
kortix apps stopsuspends a server App now. The next authorized request wakes it.kortix apps startwakes it before traffic arrives. A static App answers409 static_app_no_runtimeto both.execneedsproject.app.adminand is audited. It takes an argv of 1–256 words, runs in a running machine of the live release (or--machine <id>), and times out after--timeoutseconds (1–300, default 60). Each of stdout and stderr returns at most 1 MiB. The CLI exits with the command’s exit code. A stopped machine answers409 machine_not_running.
Account limits:
| Code | HTTP | Meaning |
|---|---|---|
app_quota_exceeded |
402 | The account is at its App limit. |
app_concurrency_limit |
429 | The account runs its maximum number of Apps at once. |
app_account_unfunded |
402 | The account cannot start compute. Its running Apps stop. |
A self-hosted instance without billing has none of these limits.
Volumes and snapshots
Volumes need the Platinum sandbox provider. Deploy with --provider platinum
when Platinum is not your instance’s default provider. A kortix.yaml block
has no provider key. A release with mounts on another provider answers
400 app_volumes_unsupported.
mounts:
- { source: data, destination: /data, initial_size_gb: 10, snapshot_retention_days: 5 }
sourceis the volume name: 1–30 lowercase letters, digits or_.destinationis an absolute path. It cannot be/, end in/, or sit under/bin,/boot,/dev,/etc,/lib,/lib64,/proc,/root,/run,/sbin,/sys,/usr,/varor/kortix. Use/data.- Size is 1–100 GB, default 1. A volume grows and never shrinks
(
400 volume_shrink_refused). - One machine writes a volume at a time, and a machine mounts at most one volume. Kortix enforces a single-writer lock.
- A volume outlives every release. A rollback changes code and config, never the data.
kortix apps volumes backend # name, group, size, attached machine, retention
kortix apps volumes create backend data --size 10 --process app
kortix apps volumes extend backend data --size 20
kortix apps volumes snapshots backend data # daily, manual and final snapshots
kortix apps volumes snapshot backend data # a manual snapshot now
kortix apps volumes restore backend data <snapshot> # project.app.admin
kortix apps volumes fork backend data data_copy # --snapshot <id> to copy a snapshot
kortix apps volumes destroy backend data --yes # project.app.admin
| Operation | What it does |
|---|---|
| Daily snapshot | Kortix takes one per volume per day. daily and manual snapshots are kept snapshot_retention_days (1–60, default 5). |
snapshot |
A manual snapshot. On a mounted volume, the last ~1 s of writes can be missing. |
restore |
The machine stops, the volume returns to the snapshot, and the machine starts on it. The replaced contents stay in the volume’s history. |
fork |
A new, unattached volume in the same App, made at once from the current contents or a snapshot |
create |
A volume made ahead of time. The next machine of its group with a mount of that name takes it. |
destroy |
Needs the volume’s name as confirmation (400 confirmation_required without it). Refused with 409 volume_attached while a machine mounts it. Kortix keeps a final snapshot for 7 days, then purges the data. |
Access
An access mode decides who can open one App. It grants no permission that the person’s role denies.
| Mode | Who can open the App |
|---|---|
private (default) |
The creator |
project |
Everyone who can read the project |
restricted |
The members and groups you list (at most 100 of each, at least one) |
public |
Anyone. No sign-in. |
password |
Anyone with the password (8–256 characters) |
Project managers, account owners and account admins see and open every App of the project, so a private App stays manageable after its creator leaves.
kortix apps deploy dist --app my-site --access project
kortix apps access my-site --mode restricted --members <member_id> --groups <group_id>
kortix apps access my-site --mode password --password '<password>'
kortix apps access-link my-site
- A person who can open the App signs in with their Kortix login. An access
link (
kortix apps access-link) works for 5 minutes and sets an 8-hour, host-only, secure cookie. Treat the link as a secret. - A change to the policy revokes every App cookie and every viewer token the App holds. Everyone signs in again.
- Kortix stores only a password hash. The API, the CLI and the SDK never return
it: the policy reports
password_configured. - The App list shows only the Apps you may open. An App you may not open
answers
404, never403. - In the web app,
?open_app=<app_id>on the Apps page opens the App through an access link.
Know who is looking
The gate signs in every visitor before a request reaches your App. Your App reads who they are without a login of its own. The viewer scope decides how much it is told:
--viewer |
The App receives |
|---|---|
identity (default) |
The viewer’s user id, email, name, picture, account role and groups, in a signed header on every request |
api |
The same, plus a 1-hour token that acts as the viewer on the Kortix API. The viewer’s role caps it. |
off |
Nothing |
kortix apps access my-site --viewer api
A viewer exists when a person opens the App with their Kortix login or an access
link. A password App never has a viewer. A public App has one only for a
visitor who arrived through an access link.
In the browser. GET /_kortix/viewer on the App’s origin returns the
viewer. Call the Kortix API through /_kortix/api/v1/* on the same origin: a
direct call to the API from an App origin fails CORS.
import { createKortix, kortixAppViewerToken } from '@kortix/sdk';
import { useKortixAppViewer } from '@kortix/sdk/react';
const kortix = createKortix({ backendUrl: '/_kortix/api/v1', getToken: kortixAppViewerToken() });
function Header() {
const { status, viewer } = useKortixAppViewer(); // 'loading' | 'anonymous' | 'viewer'
return <span>{status === 'viewer' ? viewer.email : 'Signed out'}</span>;
}
/_kortix/api/v1/* answers 403 viewer_api_disabled unless the scope is api,
401 no_viewer_identity without a viewer, and 403 cross_site_request for a
request from another origin. /v1/oauth/* and MCP paths answer 404.
On the App’s server. Import from @kortix/sdk/server:
import { readAppViewer, createAppViewerKortix } from '@kortix/sdk/server';
const viewer = await readAppViewer(request);
// { userId, email, name, picture, groupIds, groups, role, projectId, accountId, appId, accessMode, token, expiresAt } or null
if (!viewer) return new Response('Not found', { status: 404 });
const kortix = await createAppViewerKortix(request, { backendUrl: 'https://api.kortix.com/v1' }); // scope api only
readAppViewerverifies the signed header withKORTIX_APP_VIEWER_SECRET, which Kortix injects. A forged header fails.createAppViewerKortixbuilds a client that acts as the viewer, from the token the gate adds to each request. Build one per request. Never store or log the token.createKortixAppGuard()returns one resolver for both the gate header and your own Kortix sign-in.requireViewer(request)andrequireGroup(request, groupIds)return{ viewer }or{ response }, never null. A group check keeps working when the App’s access mode changes.- A viewer with the
memberrole needs an agent grant to start sessions on a project with agent permissions. Without it, the API answers403 no_agent_access.
An App on its own domain has no gate. Use Sign in with Kortix there.
Apps that use Apps
An App can use other Apps of its project. Then its code gets sign-in tokens for them and reaches them on its own origin.
kortix apps link crm --uses backend # add; kortix apps unlink removes
kortix apps set crm --uses backend,files # replace; --uses= clears
uses: [backend] in the kortix.yaml block sets the same list on every deploy. An
unknown slug, or one you cannot see, answers 400 app_not_found with slugs.
Sign-in tokens. Each project has one token issuer,
https://<api host>/v1/projects/<project_id>, with a public
/.well-known/openid-configuration and /jwks.json. A token is an ES256 JWT
that lives 15 minutes. Its aud is the id of the App it is for. It carries
sub, email, name, picture, groups, group_ids, role, account_id
and project_id. An agent’s token carries kind: "agent" and no groups or
role.
| Who | How they get a token |
|---|---|
| A person in an App | GET /_kortix/token?audience=<slug or id> on the App’s origin: the App itself (default) or an App it uses. kortixToken({ audience }) in @kortix/sdk. |
| A member, a script, an agent session | kortix apps token <app> (--json adds expires_at), or POST /v1/projects/<project_id>/apps/<app_id>/token. An agent session gets a token that names its agent. |
Any other audience answers 403 app_not_linked. A token proves who the person
is, not what they may do: check groups or role in every function whose data
is not for everyone the App admits. A person you remove loses access within one
token lifetime.
Bindings. /_kortix/apps/<slug>/* on an App’s origin forwards HTTP and
WebSocket requests to the http service of an App it uses.
/_kortix/apps/<slug>--<service>/* reaches another service. Your browser code
needs one origin and no CORS. The used App checks the token itself.
import { kortixBinding } from '@kortix/sdk';
const backend = kortixBinding('backend'); // { url: <origin>/_kortix/apps/backend, token: a token fetcher }
| The used App or service | Answer |
|---|---|
Not in uses |
403 app_not_linked |
A service with access: admins |
403 app_binding_forbidden |
| A static App | 409 app_binding_unsupported |
| No running release | 503 app_binding_unavailable |
| No such service | 404 app_service_not_found |
Server to server. A machine has no viewer cookie, so it skips the binding.
It calls KORTIX_USES_URL_<SLUG> with a token. A link change reaches that
variable with the next deploy, because a deploy makes new machines.
Verify a token. verifyKortixToken(bearer) from @kortix/sdk checks the
signature, expiry (60 s skew), issuer and audience. In a server App it needs no
options: it reads the injected KORTIX_AUTH_* values. Elsewhere, pass the App’s
auth.issuer, auth.audience and auth.jwks_uri. To verify offline, for
example in a self-hosted Convex deployment, kortix apps auth-env <app> prints
KORTIX_AUTH_ISSUER, KORTIX_AUTH_AUDIENCE and KORTIX_AUTH_JWKS (the key set
as a data: URL) as dotenv lines. These values are public.
readKortixMember(claims) turns a token’s claims, or a viewer, into one member
object, or null. requireKortixMember(claims, { groups, roles }) returns the
member or throws KortixMemberError with code unauthenticated or forbidden.
Each list means “any one of these”. A group matches by name or id. An empty list
admits nobody. Groups need the Enterprise plan; roles work on every plan.
Agents and Apps
An agent session calls an App with its session credential, in
Authorization: Bearer or X-Kortix-App-Authorization: Bearer. Use the second
header when the App reads Authorization for its own key. Before the gate
forwards the request, it removes X-Kortix-App-Authorization, an
Authorization header that holds a Kortix credential, and the Kortix cookies.
Every other header and cookie reaches the App unchanged. App code never
receives a Kortix credential.
The gate judges an agent session as the agent, not as the person who started it:
| App mode | The agent session gets in when |
|---|---|
public |
Always |
project |
The agent holds project.app.read |
restricted, private |
The App’s slug is in the agent’s apps list, and the agent holds project.app.read |
password |
Never |
In a project whose kortix.yaml declares no agents, the gate judges the
session as the person who started it. A refused request answers
401 app_auth_required. An agent session opens only Apps of its own project.
agents:
report-writer:
kortix_permissions: [project.app.read]
apps: [reports] # App slugs, all, or none (the default)
Set the list in kortix.yaml, with kortix agents scope report-writer --apps reports
(also all and none; needs a v2 kortix.yaml), or in Customize → Agents →
the agent → Apps. The App’s access dialog lists the agents granted to it.
From code, call apps.access.agents(appId). See
Agents & permissions.
A script, a CI job or a service account calls a non-public App with a personal access
key (kortix_pat_) or a service account key (kortix_sa_), in Authorization: Bearer or
X-Kortix-App-Authorization: Bearer. The gate applies the same access decision
as for a signed-in browser, within the token’s project binding. A password App
never accepts a Kortix credential.
An openapi or http connector whose base URL is an App of the same project
needs no Kortix credential. The connector gateway adds a signed, 60-second
assertion for the calling session. See Connectors.
Environment inside an App
Kortix sets these variables in every machine:
| Variable | Value |
|---|---|
KORTIX_APP_SLUG |
The App’s slug |
KORTIX_APP_URL |
The URL of the primary service |
KORTIX_SERVICE_URL_<NAME> |
The URL of each other HTTP service (- becomes _) |
KORTIX_USES_URL_<SLUG> |
The URL of each App this App uses (- becomes _) |
KORTIX_MACHINE_ID |
The machine’s id, stable across releases |
KORTIX_PROCESS_GROUP |
The machine’s process group |
KORTIX_AUTH_ISSUER, KORTIX_AUTH_AUDIENCE, KORTIX_AUTH_JWKS |
The values that verify the App’s sign-in tokens |
KORTIX_APP_VIEWER_SECRET |
The key that verifies the signed viewer header |
KORTIX_APP_ACCOUNT_ID |
The account that owns the App |
Your own values come from env and secrets in the kortix.yaml block:
- At most 128
envvalues and 128secretsmappings. A value is at most 32 KiB. - A name is uppercase letters, digits and
_(^[A-Z_][A-Z0-9_]*$), at most 128 characters. kortix validateand the deploy refuse reserved names: every name that starts withKORTIX_orOPENCODE_;PORT,PATH,HOME,PWD,USER,LOGNAME,SHELL,HOSTNAME,TERM,TMPDIR,NODE_ENV,NODE_OPTIONS,LD_PRELOAD,LD_LIBRARY_PATH; andSLACK_SIGNING_SECRET,SLACK_BOT_TOKEN.- A name cannot be in both
envandsecrets. secretsmaps a runtime name to a project secret identifier. The secret must have theenvironmentexposure. Kortix resolves it when it creates the machine and never writes it into the image. See Secrets.
URLs and what visitors see
Kortix assigns the hostname when it creates the App and never changes it. On
Kortix Cloud it is https://<env>-<slug>-<route-key>.apps.kortix.com, where
<route-key> is 16 hex characters. Another service answers on the same label
plus --<service>. A service URL is null when the label would pass 63
characters: reach that service through a binding. A self-hosted instance serves
Apps on the wildcard domain in KORTIX_APPS_BASE_DOMAIN. See
Self-hosting.
A static App serves files at once:
- HTML revalidates on every load (
no-cache). - Hashed build output (
_next/static/, and hashed files underassets/orstatic/js|css|media/) is cached for a year. Every other file revalidates. - With SPA fallback on, a page navigation to an unknown path gets
index.html. A missing file gets your404.htmlwith status404. /docsredirects308to/docs/whendocs/index.htmlexists.- Responses are
privateto shared caches unless the App ispublic.
An authorized request to a stopped server App wakes it, waits for readiness and
then serves that same request. While the App deploys or wakes, a browser gets a
status page with HTTP 202, retry-after: 3 and a 3-second refresh. Other
clients get 202 and JSON with a code that names the phase, for example
app_deployment_building or app_starting. Retry after 3 seconds.
| State | HTTP | code |
|---|---|---|
| Last deployment failed | 503 | app_deployment_failed |
| Last deployment cancelled | 503 | app_deployment_cancelled |
Account limits answer 402 or 429: see
Run mode, machines and scale.
Releases, rollback and deletion
kortix apps releases my-site # vN, status, strategy; marks the live one
kortix apps show my-site # capabilities, links, run mode, releases
kortix apps logs my-site [deployment] # runtime logs; --after N --limit N
kortix apps rollback my-site v3 # a new release from v3; waits unless --no-wait
- A rollback creates a new release from the target’s artifact and config, and
rolls it out like a deploy. Volumes keep their data. A target that is not
readyanswers409 release_not_ready. - An App keeps its live release and the 5 newest other ready releases as
rollback targets. Kortix retires older ones after each deploy. A self-hosted
operator sets the number with
KORTIX_APPS_RETAINED_DEPLOYMENTS. - Runtime log lines carry a
source:app(your process),appd(the App supervisor) orcaddy(the access log, which never holds request headers). - In the web app, open the App, then a release, to read its events and build log.
Delete one release, or the whole App:
kortix apps delete my-site --deployment v1 --yes # one release and its image
kortix apps delete my-site --yes # the App, its machines and images
- The live release cannot be deleted (
409 deployment_live). Roll back first. - A release that is still building cannot be deleted
(
409 deployment_in_progress). - A build that hits the provider’s image limit fails with
app_image_quota_exceeded. Delete releases you do not need. - Deleting an App needs
project.app.write. An App with volumes also needsproject.app.admin. Each volume keeps afinalsnapshot for 7 days, then Kortix purges it. Export data you need first.
Template: Convex backend
The convex template deploys a self-hosted Convex backend with Kortix sign-in:
a database, server functions, realtime queries, file storage, schedules and
search. It is an ordinary server App with a volume.
kortix apps launch --template convex # --provider platinum, --no-deploy
Run it in the directory that holds your kortix.yaml. A slug that is already in
the file is refused: pick another --name. The backend mounts a volume, so it
needs Platinum: pass --provider platinum when Platinum is not your default
provider. --no-deploy writes kortix.yaml only.
launch writes two blocks into kortix.yaml and the dashboard’s two files,
apps/backend/dashboard/Dockerfile and apps/backend/dashboard/start.js (a file that
already exists is kept). It deploys backend, then writes the backend’s admin key to
the project secret BACKEND_ADMIN_KEY (the slug in capitals, - becomes _). It
reads the key from the machine and never prints it. The key belongs to the
backend just launched, so every launch writes it again. Then it deploys
backend-dashboard.
There are two Apps because Convex ships the backend and the dashboard as two images, and a Kortix App runs one image. The Apps tab shows them as one: the dashboard is the backend’s Dashboard tab. The backend runs 24/7 with its volume; the dashboard sleeps when idle.
| App | What it is |
|---|---|
backend |
The Convex backend, image pinned by digest. Service http on port 3210 (access: public, a /version check): the client URL, the sync WebSocket and the admin API. Service site on port 3211 (access: public): HTTP actions. Volume data at /convex/data, 10 GB. Always on, 1 vCPU / 1 GB / 10 GB disk. Label backend. display: { preview: none }. |
backend-dashboard |
The Convex dashboard, built from apps/backend/dashboard/ (type: dockerfile). access: admins, on demand. It uses backend and reaches it through /_kortix/apps/backend. display: { parent: backend } makes it the Dashboard tab of backend. secrets: { NEXT_PUBLIC_ADMIN_KEY: BACKEND_ADMIN_KEY } signs it in. |
start.js runs Convex’s dashboard on port 6792. On the App’s port 6791 it
serves / as a page that frames the dashboard and answers its sign-in request
(Convex’s embedding protocol) with the admin key; every other path goes to the
dashboard. Only project admins reach the App, so the page may carry the key.
Both backend services are public because Convex checks sign-in in each
function. Keep the backend at 1 machine: a second machine gets its own empty
volume, which makes a second, separate database.
Connect and push your Convex code, for example from apps/backend/. A session has
BACKEND_ADMIN_KEY in its environment:
cd apps/backend
npm install convex@1.46.0 @kortix/sdk
export CONVEX_SELF_HOSTED_URL=<backend App URL> CONVEX_SELF_HOSTED_ADMIN_KEY="$BACKEND_ADMIN_KEY"
kortix apps auth-env backend | while IFS='=' read -r k v; do printf '%s' "$v" | npx convex env set "$k"; done
npx convex deploy
Use Convex CLI 1.46.0: it matches the pinned backend.
Kortix sign-in for the functions:
// convex/auth.config.ts
export default {
providers: [{
type: 'customJwt',
issuer: process.env.KORTIX_AUTH_ISSUER!,
applicationID: process.env.KORTIX_AUTH_AUDIENCE!,
jwks: process.env.KORTIX_AUTH_JWKS!,
algorithm: 'ES256',
}],
};
// convex/members.ts
import { query } from './_generated/server';
import { readKortixMember, requireKortixMember } from '@kortix/sdk';
export const me = query({
args: {},
handler: async (ctx) => readKortixMember(await ctx.auth.getUserIdentity()), // null when signed out
});
// In every function that touches non-public data:
// const member = requireKortixMember(await ctx.auth.getUserIdentity(), { roles: ['owner', 'admin'] });
In React, read the member with useQuery(api.members.me). Do not call
useConvexAuth(): it throws under a plain ConvexProvider.
-
Dashboard. Open Apps →
backend→ Dashboard (project admins). It opens signed in.kortix apps access-link backend-dashboardprints a 5-minute link to the same page. -
A backend launched before the Dashboard tab shows its dashboard as its own card, or asks for the key. Run
kortix apps launch --template convex --no-deployin an empty directory and copy itsapps/backend/dashboard/files and bothkortix.yamlblocks into your project. Then write the key and deploy the dashboard:kortix apps exec backend -- sh -c "cd /convex && ./generate_admin_key.sh" | tail -n 1 | kortix secrets set BACKEND_ADMIN_KEY=- kortix apps deploy --manifest-app backend-dashboardAdd
display: { preview: none }to the backend block; a deploy ofbackendapplies it. -
Before a migration, a bulk import or a backfill, take
kortix apps volumes snapshot backend data. Restore withkortix apps volumes restore backend data <snapshot>: the live data returns to the snapshot. -
Keep your own copy with
npx convex export --path backup.zip --include-file-storage. -
From outside,
kortix apps connect backendprints how to call the backend: its URLs and snippets for a sign-in token, an HTTP call and your own server. It prints no secret.
Build an internal tool
An internal tool is a Convex backend App for data and logic, plus a static App for the UI. Kortix signs the team in: the UI knows who is looking, and every function knows who calls it.
apps/backend/convex/ Convex functions: schema.ts, auth.config.ts, members.ts, ...
apps/crm/ Vite + React UI: src/convex.ts wires the binding
apps/backend/dashboard/ the dashboard App: Dockerfile + start.js (the template)
kortix.yaml apps.backend and apps.backend-dashboard (the template), apps.crm
Launch the backend
kortix apps launch --template convex.
Write the functions
Write the schema and functions in apps/backend/convex/, with auth.config.ts from the template section. Call requireKortixMember first in every public query and mutation. Push with npx convex deploy.
Wire the UI to the backend
Connect through the binding:
// apps/crm/src/convex.ts
import { ConvexReactClient } from 'convex/react';
import { kortixBinding } from '@kortix/sdk';
const backend = kortixBinding('backend');
export const convex = new ConvexReactClient(backend.url);
convex.setAuth(backend.token);Declare the UI App
apps:
crm:
path: apps/crm/dist
type: static
spa: true
uses: [backend]Build, deploy and open it to the project
(cd apps/crm && npm run build)
kortix apps deploy apps/crm/dist
kortix apps access crm --mode projectNever use public for internal data. A public App has no signed-in member, so
every function that requires one refuses the call.
| What broke | Undo |
|---|---|
| The UI | kortix apps rollback crm <vN> |
| The backend’s image or config | kortix apps rollback backend <vN>. The volume keeps its data. |
| Convex code | Check out the last good apps/backend/convex from git and run npx convex deploy again. |
| Data | kortix apps volumes restore backend data <snapshot>. Every later change is lost. |
Build on Apps from TypeScript
kortix.project(projectId).apps in @kortix/sdk exposes every App operation.
The CLI uses the same calls.
Branch on an App’s capabilities, never on how the App was made:
| Capability | The App has it when |
|---|---|
deployments, rollback, preview, member_tokens |
Always |
static |
The live release is static |
sleep, machines, scale, exec |
The live release runs a server |
volumes, snapshots |
The App has a volume |
A call that needs a capability the App lacks answers 409:
{ "error": "This App does not support exec.", "code": "app_capability_unsupported", "capability": "exec" }
The CLI checks first: it exits 1 with App <slug> does not support <capability>.
and sends nothing. The machine and volume routes of an App with no machines
answer 409 app_no_machines.
import { createKortix, isFeatureDisabledError } from '@kortix/sdk';
const kortix = createKortix({ backendUrl: 'https://api.kortix.com/v1', getToken: async () => process.env.KORTIX_TOKEN! });
const apps = kortix.project(projectId).apps;
const app = await apps.create({ slug: 'docs', name: 'Docs' });
const artifact = await apps.artifacts.uploadArchive(tarGzBytes); // register, upload, hash, finalize
const deployment = await apps.deployments.create(app.app_id, {
artifact_id: artifact.artifact_id,
source: { kind: 'static', spa: true },
});
for (;;) {
const { deployment: current } = await apps.deployments.get(app.app_id, deployment.deployment_id);
if (current.status === 'ready') break;
if (current.status === 'failed' || current.status === 'cancelled') throw new Error(current.error ?? current.status);
await new Promise((resolve) => setTimeout(resolve, 2000));
}
| Group | Calls |
|---|---|
| App | list, create, get, update (also labels and display), remove, start, stop, rollback(appId, deploymentId) |
| Artifacts | artifacts.register (archive or oci_image), artifacts.uploadArchive, artifacts.finalize |
| Releases | deployments.create (artifact_id, source, provider?, environment?, secrets?, config?), deployments.list, deployments.get (with events and build log), deployments.logs, deployments.remove |
| Machines | machines.list, machines.start, machines.stop, machines.restart, machines.destroy, scale(appId, { worker: 3 }), exec(appId, { command: ['ls', '/data'] }) |
| Volumes | volumes.list, volumes.create, volumes.update, volumes.destroy(appId, volumeId, name), volumes.snapshots, volumes.snapshot, volumes.restore, volumes.fork |
| Access | access.get, access.update (mode, member_ids, group_ids, password, viewer_token_scope), access.session (a 5-minute link), access.agents |
| Sign-in | token(appId), authEnv(appId) |
isFeatureDisabledError(error) is true for the 403 feature_disabled that a
project without Apps answers. React hooks in @kortix/sdk/react:
useProjectApps, useAppDeployments, useAppDeployment, useAppAccess,
useAppMachines, useAppVolumes and useAppVolumeSnapshots. Signatures and
types: SDK reference and
React hooks.
Permissions and CLI reference
| Permission | Allows |
|---|---|
project.app.read |
List and view Apps, releases, logs, machines, volumes, snapshots, the access policy and the granted agents. Create an access link. Mint a sign-in token. |
project.app.write |
Create, rename, resize and label Apps, set run mode and uses, change the access policy, delete a release, delete an App |
project.app.deploy |
Upload artifacts, deploy, roll back, start and stop. Start, stop, restart and destroy machines. Scale. Create, extend, snapshot and fork volumes. |
project.app.admin |
exec, restore or destroy a volume, open an access: admins service, delete an App that has volumes |
Every route also needs the project’s Apps switch on. See Accounts & access for roles and custom roles.
<app> is an App id or slug. Every subcommand takes --project <id>,
--host <name> and --json.
| Command | What it does |
|---|---|
kortix apps list (ls) |
The Apps you may open: slug, state, URL |
kortix apps create <slug> (new) |
Create an App without a deploy. --name, --cpu, --memory, --disk, --idle-timeout, --always-on or --on-demand, --uses |
kortix apps launch --template convex |
Write a template’s blocks to kortix.yaml in the current directory and deploy them. --name <slug>, --provider <name>, --no-deploy |
kortix apps deploy [path] |
Deploy a directory, a .tar.gz or --image. Flags: --app, --slug, --name, --manifest-app, --type static|bundle|dockerfile, --command, --port, --dockerfile, --root, --output-dir, --install-command, --build-command, --readiness-path, --spa or --no-spa, --include-node-modules, --provider daytona|platinum|e2b, --access, --password, --members, --groups, --always-on or --on-demand, --no-wait, --wait-seconds |
kortix apps set <app> (update) |
Change --name, --cpu, --memory-gb, --disk-gb, --idle-timeout, --always-on or --on-demand, --uses |
kortix apps link <app> --uses <slugs> |
Add used Apps. unlink removes them. |
kortix apps show <app> (get) |
Capabilities, links, run mode and releases |
kortix apps releases <app> |
Releases, marking the live one |
kortix apps rollback <app> <vN> |
A new release from vN. --no-wait |
kortix apps logs <app> [deployment] |
Runtime logs. --after N, --limit N |
kortix apps start <app>, stop <app> |
Wake a server App, or suspend it now |
kortix apps machines [ls|start|stop|restart|destroy] <app> [machine] (machine) |
List machines or act on one |
kortix apps scale count <app> <n> |
Machines for a group (0–20). --process <group> (default app) |
kortix apps scale show <app> |
Machines per group |
kortix apps exec <app> -- <command…> |
Run one command. --machine <id>, --timeout <s> |
kortix apps volumes [ls|create|extend|snapshots|snapshot|restore|fork|destroy] <app> … (volume) |
Manage volumes and snapshots |
kortix apps labels <app> [a,b] |
Show or replace labels. --clear |
kortix apps access <app> |
Read or set access. --mode, --password, --members, --groups, --viewer off|identity|api |
kortix apps access-link <app> |
A 5-minute sign-in link |
kortix apps connect <app> |
How to reach the App from code. No secret. |
kortix apps token <app> |
A 15-minute sign-in token that names you |
kortix apps auth-env <app> |
The KORTIX_AUTH_* values as dotenv lines |
kortix apps delete <app> --yes (rm, remove) |
Delete the App. --deployment <vN> deletes one release. |
The whole CLI: CLI. Every route: the REST reference at
https://api.kortix.com/v1/docs.