Skip to main content

Upgrading

Pre-1.0

There is no upgrade-compatibility promise yet. Pre-1.0, a minor version can include a breaking database migration or a config change without a major version bump. Treat every upgrade like the first one: read the release notes, back up first.

General procedure​

  1. Read the release's entry in the changelog, and the version notes below.
  2. Back up your database. Migrations run forward-only in practice, and some have no down at all, so the way back to an older image is restoring this backup:
    pg_dump -Fc -d "$DATABASE_OWNER_URL" -f wickermoney-$(date +%F).dump
  3. Pull or build the new image tag.
  4. Run migrations against the new image, using the owner credentials (DATABASE_OWNER_URL), before starting the new container:
    docker run --rm --env-file .env wickermoney node dist/db/cli.js up
  5. Start the new container.

With the quickstart's compose file, steps 3–5 are docker compose pull then docker compose up -d: its migrate service runs before the app starts, every time.

A release with no migrations can go back to the previous image as it is. A release whose migrations all have a down can go back by running node dist/db/cli.js down once per migration, with the new image, then starting the old one. Otherwise, the only way back is restoring the backup from step 2. The version notes below say which kind each release is.

The running API never migrates on its own boot — it only ever holds the low-privilege app role's credentials, which deliberately can't create schemas, policies or roles. Migrations are always a separate, explicit step against the owner role.

Version notes​

0.6.1 → 0.7.0​

  • Back up first. Two migrations, 034 and 035, both on the Debt payoff plugin's debts table and both with a down. The sample compose file's migrate service runs them before the app starts.
    • 034 (debt_payoff_plan_controls) adds included boolean not null default true and extra_payment numeric(19,4) not null default 0 (never negative). Every existing debt stays in the plan with no extra amount, so nothing changes until someone uses them.
    • 035 (debt_payoff_timelines) adds six columns: repayment_starts_on, promo_apr and promo_ends_on (all optional; the last two are set together), promo_deferred_interest, promo_interest_base and compounding. Each has a default that keeps today's behaviour, and checks refuse a half-set promotion, deferred interest without one, and unknown options.
  • Going back to 0.6.1: run node dist/db/cli.js down twice with the new image (each run reverts the newest migration, 035 and then 034), then start the 0.6.1 image. The two downs drop the columns above, so any "In plan" switch, extra amount, repayment start date, promotional rate or interest option anyone set is discarded; the debts themselves are kept. Restore the backup instead if you want those settings back.
  • The API now has a per-user rate limit, on by default. Each signed-in user may make 600 requests a minute to /api/v1, 30 CSV import requests and 5 data exports. People using the app will not meet it, but a script that calls the API in a loop (a sync, a poller) may now get 429 rate_limited. Raise API_RATE_LIMIT_MAX, API_RATE_LIMIT_IMPORT_MAX and API_RATE_LIMIT_EXPORT_MAX, or set API_RATE_LIMIT_ENABLED=false to turn the limit off. See API rate limit.
  • CSV imports are limited to run a few at a time. A process commits at most IMPORT_MAX_CONCURRENT imports at once (default: half of DB_POOL_MAX). Another import waits for its turn without holding a database connection, for up to IMPORT_QUEUE_WAIT_SECONDS (default 30), and is then refused with 503 import_busy; nothing is imported and sending the same file again imports it once. One person importing alone is not slowed. See Concurrent imports.
  • Three more plugins run in a sandboxed frame. The Until payday widget (Upcoming), the Forecast page and the Debt payoff page and widget now run in a frame, as Insights and Spending Trends did in 0.6.0. They read through the app with only their own grants. They look and behave as before. Only Budgets and Import CSV still run in the app's own page, fully trusted. A frame costs some time and memory, and a widget's frame now starts only when its card is near the screen (see below). See How much a plugin is trusted.
  • Dashboard widgets in frames start when you scroll to them. A card that is more than 600 px below the window holds a placeholder until you scroll near it; frames in the first screen start at load, as before. Find in page and printing see only the frames that have started, so scroll to the bottom first if you need either. See The dashboard.
  • Actions say what they did in a notification. On Transactions, Recurring, Accounts, Categories and Settings, a result such as "Deleted 'Coffee' (-$4.50)." appears as a notification at the top of the page. It has no Undo and does not close itself; see Notifications.
  • The Transactions recurring column is quieter. A matched row shows the recurring item as a pill with an Unmatch button; the choosing, suggestions and dismissals moved into a list that the row's Link button opens. See Matching.
  • Debt payoff does more. Each debt can be left out of the plan, can have its own monthly extra, can be a loan in deferment, can have a promotional rate (with deferred interest if you want that modelled), and can be compounded daily or charged simple daily interest. A debt that uses none of this gets the plan it got before. See Debt payoff.
  • Fixed: a primary button no longer turns light grey under its light text on hover; on a phone, text buttons in the Transactions recurring column no longer overlap their neighbours; and Erase my data no longer misses a write that was open when the erase started. One narrow case remains: a request that had already passed its session check and writes after the erase can still leave a row, which the next erase removes.
  • For developers: pnpm dev now proxies the plugin frame to the API, which needs a built web app and WEB_DIST_DIR set; see Setting up a dev environment. On Windows, a bundled plugin's dev script no longer fails with EPERM.
  • For API clients: additive, apart from the rate limit and the import refusal above. The Debt payoff routes accept and return new fields, and GET /plan also returns excludedDebts; see Debt payoff. A request over a limit is 429 rate_limited with Retry-After; an import that waited too long is 503 import_busy with Retry-After.
  • For plugin authors: @wickermoney/ui-kit gains an experimental toast (ToastProvider, ToastViewport, useToast) for the core app, an experimental rowDetail prop on Table and a link icon on IconButton. The experimental isolated protocol gains two additive messages, modal and modal-layout. Nothing was removed and no manifest needs editing. See Writing a plugin.

