$ cd ..
// its-a-text-file.md

It's a Text File. It's Always a Text File.

craftarchitectureretroopinion

A note on names. Two of the systems in this piece are things I work with at my day job. I’ve renamed them and am being deliberately vague about their names and purposes. What the apps do isn’t really important. What we can do with them, and what they can teach us, is the interesting part. Let’s call them Foo, a big enterprise desktop client, and Bar, a big client/server system with a licensed API. Everything else I say about how they behave is true.

I spent a couple of days in my new role taking apart Foo, a desktop client that’s been around a long time. Hundreds of megabytes of DLLs, a control engine, a plugin architecture, desktop hardware integrations, customer extensibility onramps. We won’t mention the voluminous cruft and shims that come with massive vendor packages built for every edge case for every one of the thousands of customers the vendor has served over the years, but oh… it’s there.

The app recently gained a deep linking feature. A deep link is a specialized type of link that takes you straight to a specific page or screen in an application when you click on it. Kind of like a link taking you to a specific song within Spotify vs taking you to Spotify’s home page. The vendor had shared some terse and complicated documentation along with warnings about the complexity and limitations of the feature.

I was curious! What unique and innovative means did this vendor come up with, in this year of our Lord 2026, to achieve the mystical feat of deep linking? Text files. It’s always text files. I loaded the DLLs into ILSpy, removed the Ovaltine-ring-grade .NET obfuscation they’d applied and traced how the app responded to a deep link fired using the “supported” method in the vendor’s docs. There, buried under 12 DLLs and a couple of binaries, was a plain ol’ file watcher keeping an eye on a single directory.

That was the big secret. A process that watches a folder for text files that are dropped in. As long as the syntax of the command inside the text file is correct, the app will execute it and load the desired page.

I’ve been doing this a long time, and I wasn’t surprised so much as I was validated. I’ve been seeing this exact kind of thing since 1992.

Door games

If you’re my age and into computers, you might remember door games. Legend of the Red Dragon, TradeWars, Usurper. A Bulletin Board System would hand you off to a separate program, a “door”, and that program would run the game and hand you back to the BBS when you were done.

No shared memory, no IPC, no API, no SDK. How did they do it? The BBS and the game wrote text files. DOOR.SYS, or DORINFO1.DEF, or CHAIN.TXT depending on whose software you were running. The door read it, ran, and wrote back. That’s the whole integration contract. And the screens? The fancy ANSI art login screens with the color bars and the glowing logos? Text files. Escape codes and characters. The BBS shoved them down the wire and your terminal drew them. I’ve rebuilt TriBBS in Go and the template engine at the heart of it is a text file with @VARIABLE codes sprinkled through it.

30+ years between the door games and Foo. Text files.

The thesis

So here it is: it’s a text file. Whatever it is. Peel back enough layers and you always land on a file with characters in it, sitting in a directory, and that is WONDERFUL.

Databases are a pretty big deal too! Fine. But what’s the database? What do you think a database is?

The first sixteen bytes of every SQLite database on Earth are the string SQLite format 3 followed by a null. It’s one file. Copy it, email it, check it into git. Postgres is a directory full of files. The pages are binary, sure, but it’s mostly files. tar the directory and you’ve backed up the database. There’s no magic layer where the data stops being files. It’s files all the way down, and then it’s blocks, and then it’s magnets.

A tour of the fancy stuff

The lakehouse. I’ve been coming up to speed on Databricks. New job, and my team’s job is to understand it and plug it into everything else. It comes laden with vocabulary - delta tables, medallion architecture, bronze/silver/gold, catalogs, lakeflow. Billions in valuation, conferences and certifications.

Here’s what’s underneath. A Delta table is a folder of Parquet files plus a subfolder called _delta_log. In that subfolder are numbered JSON files, 00000000000000000000.json, 00000000000000000001.json, and so on. Go open one:

{"commitInfo":{"timestamp":1757635200000,"operation":"WRITE","operationParameters":{"mode":"Append"}}}
{"add":{"path":"part-00000-3f2a9c.snappy.parquet","size":18232,"modificationTime":1757635200000,"dataChange":true}}
{"remove":{"path":"part-00000-8b1e4d.snappy.parquet","deletionTimestamp":1757635200000,"dataChange":true}}

Add this file. Remove that file. That’s a diff. That’s all it is. The lakehouse is a folder of Parquet files and a folder of diffs. The contents of the table are whatever you get by replaying the diffs in order. Databricks, Snowflake and a lot of these other tools are almost just a front end to Parquet files in blob storage. Bronze, silver and gold layers are just different Parquet files generated by scripts.

Git. Your branch is a file at .git/refs/heads/main containing forty hex characters. HEAD is a file containing the string ref: refs/heads/main. The objects are zlib-compressed, but run one through git cat-file -p and out comes a text file: a tree hash, a parent hash, an author line, a message. Everything else is gilding.

