Tally
Self-hosting guide

Run your own copy of Tally

Tally is source-available under the Functional Source License. This is a self-hosted app: you provide your own Postgres database and Plaid account, and it runs entirely on infrastructure you control. Deploying to production below is the primary path: a real instance connected to your own accounts. Local development with mock data is for iterating on the code itself, not a substitute for actually standing up an instance.

Requirements

  • A Vercel account (or any host that runs a standard Next.js app) and a Postgres database. This repo's Marketplace integrations provision both Neon Postgres and Upstash QStash automatically
  • A Plaid production application, approved and billed by Plaid, to connect real accounts (see “Deploying to production” below). A free Sandbox account is enough for local development instead
  • Node.js 20 or later and npm, only needed for local development or running one-off scripts against a deployed instance

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_SECRETYesAuthorizes the nightly Vercel Cron Job (app/api/cron/nightly) and the Upstash QStash-triggered sync (app/api/cron/sync-all). Generate with openssl rand -base64 32.
QSTASH_TOKEN / QSTASH_URL / QSTASH_CURRENT_SIGNING_KEY / QSTASH_NEXT_SIGNING_KEYYesAuto-provisioned by installing the Upstash QStash integration from the Vercel Marketplace. Powers the twice-daily transactions safety net, since Vercel's own Cron Jobs are capped at once/day on the Hobby plan.
NET_WORTH_CURRENCYOptionalDefaults to CAD. The one figure in the app that's actually currency-converted (via Frankfurter's free daily rates). Every account/holding balance elsewhere stays labeled in its own currency, unconverted.
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.

Deploying to production

This is the path for actually running Tally against real accounts. It assumes deploying to Vercel, which this repo is set up for out of the box (vercel.json's Cron Job, the Upstash QStash integration below). The app itself is a standard Next.js app and will run on any platform that supports one, but the cron/queue wiring described here is Vercel-specific.

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 used for local development. A separate redirect URI must also be registered under the production application; Sandbox registrations do not carry over.

Database, cron, and queue

There is no separate worker process to run or supervise. Webhook-triggered syncs (app/api/plaid/webhook) run inline within the request, since Vercel Functions don't support a persistent listener process, and one item's sync takes a few seconds, well inside a Function's execution budget.

Scheduled sync is two pieces, both already wired into this repo:

  • A nightly balance refresh (app/api/cron/nightly) runs as a native vercel.json Cron Job, authorized by CRON_SECRET.
  • A twice-daily transactions safety net (app/api/cron/sync-all). Vercel's Hobby plan caps native Cron Jobs at once/day, so this one runs on Upstash QStash instead. Install it from the Vercel Marketplace (vercel integration add upstash/upstash-qstash), which provisions QSTASH_TOKEN/QSTASH_URL/signing-key env vars automatically, then register the recurring schedule once:
    APP_URL=https://your-instance.example.com npx tsx scripts/setup-qstash-schedule.ts

Postgres itself: any provider works, but the Neon Postgres integration (also via the Vercel Marketplace, vercel integration add neon) provisions DATABASE_URL the same way.

After the database and env vars are in place, run migrations and seed the admin login against the production database once:

DOTENV_CONFIG_PATH=.env.production.local npx tsx db/migrate.ts
DOTENV_CONFIG_PATH=.env.production.local ADMIN_EMAIL=you@example.com ADMIN_PASSWORD=your-password-here npx tsx scripts/create-user.ts
DOTENV_CONFIG_PATH=.env.production.local npx tsx scripts/seed-categories.ts

(vercel env pull .env.production.local --environment=production first, to get real production credentials locally for that one run. Don't commit that file.)

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.

Transport and secrets

  • HTTPS is a requirement, not an option: Plaid rejects non-HTTPS redirect and webhook URLs in production. Vercel terminates TLS automatically; on another host, terminate it at a reverse proxy or 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.

Local development

For iterating on the code itself, not a substitute for the production setup above. Runs entirely with mock data by default, with zero Plaid credentials needed.

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. Manual “Sync now” and the nightly/twice-daily automatic sync both work locally the same way they do in production, with no separate process to start.

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

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 the environment variable and redeploy, 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. With the app still running on the old key, generate a new key and run:

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

Set MASTER_KEY to the new value and redeploy. 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.