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.
What is at stake
Section titled “What is at stake”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.
Accounts and sessions
Section titled “Accounts and sessions”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.
Separation between users
Section titled “Separation between users”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.
API keys
Section titled “API keys”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.
What reaches a third party
Section titled “What reaches a third party”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 |
| Address and message content to your SMTP server | |
| Market data | The symbols you look up |
Network exposure
Section titled “Network exposure”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.
Rate limiting
Section titled “Rate limiting”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.
What is off by default
Section titled “What is off by default”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.
What Tradr does not do
Section titled “What Tradr does not do”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.
Keeping an instance safe
Section titled “Keeping an instance safe”- Run a released version, and upgrade when one ships. See Upgrade an instance.
- Put TLS in front of it.
- Generate the three secrets rather than inventing them —
docker/quickstart.shdoes this. - Set
ENCRYPTION_KEY_FINGERPRINT. - Keep
.envwith your backups, and keep both somewhere the server cannot reach. - Do not expose Postgres. The shipped Compose file already does not.
Reporting a vulnerability
Section titled “Reporting a vulnerability”Report privately, not as a public issue. The process, the scope, and what to
expect are in
SECURITY.md.
Next steps
Section titled “Next steps”- Put TLS in front of the stack.
- Back up and restore — including the key that makes a backup readable.
- Environment variables — every setting named here.