FinDiff Docs

Documentation

FinDiff ships as one Docker image. There is no build step, no separate database server to run, and no schema migration to apply by hand, the binary does that on first start.

Quick start

Pull and run the image directly. This is enough to log in, add a database, and try every feature.

docker run -d --name findiff \
  -p 8080:8080 \
  -e FINDIFF_MASTER_KEY="$(openssl rand -base64 32)" \
  -e FINDIFF_ADMIN_USERNAME=admin \
  -e FINDIFF_ADMIN_PASSWORD=change-this-immediately \
  -v findiff_data:/data \
  oubaidhl/findiff:latest

Open http://localhost:8080, sign in with the admin credentials above, and add your first database from Databases → Add database.

Important: FINDIFF_MASTER_KEY encrypts every stored connection string. Generate it once, keep it in a secrets manager, and never lose it, a lost key makes stored connections unrecoverable.

Preparing a production deployment

1. Generate and store the master key

openssl rand -base64 32

Store it in your secrets manager (Vault, AWS Secrets Manager, a Kubernetes Secret) and inject it as FINDIFF_MASTER_KEY. Treat it like a database root password.

2. Connect with a dedicated read-only role, not an admin account

FinDiff generates the exact GRANT SELECT statements for your engine directly in the app, under Databases → Read-only role SQL, once you've scanned and mapped at least one table. See The read-only role below for the manual version.

3. Persist the data directory

FinDiff's own SQLite store, encrypted credentials, schema mappings, diff history, and AI attachments all live under /data inside the container. Mount a real named volume, never run production on an ephemeral filesystem.

4. Put a reverse proxy with TLS in front of it

FinDiff does not terminate TLS itself. Run it behind nginx, Caddy, Traefik, or your platform's ingress, and only expose HTTPS externally.

5. Set the bootstrap admin account

FINDIFF_ADMIN_USERNAME and FINDIFF_ADMIN_PASSWORD only take effect if no user exists yet, so it's safe to leave them set on every restart. Change the password after first login, and invite named accounts for everyone else from Settings → Users.

6. Configure retention and alerting

Set a retention window under Settings → Retention appropriate to your compliance requirements, FinDiff exports to an archive before it prunes anything. Add a Slack, Teams, email or webhook channel under Settings → Alerts so the right people see changes as they happen.

Docker Compose template

A minimal production compose file. Keep the three environment values in a .env file that never gets committed to version control.

services:
  findiff:
    image: oubaidhl/findiff:latest
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      FINDIFF_MASTER_KEY: "${FINDIFF_MASTER_KEY}"
      FINDIFF_ADMIN_USERNAME: "${FINDIFF_ADMIN_USERNAME}"
      FINDIFF_ADMIN_PASSWORD: "${FINDIFF_ADMIN_PASSWORD}"
    volumes:
      - findiff_data:/data

volumes:
  findiff_data:

Downloadable copies: docker-compose.yml and .env.example.

The read-only role, by hand

FinDiff's in-app generator does this for you, scoped to whatever you've actually mapped. If you'd rather review the shape of it yourself first, here's the general pattern for Postgres:

CREATE ROLE findiff_reader WITH LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE your_db TO findiff_reader;
GRANT USAGE ON SCHEMA public TO findiff_reader;
GRANT SELECT ON your_table_1, your_table_2 TO findiff_reader;

MySQL and MariaDB use CREATE USER ... IDENTIFIED BY and one GRANT SELECT ON db.table per table. SQL Server uses CREATE LOGIN and CREATE USER ... FOR LOGIN. Oracle uses CREATE USER ... IDENTIFIED BY plus GRANT CREATE SESSION. SQLite has no role system at all, restrict access with file permissions instead.

On top of the database-level role, FinDiff runs a statement guard that rejects anything that isn't SELECT, and opens read-only transactions where the driver supports it. The role is defense in depth, not the only control.

Environment variables

VariableRequiredDefaultPurpose
FINDIFF_MASTER_KEYYesBase64-encoded 32-byte key encrypting stored credentials. FinDiff refuses to start without it.
FINDIFF_ADMIN_USERNAMENoBootstrap admin username, created only if no user exists yet.
FINDIFF_ADMIN_PASSWORDNoBootstrap admin password. Change it after first login.
FINDIFF_PORTNo8080Port the HTTP server listens on inside the container.
FINDIFF_DATA_DIRNo./dataWhere the local store, encrypted credentials, and AI uploads live. Always mount this.

Everything else, alert channels, retention windows, the AI provider, per-database snapshot intervals, is configured from the Settings page and takes effect immediately, no restart needed.

Trying it locally

Since FinDiff's source isn't published, "development" here means trying the app against a disposable database rather than building from source.

# a throwaway Postgres database
docker run -d --name findiff-demo-db \
  -e POSTGRES_PASSWORD=findiff \
  -e POSTGRES_DB=findiff_demo \
  -p 5433:5432 \
  postgres:16

# FinDiff itself, pointed at a scratch data directory
docker run -d --name findiff-dev \
  -p 8080:8080 \
  -e FINDIFF_MASTER_KEY="$(openssl rand -base64 32)" \
  -e FINDIFF_ADMIN_USERNAME=admin \
  -e FINDIFF_ADMIN_PASSWORD=devpassword \
  -v findiff_dev_data:/data \
  oubaidhl/findiff:latest

Add the throwaway database using host host.docker.internal, port 5433, database findiff_demo. Scan its schema, map a table, insert a few rows, snapshot, mutate a row, snapshot again, and watch the change land in the Diff Timeline.

Backups & data safety

FinDiff never writes to the databases it monitors, so restoring a FinDiff backup only ever touches its own observability history, never the source data. Back up FINDIFF_DATA_DIR like you would any other database, and keep FINDIFF_MASTER_KEY backed up separately: the encrypted connection strings inside the data directory are unrecoverable without it.

Scaling to large tables

FinDiff pages through tables by primary key rather than loading them whole, so scanning stays bounded regardless of table size. Most engines get a single-round-trip checksum that detects an unchanged table with zero row data transferred. Storage for known rows is kept per distinct row, not per snapshot, so repeated scans of an unchanged table don't multiply what's stored.

For a very large or very active table, configure a watermark column, or on Oracle and SQL Server, native incremental scanning, from that table's settings in Schema Manager. Both are strictly opt-in and read-only; FinDiff never issues DDL against a monitored database.

FAQ

Does FinDiff ever write to my database?

No. A statement guard rejects anything that isn't SELECT, read-only transactions are used where the driver supports them, and we recommend connecting with a database-level read-only role as an independent third layer.

Can I use a fully local, offline AI model?

Yes. The AI Copilot works with Ollama, LM Studio, or any other OpenAI-compatible local server, no data has to leave your network unless you choose a cloud provider instead.

Is the source code available?

FinDiff ships as a versioned Docker image so every install is a tested build. The community repository carries the documentation and is where issues and feature requests are welcome.

Can more than one person use the same instance?

Yes. Any logged-in user can invite another from Settings → Users. It's a flat, shared-trust model for a small team on one instance, not a role-based permission system.

What happens if I lose the master key?

Every stored connection string becomes permanently unrecoverable, you'd need to re-add each database with a new key. Back this value up separately from the data directory.