An internal ops tool that lost a week of data every time it redeployed
A warehouse scheduling tool ran on a framework development server with its SQLite file on the container disk.Composite engagement. Details anonymised; figures are representative.
96.8% → 99.95%
measured uptime after the move
- Platform
- Replit
- Industry
- Logistics · Internal tooling
- Duration
- 2 weeks
- Findings
- 2 critical, 2 high, 2 medium
What we walked into
An operations manager built the tool in Replit to replace a shared spreadsheet that had stopped coping.
Twice in six weeks the team arrived to find the previous week's bookings missing. Both times it was written off as someone deleting rows by accident, and both times a person was spoken to about it.
The same audit found the app serving traffic from the framework's development server with the interactive debugger switched on, which meant any unhandled exception offered a stack trace and a Python console to whoever triggered it.
Every engagement opens with a free consultation and a scoped audit. This one drew on the following:
Composite engagement. Details anonymised; figures are representative.
What we found
Severity is about consequence, not effort. A critical finding is one where a stranger can read data, move money, or destroy state.
- Critical
User data on an ephemeral container disk
app.db was written to the working directory. Redeploys, idle restarts, and platform maintenance all replace that filesystem.
- Critical
Development server facing the internet
The app ran under the framework's dev command with debug mode enabled - a single-threaded server with an interactive debugger attached.
- High
Credentials hard-coded in source
The SMTP password and the carrier API token were string literals in a config module, committed since the first week.
- High
Two admin routes with no authentication
The export and reset endpoints were reachable without a session. The links to them were only rendered for admin users, which is not the same thing as protecting them.
- Medium
Cold starts on a sleeping instance
The container slept after inactivity, so the first request each morning took eleven to eighteen seconds.
- Medium
No logs that survived a restart
Output went to stdout and disappeared with the container. When something failed there was nothing to read, so every incident investigation started from a verbal description of what the screen had looked like.
The work we did
Delivered over 2 weeks, on branches, alongside a founder who kept shipping product the whole time.
- 01
Migrated the SQLite database to managed Postgres, keeping the schema and adding the constraints SQLite had been quietly ignoring - three foreign keys and two uniqueness rules that had already allowed duplicate bookings.
- 02
Ran the migration twice: a full dry run against a copy to measure the window, then the real cutover, with the old file kept read-only for a fortnight.
- 03
Replaced the development server with a production WSGI server behind a reverse proxy, sized to four workers, with health checks and graceful restarts.
- 04
Containerised the app so the same image runs on a laptop, in staging, and in production.
- 05
Moved every credential into a managed secret store, rotated all of them, and rewrote the repository history to remove the committed values.
- 06
Put both admin routes behind the session check and added tests asserting that an anonymous request to each returns 401.
- 07
Deployed to an always-on instance in the region the warehouse is in, so the first request of the day is no longer the slow one.
- 08
Set up nightly automated backups with a monthly restore drill, structured JSON logging with thirty-day retention, error tracking, and uptime alerting into the operations channel.
Before and after
The same metrics, measured the same way on both sides of the work. Where a number got worse, it is in the table.
Data-loss events
3 in 6 weeks0 in 9 monthsMeasured since the migration. The database no longer lives on a disposable disk.
Measured uptime
96.8%99.95%Thirty-day windows before and after, from the same external monitor.
p95 response time
1.9 s310 msMost of the gain is concurrency - one worker to four - plus connection pooling.
First request after idle
11–18 sUnder 400 msThe instance no longer sleeps, so nobody has to warm it up before a shift.
Backup restore
No backupsNightly, drilled monthlyThe most recent drill restored the 1.4 GB database in three minutes.
Monthly infrastructure cost
$25/mo$95/moManaged Postgres, an always-on instance, and real backups cost more than a sleeping container.
Composite engagement. Details anonymised; figures are representative. Figures are rounded from what we measured across the engagements this study is drawn from.
What it runs on now
The builder stayed in the picture where it earned its place. Nothing here was rewritten for the sake of rewriting it.
- Replit
- Python + Flask
- Gunicorn
- PostgreSQL (managed)
- Docker
- Caddy
- Fly.io
- Sentry
- Uptime alerting
We had blamed two people for losing that data. Finding out it was the disk was a relief and also not a good day.
Other write-ups
Different builder, same shape of problem.
- Base44
Record-level authorization for a Base44 equipment-rental marketplace
6 → 0 - endpoints returning other users' records
Read the write-up - Claude Code
A Claude Code B2B app with no way to deploy it twice the same way
40 min → 6 min - commit to production, unattended
Read the write-up - Bubble
A Bubble booking app whose platform bill grew faster than its revenue
$2,300 → $640 - monthly platform bill, same traffic
Read the write-up
Same builder, same symptoms?
Send us a repo link and a paragraph about what it does. The audit is free and the report is yours either way.