Developers SDK reference

AffixIO SDK documentation

Your host proves. AffixIO verifies, signs with ML-DSA-65 and anchors the digest in a Merkle audit trail. There is no server-side prove. This page documents both Node packages against the live API at api.affix-io.com.

@affix-io/sdk 1.1.1 @affix-io/sdk-light 1.1.3 Node.js 18+ ESM Apache-2.0

Overview #

AffixIO answers one question at a time: is this subject eligible, yes or no. The credential never leaves your infrastructure. Your host builds the witness and generates the proof, then sends the proof and its digest for verification, post-quantum attestation and Merkle anchoring.

Two Node packages cover the two ends of the latency and assurance trade-off. Both share the same configuration surface, storage layer, offline queue, data-check adapters and carrier tooling, so moving between them is a one-line change.

@affix-io/sdk

v1.1.1

Local zero-knowledge prove with bundled Noir circuits over Barretenberg UltraHonk. Use it when the proof itself must reveal nothing and you can spend seconds on generation.

Prove
UltraHonk, seconds
Deps
Noir + bb.js
Sync
Circuit verify + attest

@affix-io/sdk-light

v1.1.3

Affix Light proves with HMAC-SHA256 over the same yes/no witness layout, using Node crypto only. Use it for kiosks, phones and gates where the interaction has to feel instant.

Prove
HMAC, milliseconds
Deps
Node crypto (+ QR)
Sync
Digest attest + Merkle
Assurance

Affix Light proofs are not SNARKs. They bind a decision and witness with a shared light key, and anyone holding that key can mint one. Keep the light secret on hardware you control and pick @affix-io/sdk when the proof must stand on its own cryptography.

Both packages are built around the same principles: privacy-preserving verification, crypto-agility through ML-DSA-65 (FIPS 204), compliance-grade audit trails, and offline-first operation so a dropped link never blocks a decision.

Install #

Both packages are ESM, ship TypeScript declarations and require Node.js 18 or later.

shell
# zero-knowledge prove
npm install @affix-io/sdk

# millisecond HMAC prove
npm install @affix-io/sdk-light

@affix-io/sdk pulls @aztec/bb.js and @noir-lang/noir_js for proving, plus qrcode and bwip-js for carriers. @affix-io/sdk-light only needs the QR and barcode libraries, which keeps the install small enough for edge devices.

Quick start #

Prove locally, then let AffixIO verify and attest in the same call. proveAndVerify is the shortest complete path.

prove.ts
import { AffixSDK } from "@affix-io/sdk";

const sdk = new AffixSDK({ apiKey: process.env.AFFIX_API_KEY });

// Local UltraHonk prove, then Affix verify + ML-DSA-65 attestation.
const { prove, verify } = await sdk.proveAndVerify({
  mode: "online",
  circuitId: "simple_yesno",
  credential: { claim_value: "approved" },
  context: {
    secret: "0x1",
    context_id: "0x1",
    required_claim_hash: "approved",
  },
});

console.log(verify.decision);              // "yes" | "no"
console.log(verify.attestation.algorithm); // "ML-DSA-65"
console.log(verify.merkle_root);
prove-light.ts
import { AffixLightSDK } from "@affix-io/sdk-light";

const sdk = new AffixLightSDK({ apiKey: process.env.AFFIX_API_KEY });

// Milliseconds. The queue attests in the background.
const proved = await sdk.prove({
  mode: "offline",
  circuitId: "simple_yesno",
  credential: { claim_value: "approved" },
  context: {
    secret: "0x1",
    context_id: "0x1",
    required_claim_hash: "approved",
  },
});

const local = await sdk.verifyLocal("simple_yesno", proved.proof);
console.log(local.valid, local.decision);
shell
curl -X POST https://api.affix-io.com/v1/circuits/simple_yesno/verify \
  -H "Authorization: Bearer $AFFIX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: gate-8271" \
  -d '{"proof": "0x...", "requestAttestation": true}'
One request, one proof

Each prove call mints a fresh request_id and proof_id, with unique credential and context identifiers so nullifiers differ per request. Never reuse a proof across two admissions.

Authentication #

The SDKs read AFFIX_API_KEY from the environment, or take apiKey in the constructor. Raw HTTP callers send either header.

http
Authorization: Bearer aio_live_xxxxxxxxxxxxxxxx
X-API-Key: aio_live_xxxxxxxxxxxxxxxx

