> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getlago.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Migration to v1.45.0

Dear Lago Community,

We're writing to inform you about important changes in Lago v1.45.0 that introduce **wallet traceability** — a feature that links outbound wallet transactions to their source inbound transactions, providing a full audit trail of credit consumption.

# What are the changes?

## Wallet Traceability

This version introduces **wallet traceability** — a system that tracks exactly which inbound (top-up) transactions fund each outbound (consumption) transaction. This enables:

* **Credit source visibility**: Distinguishing when free (granted) vs. purchased credits are being consumed.
* **Full audit trail**: Every outbound transaction records exactly which inbound transactions funded it and in what amounts.
* **Invoice breakdown**: Invoices now show a breakdown of granted vs. purchased credit usage, enabling more accurate accounting and reporting.
* **New API endpoints**: Query which inbound transactions funded a given outbound transaction, and vice versa.

Wallets created after upgrading to v1.45.0 are automatically traceable. However, **existing wallets require a one-time backfill** to compute the consumption history from their past transactions. This backfill is performed via a rake task described below.

<Warning>
  Non-traceable wallets will continue to function for a few releases, but **a future version will require all active wallets to be traceable**. This migration is mandatory — we strongly recommend completing it as soon as possible.
</Warning>

# What should self-hosted users do?

<Note>
  Cloud users do not need to follow these instructions as the migration will be performed by the Lago Team.
</Note>

<Warning>
  If you're using a version below `v1.44.0`, please first follow the migration steps for [v1.44.0](/guide/migration/migration-to-v1.44.0).
  Only after completing those should you proceed to v1.45.0.
</Warning>

## Migration Steps

### 1. Install Lago v1.45.0

Install the new version. Schema migrations (new tables and columns) will run automatically on startup.

### 2. Validate wallets (dry-run)

Open a shell on your API server and run the migration task in dry-run mode (the default):

```bash theme={"dark"}
bundle exec rails migrations:wallet_traceability
```

You can scope the validation to a specific organization or limit the number of customers processed:

```bash theme={"dark"}
# Validate wallets for a specific organization
ORGANIZATION_ID=ORG_ID bundle exec rails migrations:wallet_traceability

# Validate wallets for a limited number of customers
LIMIT=100 bundle exec rails migrations:wallet_traceability

# Combine options
ORGANIZATION_ID=ORG_ID LIMIT=100 bundle exec rails migrations:wallet_traceability
```

The available environment variables are:

| Variable              | Default            | Description                                                                                                                                                                                                                                                                                                                                                                         |
| --------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DRY_RUN`             | `true`             | Set to `false` to run the backfill. Any other value (or unset) runs in dry-run mode.                                                                                                                                                                                                                                                                                                |
| `ORGANIZATION_ID`     | *(all)*            | Scope to a single organization by its ID.                                                                                                                                                                                                                                                                                                                                           |
| `LIMIT`               | *(all)*            | Maximum number of **customers** to process. The task iterates over distinct customers — all wallets belonging to a customer are always processed atomically as a group. When set, the task prints a **next cursor** value (see `CURSOR`).                                                                                                                                           |
| `CURSOR`              | *(first customer)* | Start processing from a specific customer ID (inclusive). Must be a valid UUID. When omitted, the task defaults to the first customer ID in scope. When used without `LIMIT`, all remaining customers from that cursor onward are processed. When combined with `LIMIT`, only the next N customers are processed and the task prints a **next cursor** value for the following run. |
| `BATCH_SIZE`          | `1000`             | Number of records fetched per database batch. Automatically capped to `LIMIT` when both are set.                                                                                                                                                                                                                                                                                    |
| `ERROR_DISPLAY_LIMIT` | `50`               | Maximum number of problematic wallets (dry-run) or errors (backfill) to display in the console output.                                                                                                                                                                                                                                                                              |
| `THREAD_COUNT`        | `0`                | Number of threads for parallel processing. `0` runs sequentially. Ensure your database connection pool size is at least `THREAD_COUNT + 1`. Due to Ruby's Global VM Lock (GVL), multi-threading primarily helps when the bottleneck is database I/O wait time. If the migration is CPU-bound, additional threads may not significantly improve performance.                         |
| `INCLUDE_TERMINATED`  | `false`            | Set to `true` to include terminated wallets in the migration. By default, only active wallets are processed.                                                                                                                                                                                                                                                                        |
| `ERROR_LOG_FILE`      | *(tmp dir)*        | File path for the CSV error log. A file is created at startup in the system's temporary directory (e.g., `/tmp/wallet_migration_errors_20260402120000.csv`), but only populated with data when errors are found. On a clean run the file will remain empty. Set this variable to override the default path.                                                                         |

The dry-run performs a validation of all your wallets **without modifying any data**. It will output:

* A progress bar showing validation progress.
* The number of wallets that **can be migrated** automatically.
* The number of **problematic** wallets with details about each issue.
* A **migration readiness** percentage.
* A **CSV error log** with all problematic wallets (written automatically to the tmp directory, see `ERROR_LOG_FILE`).

To override the default error log path:

```bash theme={"dark"}
ERROR_LOG_FILE=/tmp/problematic_wallets.csv bundle exec rails migrations:wallet_traceability
```

<Note>
  If all wallets pass validation, you can skip directly to Step 4.
</Note>

<Note>
  It is safe to run the backfill directly (Step 4) without running the dry-run first — the backfill also validates each wallet before migrating it and will roll back any customer with problematic wallets. The main purpose of the dry-run is to separate validation from migration, giving you the opportunity to review and fix problematic wallets upfront rather than discovering them during the backfill.
</Note>

### 3. Fix or handle problematic wallets

Review the output from Step 2. For each problematic wallet, choose one of the following approaches based on the issue type (see the [Troubleshooting](#troubleshooting-wallets-that-cannot-be-migrated) section below for details):

1. **Fix the underlying data** — Correct the transaction records so the wallet passes validation, then re-run Step 2 to confirm.
2. **Terminate and recreate** — If the data is unrecoverable, terminate the wallet and create a new one (see [Terminate and recreate](#terminate-and-recreate-escape-hatch)).

### 4. Run the backfill

Once all wallets pass validation (or have been handled), run the same task with `DRY_RUN=false`:

```bash theme={"dark"}
DRY_RUN=false bundle exec rails migrations:wallet_traceability
```

As with the dry-run, you can scope the backfill:

```bash theme={"dark"}
# Backfill wallets for a specific organization
DRY_RUN=false ORGANIZATION_ID=ORG_ID bundle exec rails migrations:wallet_traceability

