A little while ago I posted Edi Life OS on Hacker News. It's a self-hosted dashboard for habits, goals, focus, money and projects, written in plain PHP, MySQL and vanilla JavaScript, with an MCP server so Claude and other assistants can work with your real data.
The launch brought a lot more eyes than I expected. More people looking at the code meant more people who would actually install it on their own servers, so the first release after launch had one theme: trust.
Here's what changed in v1.1, and what I learned doing it.
A security pass, written down in public
I went through the PHP backend and the frontend JavaScript and wrote every finding into one public issue, ordered by priority. Then I fixed them from the top.
High
-
Stored XSS on the dashboard. Goal titles, categories, metrics and finance category names were rendered with
innerHTMLwithout escaping. Those values can also be written through the bearer-token API, so API data could run script in the signed-in session. They're now escaped. -
Open redirect after sign-in. The
redirectparameter only had to start with/and not//, which browsers can be tricked around. It now has to be a real same-origin path.
Medium
-
CSRF on Habittify. Every write endpoint checked a CSRF token except the habit tracker's API. Now it does too, through one shared
life_os_require_csrf()helper. -
Sign-in rate limiting. After 5 failed attempts from one client in 15 minutes you get
429withRetry-After, and the password isn't even checked. The limit is per IP (stored as a SHA-256 hash), not per username, so an attacker can't lock the owner out from everywhere. -
Security headers.
Content-Security-Policy,X-Frame-Options: SAMEORIGIN,Referrer-PolicyandX-Content-Type-Optionson every response. The CSP still allows inline scripts for now, because the app has inline handlers; moving them out is next. -
HTTPS behind a reverse proxy. A new
LIFEOS_TRUST_PROXYsetting makes the session cookieSecurebased onX-Forwarded-Protoand rate-limits by the real client IP. It's off by default, because trusting those headers on a directly exposed app would let anyone spoof them.
The lesson: with no framework, every protection a framework gives you for free (escaping, CSRF, headers) has to be built on purpose and applied everywhere. One endpoint that was written a little earlier than the others is enough to leave a gap.
A leaner MCP server
The MCP server never touches the database. Every tool goes through the same token-protected HTTP API the app uses, delete tools are marked destructive so the client asks first, and secret notes stay hidden unless the server explicitly allows them.
In v1.1, the 12 read-only tools became one lifeos_query tool with a type parameter (dashboard, goals, tasks, habits, finance_summary, notes and so on). Fewer tools means less of every conversation spent on tool descriptions, and an easier choice for the model.
Also new
- Display currency. Finance started out Toman-only. You can now pick USD, EUR, GBP, AED and others in Settings. Amounts are labels and are never converted.
- A landing page: edrisranjbar.github.io/lifeos
- First outside contributions: CI that runs the PHP and Node tests on every PR, a Docker health check, and light-theme screenshots.
Try it
git clone https://github.com/edrisranjbar/lifeos.git && cd lifeos
cp .env.example .env # change the username and passwords
docker compose up -d # then open http://localhost:8080
There's also a release zip for shared hosting: extract it, add your MySQL details, done.
- Repo: https://github.com/edrisranjbar/lifeos
- Release notes: https://github.com/edrisranjbar/lifeos/releases/tag/v1.1.0
If you self-host things: what's the first thing you check before you install an open-source app on your server? I'd love to hear it in the comments.

Top comments (1)
tr.ee/dev-to