Apps Overview
Ship a web frontend with your agents. Every deploy builds it from your project, each environment serves it on its own URL, and the browser calls agents and project data through connic.
On this page
An app is a static frontend, such as a React or Vue single-page app, that lives in your project: its settings in apps/<key>.yaml, its sources in apps/<key>/. A deploy builds it and serves it in the deployment's environment at https://<key>-<id>.connic.app or on a custom domain. When the app requires sign-in, connic signs visitors in before it serves a single file. The app's browser code reaches agents and data through paths on its own host, so it needs no API key.
Declare an app
Each file in apps/ declares one app, and a folder with the same name holds its sources. The file name without .yaml is the app's key: apps/portal.yaml builds from apps/portal/.
my-agent-project/
├── agents/
├── connectors/
│ └── portal-chat.yaml
└── apps/
├── portal.yaml # the app's settings; the file name is its key
└── portal/ # the app's sources
├── package.json
├── package-lock.json
└── src/name: Support portal # optional, defaults to the file name
build:
command: npm run build
output: dist # folder with index.html, inside apps/portal/
auth: # optional; omit it to set sign-in in the dashboard
mode: oidc
issuer: ${PORTAL_ISSUER}
client_id: ${PORTAL_CLIENT_ID}
client_secret: ${PORTAL_CLIENT_SECRET}
allowed_domains: [example.com]
connectors: # apps_only connectors the app calls
- portal-chat
access: # data the browser may use; everything else is denied
database:
tickets:
read: doc.author_id == user.id
insert: true
set:
author_id: user.id
environments: # omit to deploy to every environment
Staging:
auth:
mode: connic
Production:
domain: portal.example.com
dev: # connic dev environments the list doesn't name
auth:
mode: connic| Field | Type | Status | Description |
|---|---|---|---|
| name | string | Optional | Display name, 1 to 200 characters.Default: file name |
| build | object | Required | How connic builds the app. See Build. |
| command | string | Required | Build command, run in apps/<key>/ after the install. One line, up to 500 characters. |
| output | string | Required | Folder the build writes, relative to apps/<key>/, such as dist. It must contain index.html. |
| install | string | Optional | Install command. Defaults to the one for the lockfile: npm ci, pnpm install --frozen-lockfile, or yarn install --frozen-lockfile. |
| node | string | Optional | Node.js version: 22 or 24.Default: 24 |
| auth | object | Optional | Who can open the app: mode is public, connic, or oidc, plus the fields of that mode. Omit it to set sign-in in the dashboard. See Sign-in. |
| connectors | string[] | Optional | Keys of the connector files the app calls. See Call agents. |
| domain | string | Optional | Hostname or ID of an active custom domain of the project. Omit it to set the domain in the dashboard. See Domains. |
| content_security_policy | string | Optional | Replaces the default Content-Security-Policy. One line, up to 4,000 characters. |
| lookups | map | Optional | Documents that rules look up once per request, by name. See Lookups. |
| access | object | Optional | What the browser may read and change, per resource and operation. Without it, the app can't use any project data. See Access rules. |
| environments | map | Optional | Environment names that get the app, each with optional overrides. Omit it to deploy to every environment. See Environments and overrides. |
| dev | object | Optional | Overrides for the connic dev environments that environments doesn't name. With environments, those get the app only when dev is set. See Environments and overrides. |
Files sit directly in apps/ and use the .yaml extension. A file name starts with a letter or digit, contains only letters, digits, -, and _, and has at most 100 characters. Every file needs its source folder, and every folder in apps/ needs its file. connic lint checks the files, their rules, and the connectors they list offline, and connic deploy runs it first.
Environments and overrides
Without environments, every environment gets the app with the same settings. With it, only the listed environments do. Names must match exactly, and a name that doesn't exist in the project fails the deploy. Production: {} deploys the file unchanged. Each environment gets an app of its own, with its own URL, sign-in, and stats.
An override can set name, auth, connectors, domain, content_security_policy, lookups, and access. Each replaces the whole value from the top of the file: auth in Staging above replaces every OIDC setting, and an access override replaces all rules. To remove domain, content_security_policy, lookups, or access in one environment, set it to null there. build is the same in every environment.
dev takes the same fields and applies to every connic dev environment that environments doesn't name, quick tests included. With environments, those dev environments get the app only when dev is set; dev: {} uses the top of the file unchanged. Dev environments have Variables of their own, so the example above signs in with connic accounts there instead of reading the OIDC Variables.
Variables
${NAME} reads a Variable of the target environment in domain and in issuer, client_id, and client_secret of auth. connic resolves them when the deploy starts, and a missing Variable fails the deploy. client_secret must be a single ${NAME} reference.
Build commands can't use ${NAME}, and Variables aren't available during the build. An app that behaves differently per environment reads /_connic/config.json at runtime.
Build
Any framework that writes a static folder with an index.html works, such as Vite with React, Vue, Svelte, or Solid, Angular, Astro, Next.js with output: "export", SvelteKit with adapter-static, or Nuxt with nuxt generate. connic serves the files as they are, so server rendering, API routes, and server middleware don't run. Agents behind connectors and the data API do that work.
Each deploy builds its apps in a Node.js image of the version in build.node, as an unprivileged user and with CI=true. It copies package.json and the lockfile, runs the install command, copies the rest of apps/<key>/, runs build.command, and checks that build.output contains index.html. pnpm and yarn run through Corepack. The build output is part of the deployment log, and a failed build fails the deploy. Every deployment keeps its own build, so a rollback serves the older build again.
Sources
apps/<key>/ needs a package.json and a lockfile: package-lock.json, pnpm-lock.yaml, or yarn.lock. Deploys upload the app sources with the rest of the project, up to 20 MiB for all apps together. The upload skips node_modules, hidden files and folders, and dist, build, out, and coverage folders directly inside an app folder.
App sources may only contain these file types, and code must be UTF-8 text: .js .mjs .cjs .jsx .ts .mts .cts .tsx .vue .svelte .astro .html .htm .css .scss .sass .less .json .jsonc .md .mdx .txt .yaml .yml .toml .svg .xml .webmanifest .graphql .gql .lock .png .jpg .jpeg .gif .webp .avif .ico .woff .woff2 .ttf .otf .mp3 .mp4 .webm .pdf. Any other file, including one without an extension, fails the upload.
Plans and limits
Apps are part of the pro and enterprise plans. There, an environment can have up to 5 apps, each app's build output can be up to 25 MiB, a deploy's apps can have up to 20,000 files together, and the app builds of a deploy have 10 minutes. A deploy over a limit fails and names it.
Serving
The app answers every path of its host. / and paths that end in / serve that folder's index.html. A path without a file extension tries the file itself, then <path>.html, then <path>/index.html, and finally the root index.html, so client-side routes survive a reload. A missing file with an extension gets the app's 404.html with status 404, or a plain 404 when the app has none.
Browsers keep files whose names carry a content hash, such as assets/index-B3k9x2Qa.js from Vite or anything under _next/static/, for a year. Every other file is revalidated on each request, so a deploy shows up on the next page load. Apps in dev environments aren't cached at all.
An app that the active deployment no longer declares is retired: it stops serving and its visitors are signed out. It keeps its URL, so it comes back on the same address when a later deployment declares it again.
Security headers
Every file is served with X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, HSTS, and this Content-Security-Policy:
default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'The default loads scripts, styles, images, and fonts only from the app's own host and sends requests only to it, which covers connectors and the data API. To use other hosts, set content_security_policy; it replaces the default completely. Next.js static exports use inline scripts and need script-src 'self' 'unsafe-inline'.
Reserved paths
Paths under /_connic/ belong to connic and never reach the app's files. Unknown /_connic/ paths answer 404.
| Path | Purpose |
|---|---|
| /_connic/config.json | The app's key and name, the environment, the sign-in mode, the connector keys, and the sign-in and sign-out paths. See /_connic/config.json. |
| /_connic/login, /_connic/logout | Start sign-in and sign out. See Sign-in. |
| /_connic/c/<key> | The app's connectors. See Call agents. |
| /_connic/me, /_connic/db/…, /_connic/files/…, /_connic/runs…, /_connic/sessions/…, /_connic/approvals… | The data API. |
/_connic/config.json
The same build can run in several environments, so the app reads what differs at runtime. On public apps, login_url and logout_url are null.
{
"app": {"key": "portal", "name": "Support portal"},
"environment": "Production",
"auth": "oidc",
"connectors": ["portal-chat"],
"login_url": "/_connic/login",
"logout_url": "/_connic/logout"
}Call agents
An app reaches agents through connectors. List their keys in connectors. Each one must be a sync webhook or a WebSocket connector whose file sets apps_only: true and that is deployed to the same environments as the app. An app with dev needs its connectors in dev environments too, so a connector file with environments needs dev as well.
type: webhook
mode: sync
apps_only: true # only apps that list this connector can call it
agents:
- supportThe app calls the connector on its own host at /_connic/c/<key>. connic checks the visitor's session first, then forwards the request with the app and the visitor attached. Requests from other sites get 403: a request's Origin must be the app's own, and WebSocket connections must send it. An apps_only connector accepts nothing else: its own URL rejects direct requests, and its secret isn't used. apps_only can only be set in connectors/*.yaml.
const response = await fetch("/_connic/c/portal-chat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ message: "Where is my order?" }),
});
if (response.status === 401) {
location.assign(`/_connic/login?return=${encodeURIComponent(location.pathname)}`);
}
const result = await response.json();A WebSocket connector needs no secret message, so the first message is already a chat message. Its path takes one more segment, a connection ID that the client chooses, as in /_connic/c/portal-chat/<connection id>. Each end user has their own connection IDs. A connection with the ID of an open one replaces it, and answers the visitor missed while disconnected arrive once they reconnect with that ID; see Reconnect. Query strings are passed on. A key the app doesn't list answers 404.
const socket = new WebSocket(`wss://${location.host}/_connic/c/portal-chat`);
socket.onopen = () => socket.send(JSON.stringify({ message: "Where is my order?" }));App and end user in the run
Runs started through an app carry two context keys. connic sets them from the request it forwarded, never from the payload, so middleware and tools can trust them:
context["app"]
# {"id": "0b9c3f1e-5d2a-4e8b-9f1c-7a6d2e4b8c10", "key": "portal"}
context["end_user"]
# {"id": "8d2f6c1a-4b7e-4f0a-9c3d-2e5b7a9f1c64", "email": "ada@example.com",
# "name": "Ada Lovelace", "groups": ["support"], "signed_in": True}On a public app, end_user.id is the browser's visitor ID, email and name are None, groups is empty, and signed_in is False. Use context.end_user.id as the user of per-user MCP sign-in, or show visitors their own runs with the access rule run.context.end_user.id == user.id.
In the dashboard
The Apps page lists the apps of the selected environment, dev environments included, with their URL, sign-in, and the last 7 days of visitors, views, sign-ins, and data requests. Click an app to open its details:
- Settings: the default URL, the custom domain, sign-in, and the "Powered by connic" line at the end of every page. A setting that the file declares shows an In code badge and can only be changed in code.
- Stats: the last 30 days of visitors, views, sign-ins, data requests, failed sign-ins, denied requests, and connector calls. A view is a page the app served, and a visitor counts once per UTC day.
- Access: the lookups and access rules of the deployed file, read-only.
Seeing apps takes the View apps permission, and changing their settings takes Manage apps, which the default Admin group has. See Team and permissions. connic MCP offers the same with list_apps, get_app, update_app, and get_app_stats.