0.6.0 → 0.6.1​

  • Back up first, as for any upgrade. One migration, 033 (tenant_lookup_policies), runs before the app starts; the sample compose file's migrate service does it. It rewrites any row-level security policy that still calls core.current_user_id() bare to (SELECT core.current_user_id()), so the lookup runs once per statement, not once per row. A database migrated in order has one such policy, account_lines_isolation on the Budgets account allowances. The rewritten policy returns the same rows, so no access changes. Where nothing is left to rewrite, the migration does nothing and takes no lock.
  • Going back to 0.6.0: run node dist/db/cli.js down once with the new image, then start the 0.6.0 image. The down changes nothing in your data (there is no behaviour to take back); it only removes the migration's record, so the database's migration history matches the older image's.
  • An owner is asked before a third-party plugin is switched on. Settings → Plugins shows a confirmation naming the plugin's origin and the tables it is granted. Bundled plugins, and switching a plugin off, don't ask. See Turning a plugin on or off.
  • The Transactions table fits narrower windows. From the phone layout up the date doesn't wrap, the Edit and Delete buttons stay in view, and neither the table nor the page scrolls sideways. Every column is still there.
  • The Origin-Agent-Cluster console warning is gone. The API no longer sends that header. Nothing about the plugin sandbox changes.
  • Chrome DevTools no longer lists a "blocks the use of eval" issue for the plugin frame. The zod library probes for eval support when it builds its first schema, the frame's policy refuses it, and zod falls back, so nothing was broken, only noisy. The frame now turns the probe off. The policy is unchanged: 'unsafe-eval' was not added, and the frame still refuses eval.
  • For API clients: additive only. GET /api/v1/plugins/registry (owner only) now returns origin and tables on each plugin. See the API reference.

