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:
pnpm --filter @nexafon/cli buildnode /path/to/nexafon/cli/dist/bin.js init --dir /path/to/your/app --from /path/to/nexafonnode /path/to/nexafon/cli/dist/bin.js doctor --dir /path/to/your/appnode /path/to/nexafon/cli/dist/bin.js skill add --dir /path/to/your/appOr pack it once and run the tarball from your app’s root:
pnpm --filter @nexafon/cli pack --pack-destination /tmp # in the checkoutnpx --package /tmp/nexafon-cli-0.1.0.tgz nexafon doctor # in your appAfter the release the commands are:
npx @nexafon/cli init # in your app's rootnpx @nexafon/cli doctornpx @nexafon/cli skill addThe 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).
nexafon init
Section titled “nexafon init”Run it in your app’s root. It never writes a key: it leaves empty placeholders and tells you where to paste them.
- Detects the framework from
package.json,composer.jsonand the files around them: React (Vite), Next.js, Vue, Nuxt, Angular, Svelte, SvelteKit, plain HTML, Laravel, PHP or WordPress (--frameworkoverrides it). - Installs the packages with your package manager (npm, pnpm, yarn or bun, from the lockfile), or Composer for
PHP. With
--fromit builds and packs them from a checkout, or takes a folder of packed.tgzfiles; without it, it prints the command to use after release. - Writes env placeholders:
.env.localfor Vite and Next.js,.envfor Nuxt, SvelteKit, Angular and Laravel, with the secret key never under a public prefix. It adds the file to.gitignorewhen git would commit it, and adds the names (keys left empty) to.env.example, creating it when your app has none. - 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.tswith its extension, as Vite’s native config loader needs), Nuxt’sruntimeConfig, Laravel’sroutes/web.php. It never replaces an existing file without--force. - 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 asappearance.primaryColor(appearance).--color #rrggbboverrides it. - Prints the next steps, including the origin to add to the publishable key’s allowed websites and the embed
domains:
--origin, elseAPP_URLin 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 |
nexafon doctor
Section titled “nexafon doctor”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.
nexafon skill add
Section titled “nexafon skill add”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.