Key prefixes include aio_, demo_ and affix_. Request a key at request access or manage them in Hub. API keys are server credentials. Never ship one to a browser; put a same-origin proxy in front and let the proxy attach the key.

These reads need no key: GET /api/health, GET /api/governance-stats, GET /v1/merkle/root, GET /v1/openapi.json and GET /.well-known/affix-mldsa65.json.

Configuration #

Every option below is a field on AffixSdkConfig. Only apiKey is required. The same shape applies to AffixLightSDK.

OptionDefaultPurpose
apiKeyrequiredRead from AFFIX_API_KEY when omitted.
apiBasehttps://api.affix-io.comOverride for staging. Env: AFFIX_API_BASE.
requestAttestationtrueRequire ML-DSA-65 on verify and flush.
allowOfflineProvetrueProve locally when AffixIO is unreachable.
queueUnsyncedProofstrueAuto-enqueue offline proofs for later sync.
autoFlushtrueDrain the queue on a timer.
autoFlushIntervalMs5000Flush cadence for the full SDK.
flushConcurrency4Parallel verify workers during flush.
flushChunkSize25Proofs per aggregate verify chunk, server max 25.
maxMerkleLeaves50000Client Merkle batch ceiling.
merkleAuditBatchSize1000Digests per audit batch call, server max.
brickWithoutLicencetrueFail closed if the licence cannot be confirmed.
licenceMinIntervalMs~36 hoursMinimum licence recheck interval.
licenceMaxIntervalMs~60 hoursMaximum licence recheck interval.
presentmentBaseunsetYour URL for QR links. Env: AFFIX_PRESENTMENT_BASE.
storageJSON in .affix/Pluggable document store, see below.
timeoutMsclient defaultHTTP timeout for API calls.
sectorunsetNullifier sector for per-context single use.

Prove #

Proof generation is always local. The mode only controls licence behaviour and queuing.

sdk.prove(input: ProveInput): Promise<ProveResult>
ModeBehaviour
autoLive licence when reachable, otherwise offline local prove if allowed.
offlineAlways local, enqueue for later, no AffixIO round trip.
onlineLive licence required. Proving still happens locally.

ProveInput

FieldTypeNotes
circuitIdstringBundled circuits include simple_yesno and yesno.
credentialAffixCredentialClaim hashes and validity window. No raw personal data.
contextWitnessContextsecret, context_id, required_claim_hash.
witnessWitnessPackagePass a pre-built witness instead of credential and context.
fieldsobjectSimple string, number or boolean field map.
modeauto | offline | onlineDefaults to auto.
queueForSyncbooleanEnqueue for later verify and Merkle audit.
request_idstringOne host request maps to one proof. Generated if omitted.
originstringmanual, data_check or your own label.
metaobjectString tags carried through to the queue.

Building a witness directly

witness.ts
const witness = sdk.buildWitness("simple_yesno", credential, {
  secret: "0x1",
  context_id: "0x1",
  required_claim_hash: "approved",
});

const proved = await sdk.prove({ witness, mode: "offline" });

Verify #

Verification is a network call. A successful verify returns the decision, the Merkle leaf and the ML-DSA-65 attestation.

sdk.verify(circuitId: string, proof: string, requestAttestation?: boolean, opts?: { queueOnFailure?: boolean; proofId?: string; proofDigest?: string; }): Promise<VerifyResult>
verify.ts
const result = await sdk.verify("simple_yesno", proved.proof, true, {
  queueOnFailure: true,
});

if (result.verified && result.decision === "yes") {
  admit(result.proof_digest);
}

With queueOnFailure a network failure re-queues the proof instead of throwing away the decision, which is what you want at a gate that must keep moving.

Local verify without a round trip

The full SDK exports localVerify and unpackProof for inspecting a proof offline. SDK-Light exposes verifyLocal on the instance, which recomputes the HMAC in milliseconds.

Offline queue and auto-flush #

Anything proved without a live round trip lands in the queue. The SDK drains it on a timer, roughly every 5 seconds while work is pending. Manual and automatic flushes share one lock, so batches never overlap.

Each flush runs four steps: build a client Merkle batch over pending digests up to 50,000 leaves, run circuit verify for each proof across parallel workers, require ML-DSA-65 attestation before marking a proof synced, then anchor the Merkle leaf on the same verify.

flush.ts
const flush = await sdk.flushOfflineQueue({ maxItems: 5000 });

