Tally
Self-hosting guide

Run your own copy of Tally

Tally is source-available under the Functional Source License. You provide your own Postgres database and Plaid account, and the application runs entirely on infrastructure you control. A working Sandbox instance takes about ten minutes to bring up from a fresh clone.

Requirements

  • Node.js 20 or later, and npm
  • PostgreSQL 16, provisioned through the included Docker Compose file or an existing local install
  • A free Plaid Sandbox account, required only once mock mode is disabled. The application runs with zero Plaid credentials in development by default
  • Optional: a tunnel tool such as ngrok or cloudflared, needed only to test OAuth institutions or live webhooks against a local server

Quick start

Start Postgres

docker compose up -d

This provisions Postgres only. If a Postgres instance is already available, skip this step and point DATABASE_URL at it directly.

Configure environment

cp .env.example .env

Set AUTH_SECRET (npx auth secret) and MASTER_KEY (openssl rand -base64 32). Leave every PLAID_* variable blank for now, mock mode does not require them.

Install, migrate, seed

Install dependencies, run migrations, and seed an admin user plus the category taxonomy:

npm install
npm run db:migrate
ADMIN_EMAIL=you@example.com ADMIN_PASSWORD=your-password-here npm run seed:user
npm run seed:categories

Run the app

npm run dev

Visit http://localhost:3000 and sign in. Mock data is enabled by default in development: "Add account" connects a fixture institution with realistic accounts and transactions immediately, with no Plaid credentials required.

Run the worker

In a second terminal, start the background worker:

npm run worker

This is required for automatic, scheduled sync. The app's own "Sync now" action calls the sync engine directly and works without the worker running.

Environment variables

Every variable the application reads, and which of them are required for it to start at all.

VariableRequiredPurpose
DATABASE_URLYesPostgres connection string.
AUTH_SECRETYesSession signing secret. Generate with npx auth secret.
APP_URLYesThe public URL this instance is reachable at. Must be an HTTPS origin in production.
MOCK_DATAOptionaltrue forces mock mode, false forces live Plaid. Unset: development defaults to mock, production defaults to live.
PLAID_CLIENT_IDOptionalFrom the Plaid dashboard. Only needed once mock mode is disabled.
PLAID_SECRETOptionalFrom the Plaid dashboard, matching PLAID_ENV.
PLAID_ENVOptionalsandbox or production.
PLAID_PRODUCTSOptionalComma-separated, defaults to transactions. Actively requested at Link time; each is billed once an item connects.
PLAID_ADDITIONAL_CONSENTED_PRODUCTSOptionalComma-separated, defaults to investments,liabilities. Consented during Link but billed only once actually fetched.
PLAID_COUNTRY_CODESOptionalComma-separated, defaults to US.
PLAID_REDIRECT_URIOptionalRequired for OAuth institutions. Must be registered in the Plaid dashboard first, separately per environment.
PLAID_WEBHOOK_URLOptionalWhere Plaid pushes sync events. Requires a public HTTPS URL (a tunnel in local development).
MASTER_KEYYes32-byte base64 key. Generate with openssl rand -base64 32. Encrypts Plaid access tokens at rest.
CRON_TZOptionalTimezone for the worker's cron schedules. Defaults to America/New_York.
ADMIN_EMAILYesUsed once by npm run seed:user to create the single admin login.
ADMIN_PASSWORDYesUsed once by npm run seed:user. At least 8 characters.

Mock mode vs. live Plaid

How mock mode works

Mock mode resolves as follows: if MOCK_DATA is set, its value wins outright. If it is unset, development defaults to mock and production defaults to live.

In mock mode, "Add account" inserts a canned institution, with checking, savings, credit, and brokerage accounts, directly into Postgres. No network calls are made. Every downstream feature reads the same database rows a live connection would produce, so it exercises the same code paths.

Switching to Sandbox

  1. Get Sandbox credentials from the Plaid dashboard.
  2. Set PLAID_CLIENT_ID, PLAID_SECRET, and PLAID_ENV=sandbox in .env.
  3. Set MOCK_DATA=false.
  4. For OAuth institutions and webhooks, tunnel the local server and set PLAID_REDIRECT_URI and PLAID_WEBHOOK_URL to the tunnel URL. Register the redirect URI in the Plaid dashboard before using it; Plaid rejects Link entirely for an unregistered URI.
  5. Sandbox login: user_good / pass_good (MFA: mfa_device).

Running the worker

npm run worker processes webhook-triggered and cron-scheduled syncs against the same database as the application. It is a separate, long-running process, not a request handler, and must be kept running continuously rather than invoked on demand.

If it stops, nothing syncs automatically. The application's manual "Sync now" action is unaffected, since it calls the sync engine directly rather than enqueuing a job.

Deploying to production

Everything above describes a local instance running against Plaid Sandbox with mock data available as a fallback. Serving real accounts requires four additional decisions: Plaid access, environment configuration, process management, and transport security.

Plaid production access

Plaid does not allow Sandbox credentials to read real accounts. Production access requires an approved application in the Plaid dashboard and is billed per connected item and product; Transactions and Investments are priced separately. Confirm current pricing before connecting real accounts.

Approval issues a second, distinct PLAID_CLIENT_ID / PLAID_SECRET pair, unrelated to the Sandbox pair already in use. A separate redirect URI must also be registered under the production application; Sandbox registrations do not carry over.