0.5.0 → 0.6.0​

  • Back up first. Four migrations, 029 to 032, all with a down. The sample compose file's migrate service runs them before the app starts. Run migrations (not just a restart) either way: migrate also creates the database role the new Debt payoff plugin runs under.
    • 029 adds two functions, core.list_users_for_owner() and core.set_user_role(uuid, core.user_role), behind Settings → People. No data changes. Its down drops both functions and leaves every account's role as it is.
    • 030 creates the Debt payoff plugin's schema, plugin_debt_payoff, with two empty tables (debts and plan_settings). Its down drops the schema and everything in it: every person's debts and plan settings. If anyone has used Debt payoff, that is data loss, so restore from your backup rather than going back if you want those debts.
    • 031 adds two indexes to core.transactions, (user_id, merchant, id) and (user_id, amount, id), so sorting the transaction list by merchant or amount no longer sorts the person's whole ledger on every page. The index build blocks writes to transactions (reads continue) while each one builds, one index at a time. On a test ledger of 1,000,000 transactions each build took about 1.2 seconds and writes were held up for at most about 2.2 seconds; a much larger or slower database takes proportionally longer. Don't run it in the middle of a CSV import. Its down drops the two indexes.
    • 032 adds core.erase_instance() and core.instance_activity(uuid), the functions behind "erase this entire instance". They refuse anyone who isn't an owner right now, and no data changes when they are added. Its down drops both functions; an instance that has been erased stays erased.
  • Going back to 0.5.0: run node dist/db/cli.js down four times with the new image, then start the 0.5.0 image. That removes the owner-role functions, the Debt payoff tables and their data, the two indexes and the erase functions.
  • Debt payoff is on after the upgrade. Bundled plugins are registered switched on, so the page and the Debt-free date dashboard widget appear for everyone. An owner can turn it off from Settings → Plugins; its data is kept while it's off. See Debt payoff.
  • Owners can change roles from Settings → People, and the bottom of Settings has two erase actions. Nothing happens until someone uses them.
  • New settings, all optional: DB_POOL_MAX, DB_STATEMENT_TIMEOUT, DB_LONG_STATEMENT_TIMEOUT and IMPORT_MAX_ROWS. The defaults keep today's behaviour, with one exception: the CSV import row cap rises from 50,000 to 100,000 rows (the 8 MB file limit is unchanged). A value outside its range stops the API at start-up with a message naming the variable. See Pool, statement timeouts and the import limit. If you run through PgBouncer, read the PgBouncer note first: it needs one setting changed and two limits moved onto the role.
  • Compression. The API now compresses its responses itself. If a reverse proxy sits in front, make sure it doesn't compress a second time; see Compression: don't compress twice. There is no setting yet to turn the app's compression off.
  • Faster, and a little more accessible. The core pages load when you first visit them, the Transactions page does less work, Budgets reads the ledger at most three times per month request, recurring matches read less, and the dashboard's Upcoming widget makes one request instead of two. Dialogs and the phone drawer manage keyboard focus, charts can be shown as a table, tables have proper headers, and the light-mode green is a shade darker. The accessibility changes haven't been checked with a screen reader yet.
  • The :next image tag is gone. A release candidate is published under its version tag only; see Release tags.
  • For API clients:
    • Breaking: the data export needs a password-confirmed ticket. GET /settings/export answers 403 export_confirmation_required without an x-wickermoney-export-ticket header. Get a ticket from the new POST /settings/export-ticket with { "password": "..." }; it is good for one download for two minutes. The Settings page does both steps with a password dialog. A script that downloads the export itself must make the two requests in order. See The data export.
    • Breaking: POST /analyze on the CSV import no longer returns every row. It returns the counts for the whole file, the first 100 rows and a page of up to 500 rows that need review. A client that read every row from the response needs to read flagged (and page it) instead; commit is unchanged. See The analyze response.
    • Additive: GET /accounts?fields=basic, accountName on the legs of the upcoming outlook, truncated flags on recurring matching, and the new owner-only /users, erase and plugin-token routes.
  • For plugin authors: one breaking change to the stable SDK root. The five Zod manifest schemas (pluginManifestSchema and friends) are no longer exported; validate with parseManifest. addDays and addMonths have a stable home at @wickermoney/plugin-sdk/date; RegisterRoute has a longRunning option; and @wickermoney/plugin-sdk/isolated is a new experimental entry point that the app now hosts (Insights and Spending Trends use it). A manifest gains an optional integrity pin and an optional isolated flag. A manifest that declares network:outbound, schedule:daily or notify is now refused at load; before, it loaded and was ignored. SDK_MAJOR_VERSION is unchanged and existing manifests still load. See Writing a plugin.
  • Plugin UI can now run in a sandboxed frame. Insights and Spending Trends do. They no longer share the app's page, your session or its cookies, and they reach data only through the app, with a short-lived token that carries just their grants. They load a little differently: the first widgets take a little longer to appear because each frame loads its own copy of React, and a frame costs roughly 6 to 9 MiB and 150 ms after the first. Budgets, Import CSV, Upcoming, Forecast and Debt payoff still run in the app's own page, fully trusted. The sandbox does not stop everything: in Chromium, a hostile plugin can still cause some requests through <link rel=prerender> and WebRTC and read nothing back. It is experimental and has not been independently reviewed. See How much a plugin is trusted.
  • Third-party plugins can be added by hand, off by default. Nothing changes unless you set PLUGIN_REMOTE_ORIGINS and PLUGIN_MANIFEST_DIR, both empty by default. There is still no install flow or catalog. See Adding a third-party plugin.

