Backend Kit

Most desktop apps eventually need a little backend: a switch to turn a feature off, a place to sync a buyer's presets, crash reports, a way to call an AI model without shipping your API key. The Backend Kit gives every AppsXi app those pieces, included with AppsXi. Set it up in Developer → your app → Backend Kit.

PieceNeedsWhat it does
Remote config and flagsProduct keyJSON config and on/off flags, with percentage rollouts and version limits
StorageSign in with AppsXiPer-buyer key-value storage, shared by all their computers
Crash reportsProduct key, buyer opt-inGrouped crashes with home folder names removed
Usage countsProduct key, buyer opt-inAnonymous daily event counts and active computers
AI proxySign in with AppsXiOpenAI and Anthropic calls with your key kept on AppsXi
AnnouncementsProduct keyShort notes shown in the app and in buyers' libraries

The kit uses the same product key as Sign in with AppsXi. Test keys (pk_test_…) see the sandbox config and only sandbox data, so you can rehearse without touching live buyers.

TypeScript (Electron, Node)

Copy appxi-signin.ts and appxi-kit.ts into your project.

TypeScript
import { AppsXi } from "./appxi-signin";
import { AppXiKit } from "./appxi-kit";

const appxi = new AppsXi({ clientId: "pk_live_…", publicKeys: ["<x>"], storageDir: app.getPath("userData") });
const kit = new AppXiKit(appxi, { appVersion: app.getVersion() });

await kit.config();                                   // at launch; offline it uses the last answer
if (kit.flag("new_editor")) enableNewEditor();
const limit = kit.value("export_limit", 10);

await kit.storage.put("presets", presets);            // signed-in buyers with a license or trial
const saved = await kit.storage.get("presets");       // null when there is nothing yet

if (settings.sendCrashReports) process.on("uncaughtException", (e) => kit.reportCrash(e));
if (settings.shareUsage) kit.track("export");         // batched; call kit.flush() on quit

const res = await kit.ai("anthropic", "messages", {
  model: "claude-haiku-4-5",
  max_tokens: 400,
  messages: [{ role: "user", content: "Summarize this clip's notes: …" }],
});

for (const a of await kit.announcements()) showNote(a.title, a.body, a.url);

Other languages call the HTTP endpoints below directly. They are plain JSON over HTTPS with CORS open, so they also work from a Tauri or web front end.

Remote config and feature flags

In the dashboard, each environment (live and sandbox) has a config (any JSON object up to 64 KB) and flags:

JSON
{
  "new_editor": { "enabled": true, "rollout": 25, "min_version": "2.1" },
  "legacy_export": { "enabled": false }
}
  • rollout (0 to 100) turns a flag on for that share of computers. It is stable: the same computer stays in or out as you raise it from 5 to 25 to 100.
  • min_version and max_version compare against the app version you send (2.10 is newer than 2.9).
  • The app gets flags already resolved to true or false, so your rollout rules are not visible to buyers.
Text
GET /api/v1/kit/config?client_id=pk_live_…&device_id=<device id>&app_version=2.1.0
→ {"config": {"export_limit": 10}, "flags": {"new_editor": true, "legacy_export": false}, "version": 7, "environment": "live"}

Check once at launch and, if you like, every few hours. Up to 240 checks an hour per computer.

Storage

Per-buyer key-value storage for settings, presets and sync state: the same data on every computer the buyer signs in on. Values are any JSON up to 64 KB, with up to 500 keys and 2 MB per buyer per app. Reading works for any signed-in buyer; writing needs a license or a running trial. Refunds do not delete data, but writes stop.

Send the Sign in with AppsXi session with every call:

Text
Authorization: Bearer axr_…
X-AppXi-Client: pk_live_…
X-AppXi-Device: <device id>
List keysGET /api/v1/kit/storage
ReadGET /api/v1/kit/storage/<key> → {"key", "value", "version", "updated_at"}, or 404 not_found
WritePUT /api/v1/kit/storage/<key> with {"value": …, "if_version": 3} → {"key", "version"}
DeleteDELETE /api/v1/kit/storage/<key> (optional If-Match: 3)

Keys are 1 to 128 letters, digits or . _ : / -. if_version (or If-Match) writes only if the stored version still matches, so two computers cannot silently overwrite each other: a stale write answers 409 {"error": "conflict", "version": 4}. Read again, merge, and write with the new version. Use 0 to write only if the key does not exist yet.

The buyer's data belongs to the buyer: AppsXi does not show it to you in the dashboard, only the totals. Deleting an AppsXi account deletes it.

Crash reports

Ask the buyer first, for example with a "Send crash reports" setting that is off by default, and only report while it is on.

