Skip to main content
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.
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.

What should self-hosted users do?

Cloud users do not need to follow these instructions as the migration will be performed by the Lago Team.
If you’re using a version below v1.44.0, please first follow the migration steps for v1.44.0. Only after completing those should you proceed to v1.45.0.

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):
bundle exec rails migrations:wallet_traceability
You can scope the validation to a specific organization or limit the number of customers processed:
# 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:
VariableDefaultDescription
DRY_RUNtrueSet 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_SIZE1000Number of records fetched per database batch. Automatically capped to LIMIT when both are set.
ERROR_DISPLAY_LIMIT50Maximum number of problematic wallets (dry-run) or errors (backfill) to display in the console output.
THREAD_COUNT0Number 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_TERMINATEDfalseSet 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:
ERROR_LOG_FILE=/tmp/problematic_wallets.csv bundle exec rails migrations:wallet_traceability
If all wallets pass validation, you can skip directly to Step 4.
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.

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 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).

4. Run the backfill

Once all wallets pass validation (or have been handled), run the same task with DRY_RUN=false:
DRY_RUN=false bundle exec rails migrations:wallet_traceability
As with the dry-run, you can scope the backfill:
# 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.
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.
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.
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:
# 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:
# 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
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.

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.
IssueValidation messageCauseResolution
Decimal amount_centsDecimal 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 roundingHistorical 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.
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).

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).
IssueValidation message
Negative wallet balanceNegative wallet balance: -500 cents
Negative transaction amountNegative amount_cents on inbound TX_ID: -100
Insufficient inboundOutbound TX_ID: insufficient inbound to consume 5000 cents (available: 1000 cents, shortfall: 4000 cents)
Missing transaction historyOutbound 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.
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.
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.

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