0.4.2 → 0.5.0​

  • Back up first. Two migrations, both reversible:
    • 027 adds plugin_budgets.account_lines for account allowances.
    • 028 replaces the core.register_user function and adds core.instance_has_users() for BOOTSTRAP_OWNER_EMAIL.
  • Going back to 0.4.2: run node dist/db/cli.js down twice with the 0.5.0 image, then start the old image. That discards account allowances and restores the earlier registration function.
  • Name your owner before the first start if the instance has no accounts yet and other people can reach it: set BOOTSTRAP_OWNER_EMAIL to your address (see Owners and members). Unset, nothing changes: the first account to register is the owner. An existing instance keeps every account's role either way. In production the API now logs a warning at start-up when the instance has no accounts, registration is open and the variable is unset.
  • Account allowances. The Budgets page gains an "Account allowances" section, and the Budgets plugin now asks for read access to accounts. An account with an allowance counts as in use, so deleting it asks for the usual confirmation.
  • A faster dashboard. The three dashboard widgets that read the monthly summary now make one request. It is a browser-side change only; see Reads are shared between widgets.
  • Forms open in a dialog. On Accounts, Categories and Recurring, the add forms are buttons in the page header that open the same dialog Transactions uses (a side drawer on desktop, a full-screen sheet on a phone), so the tables get the full page width. Editing a recurring item, fixing an opening balance and resolving a delete that has history open there too. The Transactions filter row lines up.
  • Faster recurring matches. The Transactions page's recurring matches now read their data once per request, 14 database statements instead of 20, the same however many items you have. Same results; nothing is kept between requests.
  • Plugin trust wording. The docs, SECURITY.md and .env.example now say plainly that plugin UI code runs fully trusted in the app's origin, and that the per-plugin database role is a guardrail against honest mistakes in a bundled plugin's server code rather than a sandbox. Behaviour is unchanged. Keep PLUGIN_REMOTE_ORIGINS empty unless you fully trust every origin you list.
  • For API clients: the Budgets and CSV Import routes refuse an id that PostgreSQL could not have generated (a version digit of 0, say) with the usual 400. Ids from the app are unaffected. The Budgets routes gain PUT /account-line and DELETE /account-line, and the Budgets export gains accountLines.
  • For plugin authors: @wickermoney/plugin-sdk/server is new, the SDK's entry points now have stability tiers (/recurrence is experimental), and addDays and addMonths are exported from /recurrence. No export was removed or renamed, so nothing already published needs changing. The one difference is for anyone who copied the app's old date helpers: an impossible date such as 2026-02-30 now throws instead of rolling into March. See Writing a plugin.

0.4.1 → 0.4.2​

  • No migrations. Upgrading is a pull and a restart, and going back to 0.4.1 is safe: run the old image as it is.
  • Check for placeholder passwords first. 0.4.2 refuses to start in production if AUTH_SECRET or the app database password still looks like a placeholder (change-me, change_me, CHANGE_ME or the other throwaway values listed under Placeholder credentials). If you generated real values, as the quickstart says to, nothing changes. If not, fix it before you pull: it takes two minutes, and the section below has the steps.
  • Phones and narrow windows. The sidebar becomes a drawer, and Transactions, Accounts, Recurring and Categories show as cards. Categories and Rules are collapsible, and row actions are icon buttons.
  • Accessibility. Each page has its own tab title, there is a "Skip to content" link, and the light-mode warning colour is darker.
  • Matching. The Transactions page finds recurring-item matches for the newest transactions on a long ledger, where it could miss them before.
  • For API clients: nothing changed.