# Backfill for the next 100 customers
DRY_RUN=false LIMIT=100 bundle exec rails migrations:wallet_traceability

# Backfill with multithreading (ensure your DB pool size >= THREAD_COUNT + 1)
DRY_RUN=false THREAD_COUNT=4 bundle exec rails migrations:wallet_traceability
```

This task:

* Displays a progress bar showing backfill progress.
* Computes and populates `remaining_amount_cents` for all inbound transactions.
* Creates `wallet_transaction_consumptions` records linking inbound and outbound transactions.
* Sets `traceable = true` on each successfully migrated wallet.

<Warning>
  If a wallet has data inconsistencies (e.g., outbound transactions that cannot be fully consumed by available inbound transactions), the backfill will **roll back** all wallets for that customer and record the error, then **continue processing other customers**. All failures are reported in the summary output. Failed customers' wallets will remain non-traceable and can be retried after fixing the underlying data. Always run the dry-run validation (Step 2) first to identify and fix issues before backfilling.
</Warning>

<Note>
  The backfill is safe to run multiple times. It acquires customer-level advisory locks and processes each customer's wallets atomically, so it can be safely run while the application is serving traffic.
</Note>

Monitor the task's progress bar and verify completion by checking that all active wallets have `traceable = true`.

### Scaling the migration with parallel processes

For most installations, a single process is fast enough — parallelization is only worth considering if you have hundreds of thousands of customers with wallets to migrate. We recommend starting with a smaller subset (e.g., `LIMIT=1000`) to get a feel for how long the migration takes, then extrapolating from there.

There are two ways to parallelize the migration:

**Multi-threading (`THREAD_COUNT`)** runs multiple threads within a single process. Due to Ruby's Global VM Lock (GVL), this primarily helps when the bottleneck is database I/O wait time and may not significantly improve performance for CPU-bound workloads.

**Multiple processes with `CURSOR` + `LIMIT`** is the recommended approach for large datasets. By splitting the customer
space into non-overlapping windows, you can run several independent processes in parallel — each handling a different
slice. This sidesteps the GVL entirely and scales linearly with the number of processes.

To set this up, you simple launch the first process to discover the next cursor, then launch additional processes with the provided cursor to split the workload:

```bash theme={"dark"}
# Process 1: first 50,000 customers
LIMIT=50000 bundle exec rails migrations:wallet_traceability
# Output: Next cursor: <cursor_A>
# The next cursor is displayed before the run starts, so you can immediately launch the next process in parallel.

# Process 2: next 50,000 customers (can run in parallel with Process 1 after it prints the next cursor)
LIMIT=50000 CURSOR=<cursor_A> bundle exec rails migrations:wallet_traceability
# Output: Next cursor: <cursor_B>

