Skip to content
These docs describe Tradr v0.14.0. Running an older release? Check the release notes for what changed.

Security model

This page is for someone deciding whether to trust Tradr with their trading history. It states what the application does, and — more usefully — what it does not do and leaves to you.

Tradr holds your trading history: positions, fills, account balances, and whatever you wrote in your notes. It may also hold API keys you have pasted in, which can spend money.

It does not hold brokerage credentials, and it cannot place orders. There is no broker connection — trades arrive by CSV or by hand. A compromised Tradr instance exposes a record of your trading; it does not expose your brokerage account.

Passwords are hashed with bcrypt. The plaintext is never stored, and a login against a non-existent account still performs a hash comparison so that response timing does not reveal which emails are registered.

Sessions are an HttpOnly cookie, so page JavaScript cannot read it. It is SameSite=Lax and expires after 24 hours. Secure is set whenever the api runs in production mode.

The cookie is signed with SESSION_SECRET. Changing that value invalidates every session at once, which is the lever to pull if you think one has leaked.

Every query is scoped by user id in the application layer. There is no row-level security in the database, so that scoping is the boundary — a query written without it would cross users.

This is worth knowing rather than glossing: it means the protection lives in the code, and it is why an instance should run a released version rather than an arbitrary commit.

Keys you paste in are encrypted at rest with AES-256-GCM, using ENCRYPTION_KEY. GCM authenticates as well as encrypts, so a modified ciphertext fails to decrypt rather than producing wrong plaintext.

Set ENCRYPTION_KEY_FINGERPRINT as well. Without it, booting with the wrong key fails late and quietly — the app runs and keys simply refuse to decrypt. With it, the api refuses to start and says why.

With nothing configured, nothing leaves your instance. No telemetry, no outbound calls, no error reporting.

Each of these is off until you turn it on:

Configured What is sent
Analytics Product events — no trade data, users identified by an opaque id
Email Address and message content to your SMTP server
Market data The symbols you look up

The shipped Compose stack publishes one port. postgres and api are reachable only inside the Compose network, so the database is not on your host’s interface.

TLS is yours to provide. The stack does not terminate HTTPS. Put a reverse proxy in front of it — see Put TLS in front of the stack. Without one, session cookies cross the network in the clear.

If you do put a proxy in front, set TRUSTED_PROXIES to its address. Rate limiting is per-IP, and behind an untrusted proxy every request appears to come from the proxy — so the limit applies to everyone at once instead of per client.

Authentication endpoints are rate limited per IP, which blunts credential stuffing and brute force. On a single instance the counters are in memory, so they reset when the api restarts.

Feature gating (FEATURE_GATING) ships off, and with it every plan limit. A self-hosted instance has no tiers. The billing, object storage, and Redis paths are inert until configured, and the test suite enforces that an unconfigured instance behaves as a plain journal.

That is the design rule: a capability you have not configured is absent, not disabled. It makes no outbound calls and logs no errors.

Stated plainly, because a security page that only lists strengths is not useful:

  • No two-factor authentication. A password is the only factor.
  • No audit log of user actions. Admin actions are recorded; ordinary ones are not.
  • No account lockout after repeated failures — only rate limiting.
  • No encryption of trade data at rest beyond whatever your disk provides. Provider keys are encrypted; positions and notes are not.
  • No row-level security in the database, as above.
  • No security guarantee before 1.0. The project is pre-1.0 and has not been independently audited.

If any of those is disqualifying for your situation, better to know now.

  1. Run a released version, and upgrade when one ships. See Upgrade an instance.
  2. Put TLS in front of it.
  3. Generate the three secrets rather than inventing them — docker/quickstart.sh does this.
  4. Set ENCRYPTION_KEY_FINGERPRINT.
  5. Keep .env with your backups, and keep both somewhere the server cannot reach.
  6. Do not expose Postgres. The shipped Compose file already does not.

Report privately, not as a public issue. The process, the scope, and what to expect are in SECURITY.md.