0.4.0 → 0.4.1​

  • No migrations. Upgrading is a pull and a restart, and going back to 0.4.0 is safe: run the old image as it is.
  • Add the new starter categories. 0.4.1 adds Memberships (under Subscriptions, for store and shopping memberships such as Costco or Amazon Prime) and Domains / web hosting (under Technology). They aren't added to accounts that are already set up. Open Categories → Run setup again and finish the wizard: it adds only what's missing and leaves every category you already have as it is, renames included.
  • Domains / web hosting needs the tech answer. It's created when the setup question about smart home, networking, gaming or web hosting is ticked (the wording now mentions hosting). Memberships is in everyone's base set.
  • For API clients: nothing changed. The catalog is two entries larger.

0.3.0 → 0.4.0​

  • Back up first. Two migrations, both reversible:
    • 025 adds the table that remembers dismissed match suggestions, and a unique (user_id, id) key on core.transactions. Building that index blocks writes (not reads) to transactions for as long as one index build takes.
    • 026 makes only the first account an owner from now on.
  • Going back to 0.3.0: run node dist/db/cli.js down twice with the 0.4.0 image, then start the 0.3.0 image. That discards dismissed suggestions, and puts back the old default that makes every new account an owner.
  • Existing accounts keep their role. Before 0.4.0, every account was registered as an owner, and 026 doesn't change existing rows. If several people signed up on your instance, they are all still owners, and any of them can turn plugins on and off. To check, and to demote with SQL, see Owners and members.
  • For API clients: validation_failed messages are reworded, so a client that matches on message text needs updating. Read code and the new issues list instead (see API reference). GET /api/v1/transactions?uncategorized=true no longer returns linked transfer legs.
  • Building from source needs Node 22.22.2+ or 24.15+. The container image is unaffected.
  • New: Settings → Plugins, matching from the Transactions page and dismissing a suggestion (see Matching), and form errors shown on the field they're about.

0.2.1 → 0.3.0​

  • Back up first. This upgrade can't be rolled back without that backup.
    • 023 (budget windows) adds the btree_gist extension and a constraint so a category can't have two budget lines on the same day. It's reversible: its down deletes any windows and drops the constraint, which leaves monthly lines exactly as 0.2.1 had them. btree_gist is a trusted extension, so the non-superuser database owner can create it.
    • 024 (recurring occurrences) adds the tables behind matching, skips, moves and per-occurrence amounts, and a nullable core.transactions.recurring_occurrence_id. It has no down: dropping it would discard every match and override. Going back to 0.2.1 means restoring the backup.
  • PostgreSQL 15 or later is required by 024, because its foreign key uses ON DELETE SET NULL (column). 16 was already the documented minimum, so a supported install is unaffected.
  • For API clients: budgets refuses a planned amount with more than four decimal places (400) instead of quietly truncating it. Recurring occurrence views gain nominalDate, expectedDate and status; date is where the list places an occurrence, which for a late one is the first projected day.
  • New: Matching for recurring items, and budget windows.

0.2.0 → 0.2.1​

  • No migrations. Upgrading is a pull and a restart, and going back to 0.2.0 is safe.
  • Set your time zone. Accounts created before 0.2.1 are still on UTC, so "today" for recurring items and "Until payday" turns over in the evening anywhere west of UTC. After upgrading, open Settings → Time zone; it offers your browser's zone in one click. Accounts registered from 0.2.1 on take the browser's zone at sign-up.
  • New: the Forecast page (bundled plugin wickermoney.forecast). See Forecast.

