> ## Documentation Index
> Fetch the complete documentation index at: https://facturas-sdk.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Persistence Stores for Idempotency and WSAA Sessions

> Connect facturas to Postgres, Redis, file storage, or memory to enable safe retries with idempotency keys and share WSAA tickets across processes.

A single `store` serves two purposes in the `facturas` SDK: it records immutable voucher reservations so retries never produce duplicate invoices, and it caches WSAA login tickets so multiple workers share the same authenticated session. The SDK ships adapters for Postgres, Redis, file storage, and in-memory — or you can implement the `ArcaStore` interface directly against any backing store you already use.

<Tip>
  Use a Postgres or Redis store in any production or staging environment. The in-memory store is convenient for tests and local examples but does not survive a restart and cannot be shared across processes.
</Tip>

## How stores are used

<CardGroup cols={2}>
  <Card title="Voucher idempotency" icon="receipt">
    Before authorizing a voucher, the SDK writes an immutable reservation keyed by your `idempotencyKey`. On a retry with the same key, the SDK looks up the reservation, confirms the input matches, and queries ARCA for the outcome — without issuing a second CAE request.
  </Card>

  <Card title="WSAA session cache" icon="key">
    A configured `store` caches WSAA login tickets so that a warm process reuses a valid ticket obtained by another worker. Providing an explicit `wsaaSessionStore` targets ticket caching only and takes priority over the unified store.
  </Card>
</CardGroup>

## Postgres

`createPostgresStore` wraps any client that exposes a parameterized query function. Neon, Supabase Postgres, Vercel Postgres, `pg`, and `postgres` all work — results can be a row array or a `{ rows }` object.

Provision the table once before first use:

```sql theme={null}
CREATE TABLE arca_store (
  key TEXT PRIMARY KEY,
  value TEXT NOT NULL,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

Then pass your client's query function to the adapter:

<CodeGroup>
  ```typescript @vercel/postgres theme={null}
  import { createArcaClient, createPostgresStore } from "facturas";
  import { sql } from "@vercel/postgres";

  const arca = createArcaClient({
    store: createPostgresStore({
      query: (text, params) => sql.query(text, params),
    }),
  });
  ```

  ```typescript pg (node-postgres) theme={null}
  import { createArcaClient, createPostgresStore } from "facturas";
  import { Pool } from "pg";

  const pool = new Pool({ connectionString: process.env.DATABASE_URL });

  const arca = createArcaClient({
    store: createPostgresStore({
      query: (text, params) => pool.query(text, params),
    }),
  });
  ```

  ```typescript postgres.js theme={null}
  import { createArcaClient, createPostgresStore } from "facturas";
  import postgres from "postgres";

  const sql = postgres(process.env.DATABASE_URL!);

  const arca = createArcaClient({
    store: createPostgresStore({
      // postgres.js uses tagged template literals; use sql.unsafe for parameterized calls.
      query: (text, params) => sql.unsafe(text, params as string[]),
    }),
  });
  ```
</CodeGroup>

You can also pass a custom `table` option if you need to use a different table name. The value must be a simple SQL identifier (`[a-zA-Z_][a-zA-Z0-9_]*`):

```typescript theme={null}
createPostgresStore({
  query: (text, params) => sql.query(text, params),
  table: "my_arca_store",
});
```

Atomic voucher reservation uses `INSERT ... ON CONFLICT DO NOTHING RETURNING key`. The adapter never creates the table or acquires a database-level lock — provisioning is your responsibility.

## Redis

`createRedisStore` accepts an ioredis or Upstash client. The SDK auto-detects the flavor by checking for a `call` method (ioredis) or falling back to Upstash. Pass `{ flavor: "upstash" }` to override detection explicitly.

<Warning>
  Do not enable key eviction (`maxmemory-policy`) on the Redis instance that backs your store. Evicted voucher reservation keys can cause a retry to issue a duplicate invoice. Use a dedicated Redis database or instance without eviction for the store, separate from any cache instance you use for other purposes.
</Warning>

<CodeGroup>
  ```typescript ioredis theme={null}
  import { createArcaClient, createRedisStore } from "facturas";
  import Redis from "ioredis";

  const redis = new Redis(process.env.REDIS_URL);

  const arca = createArcaClient({
    store: createRedisStore(redis),
  });
  ```

  ```typescript Upstash theme={null}
  import { createArcaClient, createRedisStore } from "facturas";
  import { Redis } from "@upstash/redis";

  const redis = new Redis({
    url: process.env.UPSTASH_REDIS_REST_URL!,
    token: process.env.UPSTASH_REDIS_REST_TOKEN!,
  });

  const arca = createArcaClient({
    store: createRedisStore(redis, { flavor: "upstash" }),
  });
  ```
</CodeGroup>

The adapter uses `SET key value NX` (ioredis) or `set(key, value, { nx: true })` (Upstash) for atomic reservation writes. No TTL is applied.

## File store

`createFileStore` persists records to a local directory. Keys are hashed to filenames; writes use an exclusive-create + atomic-rename strategy to avoid partial writes. Files are created with mode `0600` and new directories with `0700`.

```typescript theme={null}
import { createArcaClient, createFileStore } from "facturas";