Environment differences

  • Set PLAID_ENV=production and the production PLAID_CLIENT_ID / PLAID_SECRET pair. MOCK_DATA does not need to be set explicitly: with NODE_ENV=production, the application already defaults to live mode. Set MOCK_DATA=true only if a production build is being run in a staging capacity without real Plaid access.
  • Set APP_URL to the instance's real, public HTTPS origin, not a local or tunnel address.
  • Set PLAID_REDIRECT_URI and PLAID_WEBHOOK_URL to paths under that same origin. No tunnel is needed in production, since the origin is already public; both values still need to be registered in the Plaid dashboard under the production application before use.

Running the app and worker

The application (npm run build then npm run start) and the worker (npm run worker) are two independent, long-running processes. Both must stay running continuously; a stopped worker fails silently, since manual sync keeps working while scheduled and webhook-triggered sync simply stop.

The docker-compose.yml included in this repository provisions Postgres only. Running the application and worker in containers means adding service definitions for them; running them directly on a host means supervising both with a process manager (systemd, pm2, or equivalent) configured to restart on failure and capture logs. A serverless request handler is not a substitute for the worker: it has no facility for a persistent job-queue listener, so a platform that runs the application as serverless functions still needs the worker hosted separately as a standing process.

Transport and secrets

  • HTTPS is a requirement, not an option: Plaid rejects non-HTTPS redirect and webhook URLs in production. Terminate TLS at a reverse proxy or the platform's load balancer, and enable HSTS there.
  • Do not commit .env or pass secrets on the command line. Use the platform's environment configuration or secret store for DATABASE_URL, AUTH_SECRET, MASTER_KEY, and PLAID_SECRET.
  • MASTER_KEY decrypts every stored Plaid access token. Losing it is equivalent to losing every connected account. Back it up with at least the rigor applied to the database itself, and store that backup somewhere other than the application host.

Backups

A running instance holds a complete transaction history for every connected account. Back up Postgres on a fixed schedule, encrypt the backup files at rest, and store them somewhere other than the database host.

# Backup
pg_dump "$DATABASE_URL" -Fc -f tally-$(date +%Y%m%d).dump

# Restore into a fresh database
pg_restore -d "$DATABASE_URL" --clean --if-exists tally-20260101.dump

A cron job invoking the pg_dump command above on a schedule is sufficient at this scale. Restore from a backup periodically to confirm it is valid; an untested backup is not a verified one.

Secret rotation

PLAID_SECRET: generate a replacement in the Plaid dashboard, update .env, restart the application and worker, and confirm a sync still succeeds before revoking the previous secret. Plaid secrets are never stored in the database, only transmitted to Plaid's API, so no data migration is required.

MASTER_KEY: this key is what every stored access token is encrypted under, so rotating it requires re-encrypting each one. Stop the application and worker, generate a new key, then run:

OLD_MASTER_KEY=<current key> NEW_MASTER_KEY=<new key> npm run rotate:master-key

Set MASTER_KEY to the new value and restart. Do not discard the old key until the rotation script has completed successfully.

License

Tally is available under the Functional Source License 1.1, Apache 2.0 Future Grant (FSL-1.1-ALv2). It may be self-hosted, modified, and used for any purpose other than offering it, or a substantially similar service, to others commercially. Each release converts to Apache 2.0 automatically two years after publication.

# Functional Source License, Version 1.1, ALv2 Future License

## Abbreviation

FSL-1.1-ALv2

## Notice

Copyright 2026 Tejas Raibagi

## Terms and Conditions

### Licensor ("We")

The party offering the Software under these Terms and Conditions.

### The Software

The "Software" is each version of the software that we make available under
these Terms and Conditions, as indicated by our inclusion of these Terms and
Conditions with the Software.

### License Grant

Subject to your compliance with this License Grant and the Patents,
Redistribution and Trademark clauses below, we hereby grant you the right to
use, copy, modify, create derivative works, publicly perform, publicly display
and redistribute the Software for any Permitted Purpose identified below.

### Permitted Purpose

A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
means making the Software available to others in a commercial product or
service that:

1. substitutes for the Software;

2. substitutes for any other product or service we offer using the Software
   that exists as of the date we make the Software available; or

3. offers the same or substantially similar functionality as the Software.

Permitted Purposes specifically include using the Software:

1. for your internal use and access;

2. for non-commercial education;

3. for non-commercial research; and

4. in connection with professional services that you provide to a licensee
   using the Software in accordance with these Terms and Conditions.

### Patents

To the extent your use for a Permitted Purpose would necessarily infringe our
patents, the license grant above includes a license under our patents. If you
make a claim against any party that the Software infringes or contributes to
the infringement of any patent, then your patent license to the Software ends
immediately.

### Redistribution

The Terms and Conditions apply to all copies, modifications and derivatives of
the Software.

If you redistribute any copies, modifications or derivatives of the Software,
you must include a copy of or a link to these Terms and Conditions and not
remove any copyright notices provided in or with the Software.

### Disclaimer

THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.

IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.

### Trademarks

Except for displaying the License Details and identifying us as the origin of
the Software, you have no right under these Terms and Conditions to use our
trademarks, trade names, service marks or product names.

## Grant of Future License

We hereby irrevocably grant you an additional license to use the Software under
the Apache License, Version 2.0 that is effective on the second anniversary of
the date we make the Software available. On or after that date, you may use the
Software under the Apache License, Version 2.0, in which case the following
will apply:

Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License.

You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.