0.1.0 → 0.2.0​

  • Back up first. This upgrade can't be rolled back without that backup. Migration 021 reshapes core.recurring_items into an item plus per-account legs (core.recurring_item_legs) and adds a once value to the recurrence enum. PostgreSQL can't remove an enum value, so 021 has no down.
  • Migration 021 copies every existing recurring item's account, amount and transfer target into legs before dropping those columns. A row it can't represent fails the migration and names its id rather than being dropped. The backfill runs in its own transaction, so a failure leaves your recurring items as they were. Fix the row and run migrations again; 021 is safe to re-run.
  • Migration 022 adds core.accounts.spendable and marks existing checking accounts spendable, so the "safe to spend" number starts from the same accounts it would have used anyway.
  • Any plugin that read account_id, amount, transfer_account_id or is_income from core.recurring_items directly breaks. Bundled plugins are already updated.
  • Known limitation (fixed in 0.2.1): there is no time zone setting yet, so "today" follows UTC.

Two version numbers, not one​

The app version (the Docker tag, e.g. 0.2.1) and the plugin API's own contract version (SDK_MAJOR_VERSION) move independently. A plugin built against plugin-sdk cares about the latter, not the former — see Architecture for why the plugin contract is versioned separately from the app itself. SDK_MAJOR_VERSION is not frozen yet (roadmap item, targeted ahead of 1.0), so a bundled plugin can still change shape between app releases.

Placeholder credentials​

From 0.4.2, with NODE_ENV=production (the container image's default), the API and the migrate step refuse to start when AUTH_SECRET or DATABASE_URL contains a placeholder or throwaway value: change-me, change_me, changeme, testpw or _dev_password, in any case (so the quickstart's old CHANGE_ME counts). A placeholder is publicly known: with a known AUTH_SECRET anyone can forge a sign-in. The check never prints the value, and no data is touched when it refuses.

You'll see this in docker compose logs migrate (or wickermoney):

Invalid configuration:
AUTH_SECRET contain a development credential. Refusing to start with NODE_ENV=production.

Are you affected?​

Only if you left a placeholder in place. In the folder with your .env:

grep -i -E "change[-_]?me|testpw|_dev_password" .env

No output means you are fine. POSTGRES_PASSWORD (the owner password) is not checked, so a hit there alone doesn't stop anything; see rotating the owner password if you want to change it anyway.

Fix it (Docker Compose)​

  1. Pick new values and put them in .env:

    openssl rand -base64 48 # AUTH_SECRET
    openssl rand -hex 24 # APP_DB_PASSWORD

    Use -hex for the database password: it is placed inside a connection URL, and +, / and = from base64 would break it. You only need to replace the ones that were placeholders.

  2. Start the stack. migrate runs first, every time:

    docker compose pull
    docker compose up -d

    It reinstalls the tenant key from the new AUTH_SECRET and sets the app database role's password to the new APP_DB_PASSWORD. Look for these two lines in docker compose logs migrate:

    Tenant context key installed from AUTH_SECRET.
    Role 'wickermoney_app' configured: LOGIN granted, password set.
  3. Sign in again if you changed AUTH_SECRET: every session is signed out. Nothing else is lost. Changing only APP_DB_PASSWORD signs nobody out.

If you pulled 0.4.2 first and it stopped, nothing is damaged: edit .env and run docker compose up -d again.

Fix it (without the sample compose)​

Set AUTH_SECRET, and the same new password in DATABASE_URL and APP_DB_PASSWORD, then run migrations with the owner credentials before starting the app, as in the general procedure:

docker run --rm --env-file .env wickermoney node dist/db/cli.js up

Rotating the owner password​

POSTGRES_PASSWORD only sets the owner password when the database is first created, so editing .env later changes nothing in the database. To rotate it, change the role first, then .env:

docker compose exec -it postgres psql -U wickermoney -d wickermoney
# then, at the psql prompt:
\password wickermoney

Set the same value as POSTGRES_PASSWORD in .env afterwards, so migrate can still connect.

After changing AUTH_SECRET​

Re-run migrations. AUTH_SECRET derives the key that signs the tenant context every row-level security policy checks; the API refuses to start if the database's installed key doesn't match the configured secret. Rotating it also invalidates every existing session — expected, not a bug.