The arc app CLI
The app kit (@arc/app-kit) ships a small CLI, arc, with three commands:
| Command | What 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 dev | Checks your token reaches your private app, then serves test-mode events on 127.0.0.1. |
arc app deploy | Publishes 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:
| Variable | Used by | What it is |
|---|---|---|
ARC_API_URL | dev, deploy | Your 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_ID | dev, deploy | Your private app's id (Settings → Apps). --app-id overrides it. |
ARC_TOKEN | dev, deploy | A staff access token from your dev tenant, for a user with settings:apps:manage. |
ARC_CLIENT_ID | dev | Your app's OAuth client id. |
ARC_CLIENT_SECRET | dev | Your 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 theexample.comURLs with your app's. It subscribes toapp.installedandapp.uninstalled, which need no scope.server.mjs— a Node server built on the kit'screateArcAppHandler(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]
- Reads and checks the manifest (name, semantic version, API version, redirect URLs).
- Asks ARC for your private app with
ARC_TOKEN, so a wrong token or app id fails now, not at deploy.--offlineskips this step. - Serves the app on
http://127.0.0.1:<port>:POST /arc/webhooksandGET /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);
| Route | Answers |
|---|---|
POST /arc/webhooks | 200 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/session | 200 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.