const arca = createArcaClient({
  store: createFileStore("/var/data/arca-store"),
});
```

Use the file store on single-server deployments with a durable, private volume. It is not suitable for multi-worker or serverless deployments where workers run on different hosts and cannot share a filesystem. The file store does not provide cross-process locking.

## Memory store

`createMemoryStore` keeps records in a plain JavaScript `Map` for the lifetime of the process. It serializes WSAA ticket refreshes within the shared object, but records do not survive a restart.

```typescript theme={null}
import { createArcaClient, createMemoryStore } from "facturas";

const arca = createArcaClient({
  store: createMemoryStore(),
});
```

Use the memory store in tests and local examples. Do not use it in production or in serverless functions where processes are short-lived.

## Custom store

Implement the `ArcaStore` interface to connect any backing store:

```typescript theme={null}
import type { ArcaStore } from "facturas";

const store: ArcaStore = {
  async get(key: string): Promise<string | null> {
    // Return the stored value, or null if the key does not exist.
  },
  async set(key: string, value: string): Promise<void> {
    // Overwrite any existing value for the key.
  },
  async add(key: string, value: string): Promise<boolean> {
    // Write the key only if it does not already exist.
    // Return true if written, false if the key was already present.
    // This operation MUST be atomic.
  },
  async delete(key: string): Promise<void> {
    // Optional: remove the key from the store.
  },
  async withLock<T>(key: string, fn: () => Promise<T>): Promise<T> {
    // Optional but recommended: serialize concurrent WSAA ticket refreshes.
    return await fn();
  },
};
```

The `add` method must be atomically exclusive — it must return `false` without modifying the stored value if the key already exists. This is the operation that makes voucher idempotency safe under concurrent retries.

## Store key structure and record lifetime

Store keys follow these patterns:

* **WSAA tickets:** `arca:v1:wsaa:{environment}:{service}:{fingerprint}`
* **Voucher reservations:** `arca:v1:attempt:{environment}:{taxId}:{idempotencyKey}`

Reservation records contain the input hash, operation, reserved voucher coordinates, and the exact input sent to ARCA. They contain fiscal and customer data — restrict access to the store and protect backups accordingly.

<Warning>
  **Never delete, expire, or overwrite voucher reservation records.** The SDK creates reservations and never overwrites them with results. Deleting a reservation can cause a subsequent retry to issue a duplicate invoice. Records must be treated as append-only for the lifetime of your application.
</Warning>

Reservation records carry a format version. `v: 1` records are plain WSFE reservations readable by any SDK version from 0.9 onward. `v: 2` records are WSMTXCA or itemized reservations — they always identify their provider, and SDK version 0.10 cannot replay them, which prevents a rollback from re-sending a WSMTXCA voucher through WSFE by accident. Preserve both versions.

Repeating an idempotency key with a different input, operation, provider, or explicit voucher number is an idempotency mismatch and throws `ARCA_INPUT_IDEMPOTENCY_MISMATCH`.

## Using one store for both purposes

You can pass the same store instance to `store` (for voucher idempotency) and rely on it automatically for WSAA ticket caching. The SDK namespaces all keys, so a single Postgres table or Redis database handles both safely:

```typescript theme={null}
import { createArcaClient, createPostgresStore } from "facturas";
import { sql } from "@vercel/postgres";

const store = createPostgresStore({
  query: (text, params) => sql.query(text, params),
});

// One store covers both idempotency records and WSAA ticket caching.
const arca = createArcaClient({ store });
```

If you need independent expiry policies or access controls, pass a separate `wsaaSessionStore` to override ticket caching while keeping the unified store for voucher idempotency.