Docker. The container revolution is a tarball. Run docker save on an image and you get a manifest.json, a config JSON, and a stack of layer tarballs. You can deploy to a live stack by piping that tarball over SSH. A file over a wire into a folder.

Kubernetes. Everything you tell it is YAML. Everything it tells you back is YAML. The most complicated orchestration system on the planet, and the interface is indented text.

Terraform. Your entire cloud footprint, real money and real running metal, described in text files, with the state of the world kept as a JSON file in a bucket.

.NET Aspire. Write real C# in an app host, it compiles down to a manifest, and the manifest is JSON that something else reads to stand up your world.

Python. That black or pytest or uv-installed tool sitting in your bin directory? It’s not a binary. Go cat it:

#!/home/you/.venv/bin/python
import re
import sys
from black import patched_main
if __name__ == "__main__":
    sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
    sys.exit(patched_main())

That’s the executable. A shebang and seven lines. The wheel it came from is a zip file, and inside the zip is METADATA and RECORD and entry_points.txt, and every one of those is plain text. Nothing compiled anywhere.

The web. A TLS certificate is base64 text between two lines that literally say BEGIN CERTIFICATE and END CERTIFICATE. robots.txt. Sitemaps. DNS zone files running name resolution for the entire planet. This site you’re reading is a directory of HTML that I rsync to a folder that nginx points at. I could serve a live API today by dropping a JSON file in a blob container. No server, no runtime, no framework. A file in a folder, reachable from anywhere on Earth.

A good thing

“It’s just text files” isn’t meant to be cynical or dismissive. Quite the opposite! When I say “It’s just text files” in my head, I’m saying it with all the enthusiasm of a Hobbit who just discovered it comes in pints. Text files are gooooooood.

Text is diffable. Text is greppable. Text is reviewable. Text is versionable. You can pipe it, sed it, sort it, sign it, and mail it to someone forty years from now and it still opens.

Now flip it around. You can’t diff a binary blob. You can’t code-review a sequence of GUI clicks. You can’t roll back a wizard. You can’t grep a proprietary format. You can’t audit what you can’t read.

Every practice we consider “real engineering” - version control, code review, CI, audit trails, reproducible builds, infrastructure as code - exists only because the artifacts are text files. Not mostly. Only.

I wrote a while back about pushing your standards into things that fail builds instead of things you hope people read. Same idea. A policy that lives in a wiki page is a wish. A policy that lives in a text file the pipeline parses is a hard deterministic gate.

In cave diving you can strip every piece of kit down to optional except one: the line. The guideline is a piece of string tied off at the entrance. Every fancy thing, the scooters, the rebreathers, the stage bottles, hangs off the assumption that the string is there and you can follow it home in zero visibility with your eyes closed. The string is the primitive. Everything else is convenience.

Text is the string.

Abstraction, and the test for good layers

There’s an old line that all problems in computer science can be solved with another level of indirection. And the standard reply: except the problem of too many levels of indirection.

Abstraction isn’t the enemy. Unpeelable abstraction is. A pretty GUI is a fine thing to have - some people like them and I’ve made a career out of building things that help make pretty GUIs. But the pretty GUI has to be additive - not the only way in. It should be an abstraction that creates convenience, not one that gatekeeps.

Cursed abstractions hide things and keep what’s really going on hidden from view.

Permission, hacking and “is this ok” aka get to the point already

Then there’s Bar - it’s a big client/server system and it’s one of those vendors. You know the type! The only integration path they push is the paid one, their documentation and information are locked behind paid training and there is an armada of MSPs just waiting to tell you how complicated software development is and they can make all your worries disappear for $250 an hour.

Instead we took the vendor’s own DLL and hosted it inside of a small .NET service and re-exposed the entirety of its methods as clean REST endpoints. Took us just under a day.

The reaction from some people wasn’t “how does that work”. It was closer to “that can’t be legitimate”. I’ve had to demonstrate it. Which is fine. Prove your claims. But it kept striking me as strange, because I wasn’t doing anything clever. A dynamically linked library is meant to be dynamically linked and called. That is the entire purpose of the format. We read the metadata and used the thing the way the thing was designed to be used.

Same with the file watcher in Foo. I didn’t break in. I read what was in front of me and walked through a door that was standing open.

And I think that’s ultimately what this article is about. It’s about curiosity. About seeking. About wondering. When you know deep down that in the end it’s all text files, you gain the confidence in knowing there’s always an answer. There’s always a way. All the cool apps, fun games and amazing services you use every day are bits in a file, in motion.

And if a computer can touch them, so can you.

A lot of very good engineers have only ever been shown a sanctioned path. When someone takes a different path it can read as magic or cheating. It’s usually a matter of literacy. The system was never sealed, there is no magic and it’s just a man with some levers behind the curtain. The DLL has metadata, the assembly decompiles and the drop file is sitting there in a directory, same as it was in 1992.

Nobody locked it. They just didn’t mention it was open.