console.log(flush.attested, flush.synced, flush.failed);
console.log(flush.merkle_api_root, flush.merkle_leaves_per_sec);

// Timer control for long-lived hosts
const status = await sdk.autoFlushStatus();
// { active, interval_ms, pending, last_at, last_error, last_result }

sdk.startAutoFlush(2000);
sdk.stopAutoFlush();
sdk.dispose(); // on shutdown
Production admits

digestsOnly: true skips zero-knowledge verify and only attests and Merkle-anchors the digests. It is useful for backfilling an audit trail, but it is not a production admit path because AffixIO never checks the proof itself.

To register digests as leaves without any circuit verify, call anchorPendingLeaves(). It returns counts for attempted, anchored and attested leaves plus the resulting root and throughput.

Inspecting the queue

queue.ts
await sdk.listStoredProofs();  // StoredProof[]
await sdk.listPendingSync();  // QueuedSyncJob[]
await sdk.isOnline();

// Push a proof produced elsewhere into the queue
await sdk.queueProofForAffix({
  proof_id: "afx_gate_8271",
  circuit_id: "simple_yesno",
  proof: carrierProof,
  proof_digest: digest,
  decision: "yes",
});

Pluggable storage #

State defaults to JSON files under .affix/ for the full SDK and .affix-light/ for SDK-Light: proofs.json, offline-queue.json, licence.json and code-uses.json. Any backend with async get and set can replace it, which matters as soon as you run more than one process.

storage.ts
import {
  AffixSDK,
  createMemoryDocumentStore,
  type AffixDocumentStore,
} from "@affix-io/sdk";

// Tests and single-process runs
const sdk = new AffixSDK({
  apiKey: process.env.AFFIX_API_KEY,
  documentStore: createMemoryDocumentStore(),
  autoFlush: false,
});

// Shared backend across a fleet
const redisStore: AffixDocumentStore = {
  async get(key) {
    return (await redis.get(`affix:${key}`)) ?? null;
  },
  async set(key, value) {
    await redis.set(`affix:${key}`, value);
  },
};

const prod = new AffixSDK({
  apiKey: process.env.AFFIX_API_KEY,
  storage: { documentStore: redisStore },
});

Keys written to a shared store are proofs, offline-queue, code-uses and licence, exported as STORAGE_KEYS. Per-domain overrides let you send the queue somewhere different from proof storage.

Merkle batches #

The client tree is AffixIO-compatible sha256-sorted-pairs with a hard ceiling of 50,000 leaves, exported as MAX_MERKLE_LEAVES. Build the batch from digests and leave the proof bytes out when the batch is large.

merkle.ts
import { verifyMerkleProof, MAX_MERKLE_LEAVES } from "@affix-io/sdk";

const batch = await sdk.buildMerkleBatch({ includeProofBytes: false });
console.log(batch.root, batch.leaf_count); // leaf_count <= 50000

// Independent inclusion check against a published root
const ok = verifyMerkleProof(leafHash, steps, batch.root);

Other exported helpers: buildMerkleTree, buildProofMerkleBatch, verifyBatchInclusions, merkleAuditItemsFromBatch and normalizeMerkleAuditLeaf. The public root is readable without a key at GET /v1/merkle/root, so a third party can audit inclusion without an account.

Data check #

AffixIO never queries your databases. Your host looks up the record, gets back exact field strings, and proves from those strings. The decision is yes only when the claim string equals the required string, byte for byte.

SQL

DialectHelper
PostgreSQLcreatePgExecutor, SqlStore.fromPg
MySQL, MariaDBcreateMysqlExecutor, SqlStore.fromMysql
SQLiteSqlStore.fromBetterSqlite, fromNodeSqlite
SQL ServerSqlStore.fromMssql
Oracle, DB2, ODBCcreateOdbcExecutor, fromOdbc
data-check.ts
import {
  createPgExecutor,
  createPrimaryReplicaExecutor,
  SqlStore,
} from "@affix-io/sdk";

const executor = createPrimaryReplicaExecutor({
  primary: createPgExecutor(primaryPool, { timeoutMs: 10_000, readOnly: true }),
  replica: createPgExecutor(replicaPool, { timeoutMs: 10_000, readOnly: true, role: "replica" }),
  preferReplica: true,
  failoverToPrimary: true,
});

