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.
| Piece | Needs | What it does |
|---|---|---|
| Remote config and flags | Product key | JSON config and on/off flags, with percentage rollouts and version limits |
| Storage | Sign in with AppsXi | Per-buyer key-value storage, shared by all their computers |
| Crash reports | Product key, buyer opt-in | Grouped crashes with home folder names removed |
| Usage counts | Product key, buyer opt-in | Anonymous daily event counts and active computers |
| AI proxy | Sign in with AppsXi | OpenAI and Anthropic calls with your key kept on AppsXi |
| Announcements | Product key | Short 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.
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:
{
"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_versionandmax_versioncompare against the app version you send (2.10is newer than2.9).- The app gets flags already resolved to
trueorfalse, so your rollout rules are not visible to buyers.
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:
Authorization: Bearer axr_…
X-AppXi-Client: pk_live_…
X-AppXi-Device: <device id>| List keys | GET /api/v1/kit/storage |
| Read | GET /api/v1/kit/storage/<key> → {"key", "value", "version", "updated_at"}, or 404 not_found |
| Write | PUT /api/v1/kit/storage/<key> with {"value": …, "if_version": 3} → {"key", "version"} |
| Delete | DELETE /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.
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.
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.
| Provider | Endpoints |
|---|---|
| OpenAI | POST /api/v1/kit/ai/openai/chat/completions, /responses, /embeddings |
| Anthropic | POST /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_tokensormax_output_tokensare lowered to it, or set to it when missing. nmust 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:
- 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.
- 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.
- List the models you allow, for example
gpt-5-mini, claude-haiku-*(required). - Turn the proxy on.
Errors from AppsXi:
| Status | error | Meaning |
|---|---|---|
| 401 | invalid_grant | Sign in again |
| 402 | not_entitled | No license or trial (or trials are not allowed AI) |
| 403 | not_enabled, model_not_allowed | The proxy is off, there is no key for this provider, or the model is not in your list |
| 413 | too_large | Requests are limited to 1 MB |
| 429 | rate_limited | A daily limit was reached; limits reset at midnight UTC |
| 503 | temporarily_unavailable | Try 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.
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.