Skip to main content
connic
Apps

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.

Last updated

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.

Sign-in
Open the app to anyone, to project members with their connic account, or to people from your identity provider. See Sign-in.
Agents
The browser calls agents through webhook and WebSocket connectors that only the app can reach. See Call agents.
Project data
The browser reads and changes the database, files, runs, sessions, and approvals under rules you write. See Access rules.

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/.

project
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/
apps/portal.yaml
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
FieldTypeStatusDescription
namestringOptionalDisplay name, 1 to 200 characters.Default: file name
buildobjectRequiredHow connic builds the app. See Build.
commandstringRequiredBuild command, run in apps/<key>/ after the install. One line, up to 500 characters.
outputstringRequiredFolder the build writes, relative to apps/<key>/, such as dist. It must contain index.html.
installstringOptionalInstall command. Defaults to the one for the lockfile: npm ci, pnpm install --frozen-lockfile, or yarn install --frozen-lockfile.
nodestringOptionalNode.js version: 22 or 24.Default: 24
authobjectOptionalWho 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.
connectorsstring[]OptionalKeys of the connector files the app calls. See Call agents.
domainstringOptionalHostname or ID of an active custom domain of the project. Omit it to set the domain in the dashboard. See Domains.
content_security_policystringOptionalReplaces the default Content-Security-Policy. One line, up to 4,000 characters.
lookupsmapOptionalDocuments that rules look up once per request, by name. See Lookups.
accessobjectOptionalWhat the browser may read and change, per resource and operation. Without it, the app can't use any project data. See Access rules.
environmentsmapOptionalEnvironment names that get the app, each with optional overrides. Omit it to deploy to every environment. See Environments and overrides.
devobjectOptionalOverrides 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 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.

PathPurpose
/_connic/config.jsonThe 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/logoutStart 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.

GET /_connic/config.json
{
  "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.

connectors/portal-chat.yaml
type: webhook
mode: sync
apps_only: true      # only apps that list this connector can call it
agents:
  - support

The 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.

webhook.js
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.

websocket.js
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
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.