const store = SqlStore.fromExecutor(executor, {
  table: "records",
  dialect: "postgres",
  lookupSql: "SELECT id, status, site FROM records WHERE id = :id LIMIT 1",
  readOnly: true,
  timeoutMs: 10_000,
});

const check = await sdk.check(store, {
  id: "REC-1001",
  claimField: "status",
  required: "active",
  select: ["site"],
});

const proved = await sdk.proveFromCheck({ check, mode: "offline" });
console.log(proved.field_aligned); // decision matches the store exactly

File and document stores

Adapters ship for JSON, Mongo exports, key-value dumps, Redis exports, CSV, pipe and other delimited text, fixed-width records, dBase, XML, LDIF and INI: JsonDocumentStore, MongoDocumentStore, KeyValueStore, RedisExportStore, CsvTableStore, PipeDelimitedStore, DelimitedTableStore, FixedWidthStore, DbaseStore, XmlDocumentStore, LdifStore and IniSectionStore. Open any of them with openDataStore({ kind, path }).

checkAndProve collapses lookup and prove into one call. proveFromCheckAndVerify adds the AffixIO round trip.

QR and barcode carriers #

Codes carry the proof, not the person. The scannable payload is the raw AFX.ZK1. carrier with no personal fields, readable by ordinary scanners. Presentment links only appear when you set presentmentBase to your own host. There is no AffixIO default link target.

codes.ts
const sdk = new AffixSDK({
  apiKey: process.env.AFFIX_API_KEY,
  presentmentBase: "https://scan.example.com",
});

const qr = await sdk.generateCodeFromProve({
  kind: "qr",
  maxUses: "unlimited",
  format: "both",
  mode: "auto",
  save: { path: "./codes", sidecar: true },
  credential: { claim_value: "approved" },
  context: { secret: "0x1", context_id: "0x1", required_claim_hash: "approved" },
});

// qr.carrier -> AFX.ZK1...   qr.link -> only when presentmentBase is set

const scan = await sdk.readCode({
  scanned: qr.content,
  sidecarPath: qr.files.sidecar,
  consume: true,
  mode: "auto",
});
OptionMeaning
maxUses: "unlimited" or 0Reusable with no cap.
maxUses: 1 to 255Cap admissions per gate.
format: "both"Render SVG and the raw carrier payload.
mode: "auto"Attest at AffixIO if reachable, otherwise admit locally and queue.
save.sidecarWrites the full proof beside the code. The carrier itself stays compact.

Lower level helpers are exported too: packCarrier, unpackCarrier, isZkCarrier, renderQrSvg, renderBarcodeSvg and buildPresentmentLink. generateCodeFromCheck goes from a store lookup straight to a printable code.

Licence #

The SDK heartbeats your API key on a randomised schedule between roughly 36 and 60 hours. If the licence cannot be confirmed and brickWithoutLicence is left at its default, the SDK fails closed rather than quietly continuing.

licence.ts
import { LicenceError } from "@affix-io/sdk";

try {
  await sdk.checkLicence(true);
} catch (err) {
  if (err instanceof LicenceError) alertOps(err.message);
}

sdk.licenceState();
// { ok, last_ok_at, last_check_at, next_check_at, key_hint }

CLI #

Both packages ship a binary, which is the fastest way to sanity check a deployment before writing any code.

shell
npx affix-sdk health
npx affix-sdk licence
npx affix-sdk prove --claim approved --offline
npx affix-sdk qr --claim approved --scans unlimited --out ./codes
npx affix-sdk barcode --claim approved --scans 5 --out ./codes
npx affix-sdk read <payload> --sidecar ./codes --consume
npx affix-sdk queue
npx affix-sdk flush --max 100
npx affix-sdk verify <proof-hex>

# SDK-Light
npx affix-sdk-light prove --offline --api-key "$AFFIX_API_KEY"
npx affix-sdk-light flush --api-key "$AFFIX_API_KEY"

SDK-Light overview #

@affix-io/sdk-light is the edge package for ordinary devices: phones, kiosks, laptops and host apps. It does not load UltraHonk, Noir or bb.js. Affix Light prove is HMAC-SHA256 over the same yes/no witness layout, so the surrounding code is identical.

@affix-io/sdk@affix-io/sdk-light
Local proveUltraHonk, secondsAffix Light HMAC, milliseconds
DependenciesNoir and bb.jsNode crypto, plus QR libraries
AffixIO syncCircuit verify and attestDigest attest and Merkle by default
State directory.affix/.affix-light/
Use whenStrongest local zero-knowledgeInstant UX on any device

