Building Tenement

2026-04-03

I keep building apps that only a few people will ever use. ourfam.lol is a family registry site for my extended family. I built a grief support group site for a friend's community. tenant.social is a private social platform for small groups. These are real apps with real users, but the user counts are tiny, maybe 10 active people, maybe 50.

Building them is fun. Deploying them is not. Every time I finish an app, I go through the same exercise: set up a VPS, install dependencies, configure nginx, write a systemd unit file, get SSL working, set up monitoring, and hope I remember to renew the certificate in three months. Three apps means three VPSes, three sets of infrastructure, and three things that can break at 2am for an audience of a dozen people. At some point I noticed I was spending more time managing servers than writing code, and I think that's roughly when this project started, even if I didn't know it yet.

Pieter Levels runs Nomad List, Remote OK, Photo AI, and about 50 other websites on a single $384/month Linode VPS. He makes millions a year with 80% profit margins and zero employees. The stack is PHP, jQuery, SQLite, and nginx on Ubuntu.

The economics of this are more interesting than the tech. A single well-optimized server handles 10,000 requests per second, and almost nobody needs that much. When Levels went on the Lex Fridman podcast and his traffic tripled, his CPU usage actually went down. The bottleneck at that scale is the complexity people put between themselves and the computer, not the computer itself.

I kept coming back to this. All my apps could run on one machine. They just need their own data and their own domains. Why was I paying for three servers?

The setup I wanted was one server running all my apps, where each gets its own subdomain, its own process, and its own SQLite database. When nobody's using the grief group site at 3am, the process shouldn't be running. When someone opens it the next morning, it should come back instantly. Like putting an apartment building on a single lot instead of building three separate houses.

ourfam.lol          ->  ourfam process   ->  /data/ourfam/app.db
remember.app        ->  remember process ->  /data/remember/app.db
tenant.social       ->  tenant process   ->  /data/tenant/app.db

systemd can run the processes, but it can't route the requests. I'd need to write nginx config for each app, and there's no way to stop idle processes and restart them on demand. Docker solves some of this but adds container overhead I don't need for my own code on my own machine. What I really wanted was something like Fly Machines running on hardware I control.

So I built it.

tenement is a process hypervisor. You give it a config file describing how to run your app, and it handles subdomain routing, health checks, scale-to-zero, and wake-on-request.

# tenement.toml
[service.api]
command = "python3 app.py"
health = "/health"
idle_timeout = 300

[service.api.env]
DATA_DIR = "{data_dir}/{id}"
ten spawn api:ourfam
ten spawn api:remember
ten spawn api:tenant

Each instance gets its own process, its own data directory, and its own subdomain. After five minutes with no requests, tenement kills the process. The next request spawns a fresh one. I measured the cold wake times and they're absurdly fast: 65ms for Python, 105ms for Node, 140ms for Go (including go run's cached compile step). Humans can't perceive anything under 250ms, so the user experience is identical to an always-on process. The data directory sticks around between restarts, so the SQLite database is still there when the process comes back.

The design goal was invisibility. Your app reads PORT from the environment and serves whoever shows up. It has no idea tenement exists. All request headers pass through unchanged, including Authorization, so your app's auth layer works exactly the same way it would running standalone. I tested this pretty thoroughly (there's an auth-test example that proves it), and the result is that three separate apps running through the same hypervisor each reject each other's tokens correctly, with tenement doing nothing but forwarding bytes.

It's a Rust binary that runs an HTTP server with a reverse proxy. When a request comes in for alice.notes.example.com, it parses the subdomain into a service name and instance ID, finds the matching process, and proxies the request to its TCP port.

One thing I ended up caring about way more than I expected was process groups. Commands like go run and uv run spawn a child process that does the actual work, and if you kill the parent without killing the child, the child keeps running and holds the port. I found this out while testing with a Go app and watching dead instances refuse to release their ports, which was confusing until I realized go run was spawning a separate binary and I was only killing the compiler. The fix was to put each instance in its own process group and send SIGKILL to the whole group when stopping.

Health checks are straightforward: tenement sends HTTP GET requests to the instance's port on an interval, and three consecutive failures trigger a restart with exponential backoff. On Linux, instances run in PID and mount namespaces for zero-overhead isolation. On macOS it falls back to bare processes, which is fine for dev.

After I built tenement for my personal stuff, I realized it also solves the classic multi-tenant SaaS problem. The traditional approach to multi-tenancy is shared databases with row-level security, connection pooling, and careful data isolation. Every query needs a WHERE clause with the tenant ID. It works, but the complexity compounds, and every new customer is another edge case for your shared infrastructure to handle.

The approach I find more appealing is to not think about tenants at all. Write your app as if it has exactly one customer, with one process and one database and no tenant ID anywhere in your codebase. Then run a copy for each customer.

customer1.api.example.com -> api:customer1 -> /data/customer1/app.db
customer2.api.example.com -> api:customer2 -> /data/customer2/app.db

This only works economically if most of those copies aren't running most of the time, and that turns out to be true for almost every SaaS product. If you have a thousand customers and twenty are active right now, the other 980 cost you nothing because their processes are stopped. You're running twenty processes on a $5 VPS instead of a thousand across a fleet of machines.

SQLite makes this especially clean. Each customer has their own database file, so schema migrations roll out one customer at a time. If a migration breaks something, one customer is affected instead of everyone. Replicate to S3 with walrust for durability and you've got a real data layer without running Postgres. It's like giving every customer their own Honda Accord instead of putting everyone on a bus. The bus is cheaper if it's always full, but it's never full.

The obvious limitation right now is that tenement runs on one server, and one server is a single point of failure. I'm working on a fleet orchestrator called slum that sits in front of multiple tenement servers and routes tenants to the right one based on capacity, geography, or failover requirements.

This is interesting to me because the failover strategy can vary per tenant. A free-tier customer might get cold failover, where slum re-spawns them on a surviving server if their primary goes down. A paying customer could get warm standby, with their process pre-spawned on a secondary server so the switchover is instant. And if someone needs active-active across multiple servers, their app handles shared state with something like haqlite for SQLite replication while slum handles the routing. You'd be offering three tiers of reliability from the same infrastructure, which feels like the right shape for a product.

The end state I'm aiming for is two servers, an S3 bucket, and a highly available multi-tenant platform that costs maybe $15/month. I genuinely don't know if that's achievable, but it seems like the right thing to try.

cargo install tenement-cli

The examples directory has working setups in Python, Node.js, and Go. The multi-runtime example runs all three at once with a 56-test integration script. Full docs at tenement.dev.