# Process 3: next 50,000 customers (can run in parallel)
LIMIT=50000 CURSOR=<cursor_B> bundle exec rails migrations:wallet_traceability
# ...and so on
```

The same goes for the backfill:

```bash theme={"dark"}
# Process 1
DRY_RUN=false LIMIT=50000 bundle exec rails migrations:wallet_traceability
# Output: Next cursor: <cursor_A>

# Process 2
DRY_RUN=false LIMIT=50000 CURSOR=<cursor_A> bundle exec rails migrations:wallet_traceability
# Output: Next cursor: <cursor_B>

# Process 3
DRY_RUN=false LIMIT=50000 CURSOR=<cursor_B> bundle exec rails migrations:wallet_traceability
# ...and so on
```

<Note>
  Re-running the same `CURSOR` + `LIMIT` combination is safe but not recommended, as it will not produce the same result. If you need to re-run, start from the beginning without a `CURSOR` and continue with the cursors printed by each run.
</Note>

# Troubleshooting: Wallets that cannot be migrated

The validation task may flag wallets with data inconsistencies caused by historical bugs. Below are the known issue types and recommended resolutions.

### Reconcilable issues

These issues can be fixed by updating the data directly in the database, then re-running the validation.

| Issue                     | Validation message                                                  | Cause                                                                   | Resolution                                                                                                                                                                                                                                |
| ------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Decimal amount\_cents** | `Decimal amount_cents on inbound TX_ID: 12.9999 (expected integer)` | Historical bug storing values non-rounded decimals instead of integers. | Update the transaction amount in the database to rounded integers.                                                                                                                                                                        |
| **Balance drift (small)** | `Balance drift < 1 unit: 0.5 cents (...) — likely rounding`         | Historical rounding bug.                                                | Update the wallet balance in the database to match the sum of its transactions.                                                                                                                                                           |
| **Balance drift (large)** | `Balance drift >= 1 unit: 500 cents (...)`                          | Historical rounding bug.                                                | This issue might be caused by a significant number of outbound transactions impacted by the historical rounding bug and therefore causing a bigger drift. Update the wallet balance in the database to match the sum of its transactions. |

<Note>
  When fixing wallet balances, you may also need to update related attributes such as `credits_balance` and `credits_ongoing_balance` to keep the wallet consistent. If this level of manual correction is not acceptable, it is preferable to terminate the wallet and recreate it instead (see [Terminate and recreate](#terminate-and-recreate-escape-hatch)).
</Note>

### Non-reconcilable issues

These issues cannot be automatically reconciled due to historical data corruption. The wallet must be terminated and recreated (see [Terminate and recreate](#terminate-and-recreate-escape-hatch)).

| Issue                           | Validation message                                                                                          |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| **Negative wallet balance**     | `Negative wallet balance: -500 cents`                                                                       |
| **Negative transaction amount** | `Negative amount_cents on inbound TX_ID: -100`                                                              |
| **Insufficient inbound**        | `Outbound TX_ID: insufficient inbound to consume 5000 cents (available: 1000 cents, shortfall: 4000 cents)` |
| **Missing transaction history** | `Outbound TX_ID: no inbound transactions available — missing transaction history`                           |

## Terminate and recreate escape hatch

Terminated wallets are **ignored** by the migration task and all traceability operations. It is safe to leave non-traceable wallets in a terminated state — active wallets will still fully benefit from traceability.

If a wallet cannot be reconciled due to data issues, the recommended approach is:

1. **Terminate** the problematic wallet.
2. **Create a new wallet** for the customer.
3. **Top up** the new wallet with the correct balance.
4. The new wallet will automatically be created as **traceable** — all future transactions will have full consumption tracking from the start.

<Note>
  Wallets created after installing v1.45.0 are automatically traceable — they do not need to be backfilled. The migration task only applies to wallets that existed before the upgrade.
</Note>

<Warning>
  If the customer has a **grace period** configured, any draft invoice will automatically recompute prepaid credit amounts upon finalization — no special handling is needed.

  For customers **without a grace period**, any in-flight invoice generated between the termination of the old wallet and the creation of the new one may not correctly reflect prepaid credit usage. We recommend enabling a grace period on affected customers before performing the terminate-and-recreate procedure to avoid this issue.
</Warning>

# Timeline

We recommend performing this migration as soon as possible after the release of v1.45.0. Non-traceable wallets will continue to function for a few releases, but **support for non-traceable wallets will be removed in a future version**. All active wallets will be required to be traceable, making this migration mandatory.

# Get Involved

If you have any questions or encounter issues during the migration, please reach out to us via the Slack community. Our team is here to help you through this transition.

Thanks for your understanding and continued support.

The Lago Team