The proof scheme is affix-light-v1 with algorithm HMAC-SHA256, both exported as LIGHT_SCHEME and LIGHT_ALG. The signing key defaults to a value derived from your API key via deriveLightSecretFromApiKey, or set lightProveSecret for a fleet-wide key.

Light prove and verify #

light.ts
import { AffixLightSDK } from "@affix-io/sdk-light";

const sdk = new AffixLightSDK({
  apiKey: process.env.AFFIX_API_KEY,
  lightProveSecret: process.env.AFFIX_LIGHT_SECRET, // optional fleet key
});

const proved = await sdk.prove({
  mode: "offline",
  circuitId: "simple_yesno",
  credential: { claim_value: "approved" },
  context: { secret: "0x1", context_id: "0x1", required_claim_hash: "approved" },
});

// Local HMAC check, milliseconds, no network
const local = await sdk.verifyLocal("simple_yesno", proved.proof);

// Local check, then ML-DSA-65 attest + Merkle anchor at AffixIO
const synced = await sdk.verify("simple_yesno", proved.proof);

verify on SDK-Light does not call UltraHonk circuit verify, because light proofs are not SNARKs. It checks the HMAC locally, then asks AffixIO to attest and anchor the digest.

Naming

AffixSDK is exported from @affix-io/sdk-light as a deprecated alias of AffixLightSDK for drop-in familiarity. Prefer the explicit name so the assurance level is obvious at the call site.

Adaptive flush #

Every light proof is ML-DSA-signed individually at AffixIO. The scheduler adapts to your ingest rate so a quiet gate stays responsive and a busy one stays efficient.

Ingest rateModeSchedule
Up to 20 proofs per secondindividualFlush every second, up to 50 per tick, plus flush on prove.
Above 20 proofs per secondbatchFlush every second, up to 5000 per tick.
light-flush.ts
const status = await sdk.autoFlushStatus();
// adds flush_mode: "individual" | "batch" and ingest_per_sec

import { DEFAULT_FLUSH_POLICY, resolveFlushPolicy } from "@affix-io/sdk-light";
const policy = resolveFlushPolicy(status.ingest_per_sec);

REST endpoints #

The SDKs wrap these. Call them directly from any language. The full contract lives at /v1/openapi.json.

MethodPathPurpose
GET/api/healthService status and circuit catalogue. No key.
GET/v1/circuitsCircuit catalogue.
POST/v1/circuits/{id}/proveRemote prove for non-Node clients.
POST/v1/circuits/{id}/verifyVerify a proof and spend the digest.
POST/v1/witness/prepareBuild a witness server side.
POST/v1/aggregate/verifyVerify up to 25 proofs in one call.
POST/v1/gate/verifyAdmit or refuse, with optional consume.
POST/api/attestSign a payload with ML-DSA-65.
POST/api/attest/verifyVerify an attestation.
GET/v1/merkle/rootPublic audit root. No key.
POST/v1/merkle/auditAppend one audit leaf.
POST/v1/merkle/audit/batchAppend up to 1000 leaves.
GET/v1/merkle/proof/{digest}Inclusion proof for a digest.
GET/v1/spent/{digest}Check whether a digest was spent.
GET/v1/verify/receiptsDecision receipts for audit.
GET/api/export/siemNDJSON evidence export.
GET/api/webhooksList webhook endpoints.
POST/api/webhooksRegister an endpoint.

ML-DSA-65 attestation #

Attestations are signed with ML-DSA-65, the FIPS 204 post-quantum signature scheme. The SDKs attach one to every synced proof when requestAttestation is on, which is the default.

type Attestation = { signed_at: string; payload_digest: string; mldsa_signature_b64: string; algorithm: "ML-DSA-65"; }

The verification key is published at GET /.well-known/affix-mldsa65.json. Verify through POST /api/attest/verify, or offline with any FIPS 204 capable library. Most builds of common crypto libraries do not expose ML-DSA-65 yet, so the API call is usually the practical route.

Webhooks #

Register an HTTPS receiver and AffixIO pushes signed events rather than making you poll. Manage endpoints in Hub. The signing secret is shown once at creation.

Each delivery carries X-Affix-Event, X-Affix-Timestamp, X-Affix-Delivery-Id and X-Affix-Signature. The signature is hmac-sha256=<hex> over {unix_timestamp}.{raw_body}.