Text
POST /api/v1/kit/crash
{"client_id": "pk_live_…", "device_id": "…", "app_version": "2.1.0", "title": "TypeError: x is undefined",
 "stack": "…", "os": "macos", "os_version": "15.1", "arch": "arm64", "context": {"screen": "export"}}
  • AppsXi replaces home folder names in stacks (/Users/jane/…, C:\Users\jane\…) before storing anything.
  • Reports are grouped by the title and the top frames, with memory addresses removed. The dashboard shows each group's count, computers, versions and the latest stack.
  • Mark a group resolved with the version that fixed it. If it happens again in a newer version, it reopens.
  • Up to 60 reports a day per computer. The 25 most recent reports per group are kept for up to 90 days, and groups not seen for 180 days are removed.

Usage counts

Anonymous daily counts of named events, plus how many computers were active each day. No buyer identity, no properties and no free text, by design. Ask the buyer first.

Text
POST /api/v1/kit/track
{"client_id": "pk_live_…", "device_id": "…", "app_version": "2.1.0", "events": [{"name": "export", "count": 3}]}

Event names are 1 to 64 letters, digits or _ . : -, up to 200 different names a day. Send a batch every few minutes rather than one request per event. Only live keys appear in the dashboard.

AI proxy

Ship AI features without putting your OpenAI or Anthropic key in the app, where anyone could extract it. Your app calls AppsXi with the buyer's session; AppsXi checks that they own the app (or are on a trial, if you allow it), checks your model list and daily limits, caps the answer's length, and forwards the request with your key. Answers, including streams, come back as the provider sent them.

ProviderEndpoints
OpenAIPOST /api/v1/kit/ai/openai/chat/completions, /responses, /embeddings
AnthropicPOST /api/v1/kit/ai/anthropic/messages, /messages/count_tokens

The body is the provider's own request format. Send the same three headers as storage. For Anthropic, anthropic-version defaults to 2023-06-01, and anthropic-beta is passed through except for betas that reach your account's files, containers or hosted tools.

What the proxy changes or refuses, to protect your bill:

  • The answer's length is capped at your longest answer setting (default 4,096 tokens): max_tokens (Anthropic and older OpenAI chat), max_completion_tokens or max_output_tokens are lowered to it, or set to it when missing.
  • n must be 1 for chat completions.
  • Tools that the provider runs and bills itself are refused: OpenAI web search, file search, code interpreter, image generation, hosted MCP and computer use; Anthropic web search, web fetch, code execution and MCP servers. Function tools, which your app runs, work as usual.

Set up in the dashboard:

  1. Paste an API key for each provider you use. Keys are encrypted at rest and never shown again. Set a spending limit at the provider too.
  2. Choose the limits: requests per buyer per day (default 100), tokens per buyer per day (optional), requests per day for the whole app (default 20,000), the longest answer per request, and whether trial users may use AI. While a request runs, its longest possible answer counts against the buyer's daily tokens, so many requests at once cannot go over the limit; the real usage replaces it when the answer ends.
  3. List the models you allow, for example gpt-5-mini, claude-haiku-* (required).
  4. Turn the proxy on.

Errors from AppsXi:

StatuserrorMeaning
401invalid_grantSign in again
402not_entitledNo license or trial (or trials are not allowed AI)
403not_enabled, model_not_allowedThe proxy is off, there is no key for this provider, or the model is not in your list
413too_largeRequests are limited to 1 MB
429rate_limitedA daily limit was reached; limits reset at midnight UTC
503temporarily_unavailableTry again shortly

Errors from the provider come back as upstream_invalid_request (with the provider's message, minus anything that identifies your account), upstream_rate_limited (429), upstream_auth (502: your key was refused) or upstream_unavailable (502), with provider_status, type and code from the provider.

Requests time out after about two minutes. If a buyer disconnects or a stream is cut off before the provider reports usage, the output is estimated from the text sent so far. Usage (requests and tokens per day, and your heaviest buyers by anonymous ID) is in the dashboard. You pay the provider directly; AppsXi does not mark up or resell model usage.

Announcements

Short notes from you to people who own or are trying the app: a new version, a known issue, a survey. Up to three a day, each shown for 1 to 365 days, optionally only to some versions. They appear in the app through the SDK and in buyers' AppsXi libraries. Not for advertising other products.

Text
GET /api/v1/kit/announcements?client_id=pk_live_…&app_version=2.1.0
→ {"announcements": [{"id": "…", "title": "Version 2.2 adds batch export", "body": "…", "url": "https://…", "created_at": "…"}]}

Limits and privacy

  • Everything is per app and per environment: test keys never see live data.
  • The device id is the same hash Sign in with AppsXi uses; AppsXi stores a second hash of it per app for crash and usage counts.
  • Usage counts and crash reports carry no buyer identity. Storage and AI usage are tied to the buyer's anonymous per-app ID, the same one the licensing API returns.
  • Rate limits answer 429 with {"error": "rate_limited"}. Back off and try later.