Skip to content

The Nexafon CLI

@nexafon/cli does the setup steps of the quickstart in your app’s own idiom and checks the result.

@nexafon/cli is not on npm yet, so npx @nexafon/cli fails for now. Until it is published, build it in a checkout of the Nexafon repo (after task setup) and run it from there; --from then installs the SDK packages from the same checkout:

Terminal window
pnpm --filter @nexafon/cli build
node /path/to/nexafon/cli/dist/bin.js init --dir /path/to/your/app --from /path/to/nexafon
node /path/to/nexafon/cli/dist/bin.js doctor --dir /path/to/your/app
node /path/to/nexafon/cli/dist/bin.js skill add --dir /path/to/your/app

Or pack it once and run the tarball from your app’s root:

Terminal window
pnpm --filter @nexafon/cli pack --pack-destination /tmp # in the checkout
npx --package /tmp/nexafon-cli-0.1.0.tgz nexafon doctor # in your app

After the release the commands are:

Terminal window
npx @nexafon/cli init # in your app's root
npx @nexafon/cli doctor
npx @nexafon/cli skill add

The CLI has no Nexafon address built in: pass them as flags or set NEXAFON_API_URL (https://api.nexafone.so), NEXAFON_APP_URL (https://app.nexafone.so) and NEXAFON_EMBED_CDN_URL (https://app.nexafone.so/embed).

Run it in your app’s root. It never writes a key: it leaves empty placeholders and tells you where to paste them.

  1. Detects the framework from package.json, composer.json and the files around them: React (Vite), Next.js, Vue, Nuxt, Angular, Svelte, SvelteKit, plain HTML, Laravel, PHP or WordPress (--framework overrides it).
  2. Installs the packages with your package manager (npm, pnpm, yarn or bun, from the lockfile), or Composer for PHP. With --from it builds and packs them from a checkout, or takes a folder of packed .tgz files; without it, it prints the command to use after release.
  3. Writes env placeholders: .env.local for Vite and Next.js, .env for Nuxt, SvelteKit, Angular and Laravel, with the secret key never under a public prefix. It adds the file to .gitignore when git would commit it, and adds the names (keys left empty) to .env.example, creating it when your app has none.
  4. Scaffolds the token endpoint and the component, and wires what it can safely change: the Vite plugin list (the endpoint runs in Vite’s dev and preview servers; the config imports ./server/nexafon-token.ts with its extension, as Vite’s native config loader needs), Nuxt’s runtimeConfig, Laravel’s routes/web.php. It never replaces an existing file without --force.
  5. Sets the appearance from your brand colour: it reads your Tailwind config, CSS custom properties (Tailwind v4 @theme, shadcn’s --primary, --brand), Sass variables and theme files (MUI, Vuetify), converts HSL and OKLCH to hex, and passes it as appearance.primaryColor (appearance). --color #rrggbb overrides it.
  6. Prints the next steps, including the origin to add to the publishable key’s allowed websites and the embed domains: --origin, else APP_URL in your env file, else your framework’s dev server. It must be the exact origin you open the app on, port included.

--dry-run prints every step the real run would take (env files, .gitignore, .env.example, files, patches and the install commands) and changes nothing. nexafon <command> --help prints the options of one command.

Every scaffolded token endpoint starts with a signedInUser that returns nobody, so it answers 401 until you connect it to your app’s own session, as user tokens describes. It also starts with a mayUseNexafon (nexafon_may_use() in plain PHP) that returns false, so it answers 403 to every signed-in user until you return true for your staff: a token can make its user a member of your workspace, so never for customers who signed up. Use your app’s staff flag or role; return true for every signed-in user only when your app truly has no customer accounts (no public sign-up), and say so in a comment there. WordPress uses the nexafon_use capability instead. Both steps are yours: the CLI cannot know your sign-in or who your staff are. The Laravel and WordPress scaffolds already sign for the framework’s own user ($request->user(), wp_get_current_user()), so there only the staff check is left. When the user record has no role, sign with agent; admin only for your app’s own admins.

Until you paste the secret key, every scaffolded endpoint answers 500 with the plain text “Nexafon secret key is not set” (no stack trace). In Laravel the CSRF check runs before the controller: a POST without the X-CSRF-TOKEN header gets 419, and 401 is what a signed-out request with a valid CSRF token gets. The scaffolded Blade component sends the header.

Script-tag integrations (Laravel, WordPress, plain HTML and PHP) need the loader’s address, NEXAFON_EMBED_CDN_URL: the folder that serves v1/nexafon.js, here https://app.nexafone.so/embed (the same address as in the HTML guide’s script tag; for a local Nexafon stack, EMBED_CDN_URL in the checkout’s .env). Pass it as --embed-cdn-url or set it in the environment and init writes it in; left empty, the page asks its own host for /v1/nexafon.js and fails with “Nexafon is not defined”. The npm packages have it built in.

Without Composer, init prints the PHP install also as docker run commands with the pinned Composer image. Installed from a checkout, composer.json gets a path repository at the checkout’s absolute path, which works only where that path exists until nexafon/nexafon-php is published.

Framework Token endpoint Component
React (Vite) server/nexafon-token.ts and a Vite plugin src/NexafonSupport.tsx
Next.js app/api/nexafon-token/route.ts app/nexafon-support.tsx
Vue server/nexafon-token.ts and a Vite plugin src/nexafon.ts, src/components/NexafonSupport.vue
Nuxt server/api/nexafon-token.post.ts plugins/nexafon.client.ts, components/NexafonSupport.vue
Angular src/nexafon-token.ts (Express) src/app/nexafon.ts, src/app/nexafon-support.ts
Svelte, SvelteKit a Vite plugin, or src/routes/api/nexafon-token/+server.ts src/lib/NexafonSupport.svelte
HTML nexafon/nexafon-token.ts (Node) nexafon/nexafon.html
Laravel NexafonTokenController and POST /nexafon/token <x-nexafon-support />, config/nexafon.php
PHP nexafon-token.php nexafon-snippet.html
WordPress a plugin’s REST route the [nexafon_inbox] shortcode
Flag
--dir <path> The app’s root (default: the current folder)
--framework <name> react-vite, next, vue, nuxt, angular, svelte, sveltekit, html, laravel, php, wordpress
--from <path> A Nexafon checkout or a folder of packed .tgz files to install from
--no-install Only print the install commands
--color <hex> The brand colour, instead of the detected one
--embed-cdn-url <url> The loader’s address for script-tag integrations (NEXAFON_EMBED_CDN_URL)
--origin <url> The origin you open your app on, printed for the allowed websites (NEXAFON_APP_ORIGIN)
--force Replace files init created before
--dry-run Print every step it would take, write and install nothing

doctor reads the keys from your app’s env files (and wp-config.php, and the environment) without printing them, and checks the integration end to end. It exits with 1 when a check fails, so it can run in CI.

Check What it catches
Keys A missing or empty key, the wrong key type (a secret key in the publishable key’s variable or the reverse), test and live keys mixed, a workspace key with a partner key
Loader script Whether the loader address your page uses (NEXAFON_EMBED_CDN_URL for Laravel and WordPress, the HTML/PHP snippet’s script tag, or the address built into the installed @nexafon/js) is set and serves v1/nexafon.js
Secrets out of the frontend A secret key under a VITE_, NEXT_PUBLIC_, NUXT_PUBLIC_ or PUBLIC_ variable, an env file with secrets that git tracks, a secret key written in frontend code
Token signing Signs a test user token and exchanges it at POST /api/v1/sdk/sessions; Nexafon checks the signature and stops before creating a session. Finds a secret key of another workspace or mode, a revoked key, a plan without SDK embeds
Allowed origins Whether your app’s origin may start sessions with the publishable key (origin not allowed: client_not_allowed)
Embed domains Whether Nexafon’s embed pages list your origin in their CSP frame-ancestors
Webhooks Whether your NEXAFON_WEBHOOK_SECRET is the endpoint’s signing secret (bad webhook secret), then sends a real webhook.test delivery and reports whether your receiver accepted it; --listen <port> receives it in doctor and verifies the signature there
Mobile push credentials Not available yet: the mobile SDKs are coming soon
Flag
--api-url <url> Nexafon’s API (NEXAFON_API_URL)
--nexafon-app-url <url> Nexafon’s app, which serves the embed pages (NEXAFON_APP_URL)
--origin <url> Your app’s origin (NEXAFON_APP_ORIGIN; default: your framework’s dev server)
--workspace <id> Your workspace id, to check webhooks with a secret key (NEXAFON_WORKSPACE_ID)
--webhook-endpoint <id> The endpoint to test (default: the one whose secret you configured)
--listen <port> Receive the test delivery on this port and verify it
--json Machine-readable output

Webhook checks need a secret key with developers:manage (partner.webhooks:manage for a partner key).

doctor takes Nexafon’s addresses only from its flags or your shell’s environment, never from the app’s env files, because it sends your secret key there. It refuses an address that is not https, except http to localhost.

Installs the Nexafon agent skill in a project: .claude/skills/nexafon/ for Claude Code, a Nexafon section in AGENTS.md for Codex, Copilot and other agents, and .cursor/rules/nexafon.mdc for Cursor. Running it again only updates them. --no-agents-md and --no-cursor leave those files alone.