webhook.ts
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyDelivery(secret, rawBody, timestamp, header) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const got = String(header).replace("hmac-sha256=", "");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(got));
}

Reject anything outside a 300 second window to block replays. Failed deliveries retry up to three times. Rotate a leaked secret with POST /api/webhooks/{id}/rotate-secret.

SIEM and evidence export #

Pull decision receipts into a SIEM or GRC pipeline as newline-delimited JSON, filtered by time, gate or circuit.

shell
curl "https://api.affix-io.com/api/export/siem?since=2026-01-01T00:00:00Z" \
  -H "Authorization: Bearer $AFFIX_API_KEY" \
  -o affix-receipts.ndjson

The export is backed by GET /v1/verify/receipts. Combined with the public Merkle root, it gives an auditor a decision trail they can verify without holding any personal data.

Types #

Both packages export the same core types. These are the shapes you will handle most often.

types.d.ts
type ProveResult = {
  proof_id: string;
  circuit_id: string;
  proof: string;
  valid: boolean;
  decision: "yes" | "no";
  proof_digest: string;
  source: "local";
  offline: boolean;       // produced without a live round trip
  pending_sync: boolean;  // queued for verify and audit
  request_id?: string;
};

type VerifyResult = {
  proof_id: string;
  valid: boolean;
  verified: boolean;
  decision: "yes" | "no";
  circuit_id: string;
  proof_digest: string;
  merkle_root?: string;
  merkle_leaf_hash?: string;
  attestation?: Attestation;
};

type AffixCredential = {
  schema_id: string;
  issuer_id: string;
  issuer_pubkey_hash: string;
  credential_id: string;
  claim_value: string | number;
  valid_from: number;
  valid_until: number;
};

type WitnessContext = {
  secret: string;
  context_id: string;
  required_claim_hash?: string;
  logic_mode?: string | number;
};

type SyncFlushResult = {
  online: boolean;
  attempted: number;
  synced: number;
  failed: number;
  attested?: number;
  merkle_api_root?: string;
  merkle_leaves_per_sec?: number;
  results: Array<{ proof_id: string; ok: boolean; verify?: VerifyResult }>;
};

Errors and limits #

The default rate limit is 10 requests per second per key. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and, on a 429, Retry-After.

Send an Idempotency-Key on prove and verify. A replay within 24 hours returns the original 2xx body with Idempotency-Replayed: true, so a retry after a timeout cannot double-spend a digest.

StatusMeaningHandling
401, 403Missing or rejected keyCheck the header and key prefix.
409Digest already spentTreat as a replay, do not admit twice.
429Rate limitedBack off using Retry-After.
5xxUpstream failureVerify with queueOnFailure so the proof survives.

LicenceError is thrown when the licence heartbeat fails and brickWithoutLicence is on. Catch it at startup and alert, rather than letting a gate fail closed without warning.

FAQ #

What is the difference between @affix-io/sdk and @affix-io/sdk-light?

The full SDK proves with UltraHonk over Noir circuits via Barretenberg, taking seconds and producing genuine zero-knowledge proofs. SDK-Light proves with HMAC-SHA256 in milliseconds using Node crypto only. Light proofs are not SNARKs. Both sync to AffixIO for ML-DSA-65 attestation and Merkle audit.

Does AffixIO ever generate proofs on the server?

No. Both Node packages prove on your host. AffixIO verifies, signs the payload with ML-DSA-65 and anchors the digest. There is no server-side prove.

How does the offline queue and auto-flush work?

Proofs made without a live round trip are queued. The full SDK flushes about every 5 seconds: a client Merkle batch of up to 50,000 digests, circuit verify per proof across parallel workers, ML-DSA-65 attestation before a proof counts as synced, then the Merkle leaf anchor. SDK-Light flushes every second, individually up to 20 proofs per second and in batches of up to 5000 above that.

Can I replace the default JSON file storage?

Yes. Pass any object with async get and set as documentStore to use Redis, SQL, memory or a custom backend. The keys are proofs, offline-queue, code-uses and licence.

Is a proof carried in a QR code personally identifying?

No. Codes encode the raw AFX.ZK1 carrier with no personal fields. Presentment links only use a base URL you configure through presentmentBase.

How do I authenticate against api.affix-io.com?

Send Authorization: Bearer <key> or X-API-Key: <key>. The SDKs read AFFIX_API_KEY from the environment. Request a key at request access or manage keys in Hub.