Skip to main content

The arc app CLI

The app kit (@arc/app-kit) ships a small CLI, arc, with three commands:

CommandWhat it does
arc app init <dir>Scaffolds a new app: a manifest ARC accepts, a server that verifies webhooks and session tokens, and a git-ignored place for your secrets.
arc app devChecks your token reaches your private app, then serves test-mode events on 127.0.0.1.
arc app deployPublishes your manifest as a new version of your private app (publishPrivateAppVersion).

It needs Node 18 or later and has no dependencies beyond the kit.

Secrets stay in the environment​

The CLI reads every secret from environment variables and never writes one to disk:

VariableUsed byWhat it is
ARC_API_URLdev, deployYour dev tenant's address, e.g. https://myapp-sandbox.<your ARC domain>. Must be https; plain http is allowed only to 127.0.0.1 or localhost.
ARC_APP_IDdev, deployYour private app's id (Settings → Apps). --app-id overrides it.
ARC_TOKENdev, deployA staff access token from your dev tenant, for a user with settings:apps:manage.
ARC_CLIENT_IDdevYour app's OAuth client id.
ARC_CLIENT_SECRETdevYour app's client secret.

There is no --token flag: a flag ends up in your shell history. arc app init writes a .env.example with these names and a .gitignore that excludes .env. Copy the example to .env, fill it in, and load it with Node's --env-file:

node --env-file=.env node_modules/.bin/arc app deploy

Use a dev tenant's token, never a production one (see Dev tenants and test mode).

arc app init​

arc app init my-app --name "My app"

Writes into my-app/ (which must be empty or absent):

  • arc.app.json — the manifest. It is valid as written; replace the example.com URLs with your app's. It subscribes to app.installed and app.uninstalled, which need no scope.
  • server.mjs — a Node server built on the kit's createArcAppHandler (below).
  • package.json, .env.example, .gitignore, README.md.

arc app dev​

arc app dev [--manifest arc.app.json] [--port 8787] [--app-id <id>] [--api-url <url>] [--offline]
  1. Reads and checks the manifest (name, semantic version, API version, redirect URLs).
  2. Asks ARC for your private app with ARC_TOKEN, so a wrong token or app id fails now, not at deploy. --offline skips this step.
  3. Serves the app on http://127.0.0.1:<port>: POST /arc/webhooks and GET /arc/session. It prints each event it handles.

The dev server takes test-mode events only. An event whose X-ARC-Environment is not test is refused with HTTP 409, which ARC treats as final, so a live organization's traffic can never land in your sandbox.

ARC delivers webhooks to a public https URL only, so to receive your dev tenant's events, expose the port with a tunnel of your choice and set webhooks.endpoint in the manifest to the tunnel's address.

arc app deploy​

arc app deploy [--manifest arc.app.json] [--app-id <id>] [--api-url <url>] [--no-make-current]

Publishes the manifest as a new version. ARC validates it in full (scopes, topics, redirect URLs, extensions); when it refuses, the CLI prints ARC's own messages and exits with status 1. A version is immutable and its number must be new for the app, so bump version before each deploy. --no-make-current publishes without making it the version new installs get.

Exit status: 0 on success, 1 when the command failed, 2 for a usage error.

The app handler​

createArcAppHandler is the server init scaffolds and dev runs. Use it as is, or as a reference:

import { createServer } from 'node:http';
import { createArcAppHandler } from '@arc/app-kit';

const handler = createArcAppHandler({
clientId: process.env.ARC_CLIENT_ID!,
secret: process.env.ARC_CLIENT_SECRET!, // or [current, previous] during a rotation
environments: ['live'], // production: refuse test events
onEvent: async (event, { environment, deliveryId }) => { /* … */ },
});
createServer(handler).listen(8787);
RouteAnswers
POST /arc/webhooks200 when handled (an event id seen before is acknowledged, not handled again); 401 for a bad signature, a stale timestamp or a missing header; 409 for an environment the server does not take; 500 when onEvent throws, so ARC retries.
GET /arc/session200 with { organizationId, userId, installationId, target } for a valid session token in Authorization: Bearer; 401 otherwise, including a token whose jti was already used.

Seen event ids and jtis are kept in memory. A real app keeps them in its own store.

The API's conformance suite checks this handler, and the manifest init writes, against ARC's own signers.