Flagship · Live in production
PropVexis
A multi-tenant SaaS trading journal for prop-firm traders. Designed, built and operated solo.
The problem
Traders on funded prop accounts get one shot at a strict rule set — breach the daily or maximum drawdown and the account is gone. Most of them track it in spreadsheets that tell them what happened yesterday, not that they are 0.4% away from losing the account right now.
How it fits together
What I built
Real-time ingestion, end to end
A MetaTrader 5 agent I wrote in MQL5 streams closed trades and account equity into an idempotent ingest API, so a retry or a replayed batch can never double-count a trade. Data lands in PostgreSQL and reaches the open browser over WebSockets within a second of the position closing.
A configurable rule engine
Every equity snapshot is evaluated against the trader's prop-firm rule set — daily and maximum drawdown, profit targets, minimum trading days — firing breach and proximity alerts before an account is lost, alongside ROI and payout tracking. Rule sets are data, so onboarding a new firm needs no code change.
Analytics moved into the database
The dashboard originally aggregated in application loops. I rebuilt it as PostgreSQL CTE and GROUP BY queries with composite indexes, cached in Redis and invalidated across clustered workers over Pub/Sub. It now holds roughly 1,000 concurrent users on a single instance.
Multi-tenancy and billing
Google OAuth 2.0 into JWT httpOnly-cookie sessions, row-level tenant scoping on every query, plan-gated entitlements, and Razorpay subscriptions with idempotent webhooks.
Decisions and trade-offs
The interesting part of a system isn’t what it does, it’s what it gave up. Each of these had a cheaper option I didn’t take.
- 01
Idempotent ingest, not at-most-once delivery
The agent runs on a trader's home PC over a connection I don't control, so retries and duplicate batches aren't edge cases — they're the normal operating condition. Deduplicating on a deterministic key at write time makes a replay a no-op, which lets the agent retry blindly and stay dumb. The alternative, tracking acknowledgement state on the client, puts correctness in the least reliable part of the system.
- 02
Rule sets as data, not code
Every prop firm has slightly different rules — different drawdown basis, different reset times, different minimum trading days. Storing rule sets as rows means onboarding a firm is a config change rather than a deploy, and one evaluator covers all of them. The cost is a more abstract engine and validation I have to write by hand instead of getting it from the type system.
- 03
Aggregation in PostgreSQL, not in Node
The dashboard originally pulled rows and reduced them in application loops. I moved it to CTEs and GROUP BY with composite indexes, so it's one round trip and the query planner does the work. What made the rewrite safe was that the existing tests asserting on the JavaScript aggregation became the oracle for the SQL version — same inputs, same numbers, or the build fails.
- 04
Invalidation over short TTLs
A short TTL is less machinery, but it means a trader can close a position and still see a stale dashboard — the one thing this product cannot do. Publishing invalidation over Redis Pub/Sub keeps every clustered worker consistent the moment data changes. The price is a message bus in the read path and a cache that fails toward correctness rather than availability.
- 05
httpOnly cookies over localStorage tokens
A token in localStorage is readable by any script that ends up on the page; an httpOnly cookie isn't. That buys XSS resistance and costs me CSRF handling and a same-site policy — a smaller and much better-understood problem than token theft.
- 06
Row-level tenant scoping over schema-per-tenant
Separate schemas are safe by construction but make migrations and any cross-tenant query painful. A single schema with scoping applied in one shared place is easier to evolve, and the isolation risk is concentrated where I can test it directly — so there are tests that assert one tenant's queries can never return another's rows.
Shipping it
I own the infrastructure too: GitHub Actions CI/CD to AWS EC2, three isolated environments, secrets in SSM Parameter Store, Terraform, Docker, Prometheus/Grafana and Sentry, nightly S3 backups.
- 01testnode:test suite
- 02buildfrontend bundle
- 03shiprsync to EC2
- 04migrateschema forward
- 05releasepm2 restart
What I’d change next
- Move PostgreSQL off the application box to a managed instance. Co-locating them is the current single point of failure and the thing I'd fix first.
- Containerise the whole thing and run it on Kubernetes, with Terraform managing the infrastructure end to end rather than existing alongside it.
- Replace the last O(n) read path — the equity curve — with a materialised view, since it's the only query that still scales with a trader's history.
Happy to walk through any of this in detail — get in touch.