<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Priyanshu Verma</title>
    <description>The latest articles on DEV Community by Priyanshu Verma (@priyanshuverma).</description>
    <link>https://dev.to/priyanshuverma</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F994703%2F071b4abb-44c9-4685-aa5a-04f98019e345.png</url>
      <title>DEV Community: Priyanshu Verma</title>
      <link>https://dev.to/priyanshuverma</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/priyanshuverma"/>
    <language>en</language>
    <item>
      <title>Storing Documents: From a JSON String to Bytes on Disk</title>
      <dc:creator>Priyanshu Verma</dc:creator>
      <pubDate>Sun, 25 Oct 2026 10:40:07 +0000</pubDate>
      <link>https://dev.to/priyanshuverma/storing-documents-from-a-json-string-to-bytes-on-disk-4cj8</link>
      <guid>https://dev.to/priyanshuverma/storing-documents-from-a-json-string-to-bytes-on-disk-4cj8</guid>
      <description>&lt;p&gt;&lt;em&gt;Series: nosqlite devlog, Part 4&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;By the end of the third article, nosqlite had empty collections.&lt;/p&gt;

&lt;p&gt;You could launch the shell, type &lt;code&gt;create users&lt;/code&gt;, and the database would happily nod, update its catalog page, and save the collection name to disk. But a database that only stores collection names is just an expensive notepad. It is the architectural equivalent of putting empty cardboard boxes in a warehouse and calling it a logistics empire.&lt;/p&gt;

&lt;p&gt;It was time to store actual data.&lt;/p&gt;

&lt;p&gt;This was the part of the project I had been looking forward to the most, because this is where database theory stops being abstract and starts turning into raw binary operations. When someone uses a document database, they think in JSON. They type &lt;code&gt;{ "name": "Priyanshu", "age": 22 }&lt;/code&gt; and expect the database to tuck that object away safely on disk, keep it intact through computer reboots, and hand it back when asked.&lt;/p&gt;

&lt;p&gt;Turning that simple expectation into bytes inside a 4096-byte page forced me to make some of the most critical design decisions in the entire project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Temptation of Raw JSON
&lt;/h2&gt;

&lt;p&gt;When you sit down to build a document database from scratch, there is an obvious shortcut whispering in your ear: why not just store JSON strings?&lt;/p&gt;

&lt;p&gt;Think about how easy that sounds. In Rust, you can add &lt;code&gt;serde_json&lt;/code&gt;, call &lt;code&gt;to_string()&lt;/code&gt;, and get a plain UTF-8 string. You write that string directly into the file. If you want to read it back, you read the string and parse it. It is human-readable, every language supports it, and you can inspect your database with a regular text editor.&lt;/p&gt;

&lt;p&gt;It feels like the right move until you think about what a database actually does when it reads data.&lt;/p&gt;

&lt;p&gt;Imagine you have a collection with ten thousand documents. Each document has fifty fields, and you want to find all users where the &lt;code&gt;age&lt;/code&gt; field is greater than twenty.&lt;/p&gt;

&lt;p&gt;If your documents are stored as raw JSON strings, the database has to parse the text of every single document from byte zero. It has to scan through characters, match curly braces, skip whitespace, interpret quotation marks, unescape special characters, and search for the characters &lt;code&gt;"age"&lt;/code&gt;. If you are scanning ten thousand documents, your CPU spends all its cycles doing string parsing instead of database work.&lt;/p&gt;

&lt;p&gt;Then there is the data type problem. JSON is loose. When JSON says &lt;code&gt;42&lt;/code&gt;, is that an 8-bit integer, a 32-bit integer, a 64-bit integer, or a floating-point number? JSON does not specify. It just has numbers.&lt;/p&gt;

&lt;p&gt;What if you want to store a binary payload, like a cryptographic hash or an image thumbnail? JSON has no native binary type. You have to encode the bytes as base64 strings, which inflates the size by thirty-three percent and wastes storage.&lt;/p&gt;

&lt;p&gt;What if you want to store a timestamp? JSON has no date type. You store it as an ISO 8601 string like &lt;code&gt;"2026-10-19T20:00:00Z"&lt;/code&gt;, which takes twenty-four bytes and requires string parsing every time you want to compare two dates.&lt;/p&gt;

&lt;p&gt;This is why the team behind MongoDB created BSON, which stands for Binary JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why BSON Changes the Game
&lt;/h2&gt;

&lt;p&gt;BSON takes the flexibility of JSON and encodes it into a structured binary format.&lt;/p&gt;

&lt;p&gt;Instead of writing text that has to be parsed character by character, BSON prefixes every single field with a type byte and a length.&lt;/p&gt;

&lt;p&gt;Let me make this concrete. If a document has a string field named "name" with the value "Priyanshu", BSON does not write &lt;code&gt;"name": "Priyanshu"&lt;/code&gt;. It writes something closer to this:&lt;/p&gt;

&lt;p&gt;First, a single type byte (&lt;code&gt;0x02&lt;/code&gt;), which signals: the following value is a UTF-8 string.&lt;br&gt;
Next, the field name as a null-terminated C string: &lt;code&gt;name\0&lt;/code&gt;.&lt;br&gt;
Next, a 4-byte integer recording the length of the string value: &lt;code&gt;10&lt;/code&gt; (including the null terminator).&lt;br&gt;
Finally, the string bytes themselves: &lt;code&gt;Priyanshu\0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why does this matter so much for performance?&lt;/p&gt;

&lt;p&gt;Because of the length prefix! If a reader is scanning a document looking for &lt;code&gt;age&lt;/code&gt;, and it encounters the &lt;code&gt;name&lt;/code&gt; field, it does not have to parse the string "Priyanshu". It reads the length prefix (10 bytes), adds 10 to its read cursor, and jumps over the string in a single CPU instruction.&lt;/p&gt;

&lt;p&gt;Integers in BSON are stored as raw 4-byte or 8-byte little-endian numbers, not digit characters. Comparing two integers is a single CPU comparison, not string parsing. Binary blobs are stored as raw bytes with a length prefix, with zero base64 overhead.&lt;/p&gt;

&lt;p&gt;The only downside is that you cannot open the database file in Notepad and read your documents. But for an embedded database engine, that is a trade I will make every single day. The shell handles formatting JSON for humans; the storage engine works in binary.&lt;/p&gt;

&lt;p&gt;Rather than reinventing a binary serialization format, nosqlite uses the &lt;code&gt;bson&lt;/code&gt; crate from crates.io. It is battle-tested, fast, and handles the full BSON specification.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Twelve Bytes of an ObjectId
&lt;/h2&gt;

&lt;p&gt;In a relational database, you usually have an auto-incrementing integer for your primary key: 1, 2, 3, and so on.&lt;/p&gt;

&lt;p&gt;In a document database, that approach falls apart quickly. If two different worker threads or distributed clients insert documents at the same time, coordinating an auto-incrementing integer requires centralized locking.&lt;/p&gt;

&lt;p&gt;Instead, document databases use an &lt;code&gt;_id&lt;/code&gt; field containing an ObjectId.&lt;/p&gt;

&lt;p&gt;When someone inserts a document into nosqlite without an &lt;code&gt;_id&lt;/code&gt;, the engine generates one automatically. An ObjectId is exactly 12 bytes long. But those 12 bytes are not random noise. They are packed with information:&lt;/p&gt;

&lt;p&gt;The first 4 bytes are a Unix timestamp representing seconds since 1970.&lt;br&gt;
The next 5 bytes are a random value generated once when the process starts.&lt;br&gt;
The final 3 bytes are an incrementing counter that starts at a random number.&lt;/p&gt;

&lt;p&gt;The most elegant part of this design is the first 4 bytes. Because the Unix timestamp sits at the very beginning in big-endian order (most significant byte first), ObjectIds naturally sort chronologically!&lt;/p&gt;

&lt;p&gt;If you take two ObjectIds and compare them byte by byte from left to right, the one created earlier will almost always sort before the one created later. This gives you time-ordered IDs for free. You can query documents by insertion time or extract the exact creation timestamp of any document directly from its ID without storing a separate &lt;code&gt;created_at&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;The random 5 bytes ensure that two different processes or machines inserting at the exact same second will not generate conflicting IDs. The 3-byte counter ensures that a single process inserting thousands of documents within the same second generates unique IDs for every single one.&lt;/p&gt;

&lt;p&gt;In Rust, generating a new ObjectId with the &lt;code&gt;bson&lt;/code&gt; crate is a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;bson&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple on the surface, but mathematically brilliant underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Documents Live Inside Pages
&lt;/h2&gt;

&lt;p&gt;In the second article, we established that nosqlite organizes its entire file into fixed 4096-byte pages.&lt;/p&gt;

&lt;p&gt;A collection might have five documents, or it might have fifty thousand documents. A single 4096-byte page can hold only a few kilobytes of data. How do we store a collection that spans dozens or hundreds of pages?&lt;/p&gt;

&lt;p&gt;We build a classic computer science data structure directly on disk: a singly linked list.&lt;/p&gt;

&lt;p&gt;The first page of documents in a collection points to the second page. The second page points to the third page. The third page has its pointer set to 0, which signals that it is the end of the chain.&lt;/p&gt;

&lt;p&gt;Here is what the header of every document page looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Document page layout:
// +-----------------------------------+
// | Generic Page Header               |   16 bytes (page number, page type)
// +-----------------------------------+
// | next_page (u64)                   |    8 bytes (pointer to next page in chain)
// +-----------------------------------+
// | document_count (u32)              |    4 bytes (active documents on this page)
// +-----------------------------------+
// | used_space (u32)                  |    4 bytes (bytes written in payload area)
// +-----------------------------------+
// | [records follow here]             |   up to 4064 bytes of document data
// +-----------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here are the offset constants from &lt;code&gt;crates/nosqlite-core/src/document/page.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;NEXT_PAGE_OFFSET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PAGE_HEADER_SIZE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// 16&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;DOCUMENT_COUNT_OFFSET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NEXT_PAGE_OFFSET&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 24&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;USED_SPACE_OFFSET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DOCUMENT_COUNT_OFFSET&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 28&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;DOCUMENT_HEADER_SIZE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;USED_SPACE_OFFSET&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 32&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first 16 bytes are the generic page header.&lt;br&gt;
Bytes 16 to 23 hold &lt;code&gt;next_page&lt;/code&gt;, which is an 8-byte page number pointing to the next document page in the collection.&lt;br&gt;
Bytes 24 to 27 hold &lt;code&gt;document_count&lt;/code&gt;, a 4-byte integer tracking how many active documents live on this specific page.&lt;br&gt;
Bytes 28 to 31 hold &lt;code&gt;used_space&lt;/code&gt;, a 4-byte integer tracking how many bytes of data have been written into the payload area so far.&lt;/p&gt;

&lt;p&gt;Together, the header takes up exactly 32 bytes. Since a page is 4096 bytes, that leaves 4064 bytes of usable space for storing records.&lt;/p&gt;

&lt;p&gt;Notice how important that &lt;code&gt;used_space&lt;/code&gt; field is. When you want to insert a new document, you do not have to scan through the page to find where the free space begins. You just read the 4 bytes of &lt;code&gt;used_space&lt;/code&gt;, add 32, and you immediately have the exact byte offset where the new record should be written. It turns finding the insertion point into an instant O(1) operation.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Anatomy of a Record
&lt;/h2&gt;

&lt;p&gt;Inside that 4064-byte payload region, records are packed one after another.&lt;/p&gt;

&lt;p&gt;Each individual record has a small 5-byte header followed by the raw BSON document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Per-record layout:
// +-------------------------------+
// | deleted_flag (u8)             |    1 byte  (0 = active, 1 = deleted)
// +-------------------------------+
// | bson_size (u32)               |    4 bytes (length of the BSON payload)
// +-------------------------------+
// | bson_bytes...                 |    bson_size bytes of raw document data
// +-------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;RECORD_DELETED_FLAG_SIZE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;RECORD_BSON_SIZE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;RECORD_HEADER_SIZE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RECORD_DELETED_FLAG_SIZE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;RECORD_BSON_SIZE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first byte is the &lt;code&gt;deleted_flag&lt;/code&gt;. If it is 0, the record is alive. If it is 1, the record has been deleted.&lt;/p&gt;

&lt;p&gt;The next 4 bytes store &lt;code&gt;bson_size&lt;/code&gt; as a little-endian unsigned 32-bit integer. This tells the reader exactly how many bytes to read for this document.&lt;/p&gt;

&lt;p&gt;Why do we need &lt;code&gt;bson_size&lt;/code&gt; right in the record header?&lt;/p&gt;

&lt;p&gt;Because documents in a NoSQL database have variable lengths. One document might be 40 bytes. The next document might be 600 bytes. Without &lt;code&gt;bson_size&lt;/code&gt;, the reader would have no idea where the current document ends and where the next record begins. Furthermore, if a record is marked as deleted, the reader can look at &lt;code&gt;bson_size&lt;/code&gt;, skip forward by that exact number of bytes, and arrive at the next record header without wasting CPU cycles decoding dead BSON data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Insertion Pipeline: From Shell to Disk
&lt;/h2&gt;

&lt;p&gt;Let us trace the complete path of an insertion. You sit at your terminal and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;"Priyanshu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what happens inside nosqlite, step by step.&lt;/p&gt;

&lt;p&gt;First, the shell parses the command. It extracts the collection name ("users") and the raw JSON text. It passes that JSON text to a conversion function that parses it into a &lt;code&gt;bson::Document&lt;/code&gt; in memory.&lt;/p&gt;

&lt;p&gt;Second, the engine inspects the document. Does it have an &lt;code&gt;_id&lt;/code&gt; field? No. So the engine calls &lt;code&gt;ObjectId::new()&lt;/code&gt;, generates a fresh 12-byte identifier, and inserts it into the document under the key &lt;code&gt;_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Third, the engine serializes the document into binary BSON bytes using our codec helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;encode_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="nf"&gt;.to_writer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BsonEncode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The method &lt;code&gt;to_writer&lt;/code&gt; turns the in-memory document into a compact slice of bytes. Let us say this document serializes to 45 bytes.&lt;/p&gt;

&lt;p&gt;Fourth, the engine looks up the "users" collection in the catalog. It reads the catalog entry and checks &lt;code&gt;first_document_page&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;first_document_page&lt;/code&gt; is 0, this collection has never had a document before. The engine calls the pager to allocate a brand new page from disk. Let us say the pager allocates page 2. The engine initializes page 2 as a &lt;code&gt;PageType::CollectionData&lt;/code&gt; page, sets &lt;code&gt;next_page = 0&lt;/code&gt;, sets &lt;code&gt;document_count = 0&lt;/code&gt;, and sets &lt;code&gt;used_space = 0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fifth, the engine checks if there is enough space on page 2 for the new record.&lt;/p&gt;

&lt;p&gt;The math is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;available_space = PAGE_SIZE - DOCUMENT_HEADER_SIZE - used_space
required_space  = RECORD_HEADER_SIZE + bson_bytes.len()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plugging in our numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;available_space = 4096 - 32 - 0 = 4064 bytes
required_space  = 5 + 45 = 50 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have 4064 bytes available and only need 50. There is plenty of space.&lt;/p&gt;

&lt;p&gt;Sixth, the engine writes the record into the page buffer starting at offset &lt;code&gt;32 + used_space&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Write active flag&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="py"&gt;.data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Write BSON size as little-endian u32&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bson_bytes&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="py"&gt;.data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.copy_from_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="nf"&gt;.to_le_bytes&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Write raw BSON bytes&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="py"&gt;.data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;bson_bytes&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;&lt;span class="nf"&gt;.copy_from_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;bson_bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seventh, the engine updates the page bookkeeping. &lt;code&gt;document_count&lt;/code&gt; becomes 1. &lt;code&gt;used_space&lt;/code&gt; increases by 50 bytes. The page is marked dirty and written back to disk.&lt;/p&gt;

&lt;p&gt;Finally, because this was the very first document for the "users" collection, the engine updates the catalog entry on page 1 so that &lt;code&gt;first_document_page&lt;/code&gt; points to page 2, and updates the catalog's document count.&lt;/p&gt;

&lt;p&gt;What happens if a page does not have enough space for the new record?&lt;/p&gt;

&lt;p&gt;The engine allocates a new page, updates the existing page's &lt;code&gt;next_page&lt;/code&gt; field to point to the new page number, writes the existing page to disk, and then writes the new record onto the newly allocated page. The on-disk linked list grows by one node.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading Data: The Honest Linear Scan
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;find users&lt;/code&gt; in the shell, the engine reverses that pipeline.&lt;/p&gt;

&lt;p&gt;It reads the catalog to find &lt;code&gt;first_document_page&lt;/code&gt;. It loads that page into memory. It sets a cursor to offset 32 (where the first record starts) and walks forward through the payload.&lt;/p&gt;

&lt;p&gt;For every record, it reads the 1-byte deleted flag. If the flag is 0, it reads the 4-byte &lt;code&gt;bson_size&lt;/code&gt;, slices out the BSON bytes, and decodes them into a document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;decode_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_reader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BsonDecode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shell takes that document, converts it to readable JSON, and displays it in the terminal.&lt;/p&gt;

&lt;p&gt;The cursor advances by &lt;code&gt;5 + bson_size&lt;/code&gt;. When the cursor reaches the end of the used space on the page, the engine checks &lt;code&gt;next_page&lt;/code&gt;. If &lt;code&gt;next_page&lt;/code&gt; is not 0, it loads the next page in the chain and repeats the process. When &lt;code&gt;next_page&lt;/code&gt; is 0, the scan is complete.&lt;/p&gt;

&lt;p&gt;This is a linear scan. Every record on every page in the collection is inspected.&lt;/p&gt;

&lt;p&gt;There is no B-tree index. There is no hash map shortcut. If you have ten documents, it is instantaneous. If you have ten million documents, it will take several seconds.&lt;/p&gt;

&lt;p&gt;And I love that this is how it works right now. Because when you build a database from scratch, starting with an honest linear scan teaches you why indexes exist in the first place. An index is not magic; it is simply a separate data structure built to avoid this exact linear scan. We will get to indexes in a later article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Soft Deletion and the Concept of Tombstones
&lt;/h2&gt;

&lt;p&gt;When someone runs &lt;code&gt;delete users &amp;lt;id&amp;gt;&lt;/code&gt;, what should the database do?&lt;/p&gt;

&lt;p&gt;If you were working with a &lt;code&gt;Vec&amp;lt;u8&amp;gt;&lt;/code&gt; in RAM, your first thought might be to delete the bytes and slide everything after it to the left, just like we did in the catalog.&lt;/p&gt;

&lt;p&gt;But on a data page, that is a terrible idea.&lt;/p&gt;

&lt;p&gt;First, performance. If a document page holds forty documents, and you delete the second document, you would have to shift thousands of bytes of memory to the left, update the offsets, and recalculate &lt;code&gt;used_space&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Second, crash safety. What happens if the program crashes or the power cuts out right in the middle of sliding those three thousand bytes? Half your page is shifted, half is unshifted, and your database file is irreparably corrupted.&lt;/p&gt;

&lt;p&gt;Third, concurrency. If a background query is reading documents while a write operation shifts memory beneath its feet, the reader will read scrambled garbage.&lt;/p&gt;

&lt;p&gt;Instead, nosqlite uses soft deletion, often called tombstoning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Mark the record as deleted&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="py"&gt;.data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;record_offset&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To delete a document, we write a single byte with the value 1 into the record's &lt;code&gt;deleted_flag&lt;/code&gt;. We decrement the page's &lt;code&gt;document_count&lt;/code&gt;, decrement the catalog's document count, and flush the page.&lt;/p&gt;

&lt;p&gt;That is it. The actual BSON document bytes remain on disk. They do not move. But the next time a reader scans the page, it sees &lt;code&gt;deleted_flag == 1&lt;/code&gt;, notes the &lt;code&gt;bson_size&lt;/code&gt;, skips right over the dead bytes, and moves to the next record.&lt;/p&gt;

&lt;p&gt;The trade-off is fragmentation. If you insert a hundred documents and delete ninety of them, those ninety dead records still consume space on disk. The page's &lt;code&gt;used_space&lt;/code&gt; counter does not decrease.&lt;/p&gt;

&lt;p&gt;In production databases, this is solved by a process called compaction, where the database rewrites pages in the background, copying only live records to fresh pages and reclaiming the free space. Compaction is on the roadmap for nosqlite, but for a learning project, soft deletion is the cleanest, safest, and most robust starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Built
&lt;/h2&gt;

&lt;p&gt;Take a step back and look at what we have now.&lt;/p&gt;

&lt;p&gt;We started in Article 1 with an idea: build a single-file document database in Rust.&lt;br&gt;
In Article 2, we formatted a file with 4096-byte pages and magic bytes.&lt;br&gt;
In Article 3, we built a catalog page to track collections and added crash-safe locking.&lt;br&gt;
And now, in Article 4, we have a fully functional document storage pipeline.&lt;/p&gt;

&lt;p&gt;You can open nosqlite, create collections, insert arbitrary JSON documents, have them stored as structured binary BSON on disk, scan them back out, and safely delete them using tombstones. If you close the shell and turn off your computer, the data will still be there tomorrow morning.&lt;/p&gt;

&lt;p&gt;It is a real database.&lt;/p&gt;

&lt;p&gt;But right now, it only exists as a Rust library and a command-line tool. If someone wants to build a web backend in Go, a data science pipeline in Python, or a mobile app in Flutter, they cannot use nosqlite. Rust code cannot simply be imported into another language.&lt;/p&gt;

&lt;p&gt;To make nosqlite accessible to the rest of the programming world, we need a bridge. We need an interface that every programming language on earth understands: the C Application Binary Interface.&lt;/p&gt;

&lt;p&gt;In the next article, we dive into the FFI layer. We will look at how to expose Rust structs as opaque C pointers, how to pass strings and error codes across language boundaries safely, and how to stop Rust panics from crashing a foreign host program.&lt;/p&gt;

</description>
      <category>database</category>
      <category>nosqlite</category>
      <category>dsa</category>
      <category>learning</category>
    </item>
    <item>
      <title>Collections and the Catalog: How a Database Remembers What Exists</title>
      <dc:creator>Priyanshu Verma</dc:creator>
      <pubDate>Thu, 22 Oct 2026 01:51:14 +0000</pubDate>
      <link>https://dev.to/priyanshuverma/collections-and-the-catalog-how-a-database-remembers-what-exists-5e6e</link>
      <guid>https://dev.to/priyanshuverma/collections-and-the-catalog-how-a-database-remembers-what-exists-5e6e</guid>
      <description>&lt;p&gt;&lt;em&gt;Series: nosqlite devlog, Part 3&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once I had page zero written to disk with those magic bytes &lt;code&gt;NOSQLITE_DB___P8&lt;/code&gt;, I had a brief moment of satisfaction. I opened a hex editor, saw my custom signature staring back at me, and felt like I was actually getting somewhere.&lt;/p&gt;

&lt;p&gt;Then reality set in.&lt;/p&gt;

&lt;p&gt;I had an empty file with a header and absolutely no way to store anything useful yet. If you open a database, what is the very first thing you do? You create a collection. In SQL, you write &lt;code&gt;CREATE TABLE&lt;/code&gt;. In MongoDB, you just start writing to a collection or create one explicitly. But what actually is a collection when you are sitting in front of a raw binary file?&lt;/p&gt;

&lt;p&gt;It is not a folder on your hard drive. It is not some magical in-memory table. At the byte level, a collection is just a string name tied to a starting page number somewhere inside that single file.&lt;/p&gt;

&lt;p&gt;My first lazy thought when planning this was tempting: why not just keep a &lt;code&gt;HashMap&amp;lt;String, Vec&amp;lt;Document&amp;gt;&amp;gt;&lt;/code&gt; in memory and dump it to disk as a JSON blob when the program exits?&lt;/p&gt;

&lt;p&gt;That would have taken an hour to write. But that is what you do if you are building a toy in Python or Node, and it completely misses the point of this experiment. If the power cuts out, your in-memory map is gone. If you copy the database file to another computer, that catalog has to travel with it inside the exact same file. The file has to be completely self-contained. It has to know its own collections, where they live, and how many documents they hold.&lt;/p&gt;

&lt;p&gt;That meant building a catalog page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Table of Contents for Your Database
&lt;/h2&gt;

&lt;p&gt;Think about a book. Before you read chapter three, you flip to the table of contents. The table of contents does not contain the story. It just tells you: Chapter 3 starts on page 45.&lt;/p&gt;

&lt;p&gt;The catalog in nosqlite is literally just that table of contents.&lt;/p&gt;

&lt;p&gt;For every collection you create, the engine needs to remember exactly three things:&lt;br&gt;
What is the collection's name?&lt;br&gt;
What page number does its first document live on?&lt;br&gt;
How many documents are currently inside it?&lt;/p&gt;

&lt;p&gt;That is all. If you know those three things, you can find the data, display counts in the shell, and know where to start scanning.&lt;/p&gt;

&lt;p&gt;Where does this table of contents live? It gets its own dedicated page in the file. Specifically, it lives on page 1, sitting right after the file header. If you remember from the previous article, the file header on page 0 stores a field called &lt;code&gt;catalog_page&lt;/code&gt; that holds the value 1. When the database boots up, it reads page 0, checks &lt;code&gt;catalog_page&lt;/code&gt;, and immediately loads page 1 into memory.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Anatomy of the Catalog Page
&lt;/h2&gt;

&lt;p&gt;Let us look at how page 1 is laid out in memory and on disk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Catalog page layout:
//
// +-------------------------------+
// | Page Header                   |   16 bytes (generic header on every page)
// +-------------------------------+
// | collection_count (u32)        |    4 bytes (how many collections exist)
// +-------------------------------+
// | entries...                    |   packed one after another
// +-------------------------------+
//
// Each entry looks like this:
//
// +-------------------------------+
// | collection_name (\0)          |   variable length, null-terminated
// +-------------------------------+
// | first_document_page (u64)     |    8 bytes
// +-------------------------------+
// | document_count (u64)          |    8 bytes
// +-------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first 16 bytes belong to the generic page header that every single page in nosqlite has. That header records the page number, the page type (in this case, &lt;code&gt;PageType::CollectionCatalog&lt;/code&gt;), and a couple of reserved bytes.&lt;/p&gt;

&lt;p&gt;Right after those 16 bytes, we store 4 bytes for &lt;code&gt;collection_count&lt;/code&gt;. This is an unsigned 32-bit integer that tracks how many collections currently exist in the database.&lt;/p&gt;

&lt;p&gt;Why do we need this count? Because without it, a loop reading through the file would have no idea when to stop. It would keep reading random bytes until it crashed or hit corrupted memory. With &lt;code&gt;collection_count&lt;/code&gt;, the reader knows upfront: there are 3 collections here, so read exactly 3 entries and then stop.&lt;/p&gt;

&lt;p&gt;The catalog header size is therefore 20 bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CATALOG_HEADER_SIZE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PAGE_HEADER_SIZE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 16 + 4 = 20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since a page is 4096 bytes total, that leaves 4076 bytes for collection entries. In an embedded database where you usually have five, ten, or maybe thirty collections, 4076 bytes is plenty of room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Null Terminators and C Strings
&lt;/h2&gt;

&lt;p&gt;Here is the struct that represents a collection entry in Rust:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Clone,&lt;/span&gt; &lt;span class="nd"&gt;Serialize,&lt;/span&gt; &lt;span class="nd"&gt;Deserialize)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;CatalogEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;first_document_page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;document_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Rust, this looks clean and friendly. But when this struct gets serialized into raw bytes on disk, we have to make an important choice: how do we store the string name?&lt;/p&gt;

&lt;p&gt;In modern high-level languages, a string always carries its length with it. But at the raw binary level, there are two classic approaches. You can either use a Pascal string, where you write a length byte first and then the characters, or you can use a null-terminated C string, where you write the characters and end with a literal zero byte (&lt;code&gt;\0&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;I chose the null-terminated C string approach.&lt;/p&gt;

&lt;p&gt;When you create a collection named "users", the engine writes the ASCII bytes for &lt;code&gt;u&lt;/code&gt;, &lt;code&gt;s&lt;/code&gt;, &lt;code&gt;e&lt;/code&gt;, &lt;code&gt;r&lt;/code&gt;, &lt;code&gt;s&lt;/code&gt;, followed by &lt;code&gt;0x00&lt;/code&gt;. In hex, that is &lt;code&gt;75 73 65 72 73 00&lt;/code&gt;. That zero byte acts as a red light for the reader. The reader walks forward byte by byte, and the instant it sees a zero, it knows the name is finished.&lt;/p&gt;

&lt;p&gt;Right after that zero byte, we write 8 bytes for &lt;code&gt;first_document_page&lt;/code&gt; and 8 bytes for &lt;code&gt;document_count&lt;/code&gt;, both encoded as little-endian unsigned 64-bit integers.&lt;/p&gt;

&lt;p&gt;This brings up a fascinating detail about variable-length binary layouts. Because collection names can have different lengths, the entries do not have a fixed size. A collection named "users" takes 6 bytes for the name plus 16 bytes for the two u64 numbers, totaling 22 bytes. A collection named "audit_logs_v2" takes 14 bytes for the name plus 16 bytes for the numbers, totaling 30 bytes.&lt;/p&gt;

&lt;p&gt;Because of this, you cannot simply calculate an offset with math like &lt;code&gt;index * entry_size&lt;/code&gt;. You cannot jump directly to the fifth collection. You have to start at byte 20 and walk forward with a cursor, reading each entry to know where the next one begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Catalog Struct Is Completely Empty
&lt;/h2&gt;

&lt;p&gt;When you look at the source code in &lt;code&gt;crates/nosqlite-core/src/catalog/catalog.rs&lt;/code&gt;, you find something that might look bizarre at first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Catalog&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The struct has no fields. It holds no data in memory. It is a zero-sized type.&lt;/p&gt;

&lt;p&gt;Every single method on &lt;code&gt;Catalog&lt;/code&gt; takes a reference to a &lt;code&gt;Page&lt;/code&gt; or &lt;code&gt;Pager&lt;/code&gt; and acts directly on the byte buffer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Catalog&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="nf"&gt;.set_page_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;PageType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CollectionCatalog&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// collection_count = 0&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="py"&gt;.data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PAGE_HEADER_SIZE&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;PAGE_HEADER_SIZE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="nf"&gt;.copy_from_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="mi"&gt;0u32&lt;/span&gt;&lt;span class="nf"&gt;.to_le_bytes&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let us look closely at what &lt;code&gt;0u32.to_le_bytes()&lt;/code&gt; is doing here, because this is fundamental to how systems programming works.&lt;/p&gt;

&lt;p&gt;The number 0 as an unsigned 32-bit integer occupies 4 bytes. The method &lt;code&gt;to_le_bytes()&lt;/code&gt; converts that number into a 4-byte array in little-endian order, where the least significant byte comes first. For zero, that array is just &lt;code&gt;[0, 0, 0, 0]&lt;/code&gt;. If the count were 1, it would be &lt;code&gt;[1, 0, 0, 0]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The call to &lt;code&gt;copy_from_slice&lt;/code&gt; takes those 4 bytes and copies them directly into &lt;code&gt;page.data&lt;/code&gt; starting at offset 16. That is how the collection count is set to zero when the database is first formatted.&lt;/p&gt;

&lt;p&gt;Why did I make &lt;code&gt;Catalog&lt;/code&gt; stateless instead of storing a &lt;code&gt;Vec&amp;lt;CatalogEntry&amp;gt;&lt;/code&gt; inside it?&lt;/p&gt;

&lt;p&gt;Because holding an in-memory copy of the catalog creates two sources of truth. What happens if your in-memory list says there are three collections, but a disk write failed and the file only has two? Now your program is in an inconsistent state, and debugging that will make you question your career choices.&lt;/p&gt;

&lt;p&gt;By making &lt;code&gt;Catalog&lt;/code&gt; stateless, the page itself is always the single source of truth. Every time we need to create, delete, or list collections, we read the page, do the work, and flush the page. There is zero risk of in-memory cache drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Collection: The Journey of a Write
&lt;/h2&gt;

&lt;p&gt;When you type &lt;code&gt;create users&lt;/code&gt; in the shell, here is the exact journey that command takes through the engine.&lt;/p&gt;

&lt;p&gt;First, the engine calls the pager to load the catalog page into memory. The pager gives back a 4096-byte mutable slice of memory representing page 1.&lt;/p&gt;

&lt;p&gt;The engine reads the first 4 bytes after the page header to find out how many collections already exist. Let us say &lt;code&gt;collection_count&lt;/code&gt; is 2.&lt;/p&gt;

&lt;p&gt;Next, it has to make sure we are not creating a duplicate. It sets a cursor variable to offset 20 (the start of the entries). It loops twice, reading each existing collection name using a helper function called &lt;code&gt;read_cstring&lt;/code&gt;. If any name matches "users", it stops immediately and returns an error. We do not allow duplicate collections.&lt;/p&gt;

&lt;p&gt;If no duplicate is found, the cursor is now pointing to the exact byte where the previous entry ended. That is our insertion point.&lt;/p&gt;

&lt;p&gt;The engine checks whether there is enough space left on page 1 for the new entry. The new entry needs 6 bytes for "users\0", 8 bytes for the page pointer, and 8 bytes for the document count, which is 22 bytes. If the page has enough room, it writes the string bytes, writes 8 bytes of zero for &lt;code&gt;first_document_page&lt;/code&gt; (since a new collection has no documents yet), and writes 8 bytes of zero for &lt;code&gt;document_count&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then it goes back to offset 16, increments the &lt;code&gt;collection_count&lt;/code&gt; from 2 to 3, and writes the new count back to the page buffer.&lt;/p&gt;

&lt;p&gt;Finally, the pager marks the page as dirty and flushes it to disk.&lt;/p&gt;

&lt;p&gt;Notice how simple and mechanical that is. There is no query planner, no schema validator, no background worker. It is just reading a small buffer, checking for duplicates, appending 22 bytes, incrementing a counter, and writing the page back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Listing Collections with a Cursor
&lt;/h2&gt;

&lt;p&gt;Reading the list of collections reverses that entire process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;Pager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CatalogEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pager&lt;/span&gt;&lt;span class="nf"&gt;.read_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pager&lt;/span&gt;&lt;span class="py"&gt;.header.catalog_page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;collection_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CATALOG_HEADER_SIZE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read_cstring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="py"&gt;.data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;first_document_page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read_u64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="py"&gt;.data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;document_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read_u64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="py"&gt;.data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CatalogEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;first_document_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;document_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cursor pattern is one of the most useful patterns in binary parsing. A cursor is simply a &lt;code&gt;usize&lt;/code&gt; variable that keeps track of where we are currently reading in the byte slice.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;read_cstring(&amp;amp;page.data, &amp;amp;mut cursor)&lt;/code&gt; runs, it starts at &lt;code&gt;cursor&lt;/code&gt;, reads characters until it hits &lt;code&gt;0x00&lt;/code&gt;, constructs a Rust &lt;code&gt;String&lt;/code&gt;, and advances &lt;code&gt;cursor&lt;/code&gt; past the zero byte.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;read_u64&lt;/code&gt; reads the next 8 bytes as a little-endian number and advances &lt;code&gt;cursor&lt;/code&gt; by 8. Then it does it again for &lt;code&gt;document_count&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After looping &lt;code&gt;count&lt;/code&gt; times, the function returns a clean &lt;code&gt;Vec&amp;lt;CatalogEntry&amp;gt;&lt;/code&gt; ready for the shell to print.&lt;/p&gt;

&lt;p&gt;Now, someone who took a computer science algorithms class might look at this and say: wait, that is an O(n) linear scan. Why didn't you build a B-tree or a hash map for the catalog?&lt;/p&gt;

&lt;p&gt;Because context matters. The entire catalog page is 4096 bytes. That is 4 kilobytes. A modern CPU can scan 4KB of contiguous memory in its L1 cache in a fraction of a microsecond. If you have 20 collections, an O(n) linear scan takes virtually zero time. Building a complex B-tree just to store 20 collection names would be textbook over-engineering. Never pay for complexity you do not actually need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deleting a Collection: The Shift-Left Trick
&lt;/h2&gt;

&lt;p&gt;Deleting a collection is where the byte manipulation gets interesting.&lt;/p&gt;

&lt;p&gt;When someone deletes a collection, you cannot simply write zeros over its entry and leave a hole in the middle of the page. If the reader encounters a gap of zeros, it will either think the collection name is an empty string or get confused about where the next entry begins. The entries must always remain packed tightly together.&lt;/p&gt;

&lt;p&gt;To solve this, I used a shift-left strategy.&lt;/p&gt;

&lt;p&gt;Imagine a row of physical books sitting on a shelf. If you remove the book in the middle, you get an empty gap. What do you do? You slide all the books to the right of that gap one position to the left. The gap disappears, and all the books are neatly packed again.&lt;/p&gt;

&lt;p&gt;In Rust, the standard library provides a method called &lt;code&gt;copy_within&lt;/code&gt; on slices that does exactly this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="py"&gt;.data&lt;/span&gt;&lt;span class="nf"&gt;.copy_within&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry_end&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;used_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry_start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;copy_within&lt;/code&gt; takes a source range of bytes and copies them to a new destination offset inside the exact same slice. It safely handles overlapping memory regions, which is crucial because when you slide bytes to the left, the source and destination overlap.&lt;/p&gt;

&lt;p&gt;After sliding the bytes left, we decrement &lt;code&gt;collection_count&lt;/code&gt; by 1.&lt;/p&gt;

&lt;p&gt;What happens to the leftover bytes at the far right of the entries region? Nothing! We do not even need to zero them out. Because &lt;code&gt;collection_count&lt;/code&gt; is decremented, the reader will stop reading earlier and will never look at those leftover bytes. They will simply be overwritten the next time a new collection is created.&lt;/p&gt;

&lt;p&gt;When a collection is deleted, we also free any document pages that belonged to it so they can be reused later. We will see how page reuse works when we talk about the free-page list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing Catastrophe with a Lock File
&lt;/h2&gt;

&lt;p&gt;Above the &lt;code&gt;Catalog&lt;/code&gt; and the &lt;code&gt;Pager&lt;/code&gt; sits the &lt;code&gt;Database&lt;/code&gt; struct. This is the main entry point that the shell and external applications interact with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Database&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Pager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;lock_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PathBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_lock_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that field named &lt;code&gt;_lock_file&lt;/code&gt; with the leading underscore.&lt;/p&gt;

&lt;p&gt;In Rust, if you declare a variable or a field and never call any methods on it, the compiler will yell at you about an unused variable. The underscore tells the compiler: I know this looks unused, but please leave it alone.&lt;/p&gt;

&lt;p&gt;And leaving it alone is the whole point!&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;Database::open&lt;/code&gt; is called, it creates a small file next to the database with a &lt;code&gt;.lock&lt;/code&gt; extension. It opens that file and stores the open file handle inside &lt;code&gt;_lock_file&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As long as the &lt;code&gt;Database&lt;/code&gt; instance exists in your program, that file handle remains open. When your program finishes or the &lt;code&gt;Database&lt;/code&gt; goes out of scope, Rust drops the struct. Rust's ownership system automatically closes the file handle, and the operating system releases the lock.&lt;/p&gt;

&lt;p&gt;This is the classic RAII pattern, which stands for Resource Acquisition Is Initialization. You acquire the resource on creation, and cleanup happens automatically on destruction. You do not have to remember to call a &lt;code&gt;close()&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;But what happens if the power cord is pulled out of the wall while the database is running?&lt;/p&gt;

&lt;p&gt;If the system crashes violently, the &lt;code&gt;.lock&lt;/code&gt; file remains sitting on the hard drive even though the process is dead. If another program tries to open the database later, it sees the &lt;code&gt;.lock&lt;/code&gt; file and thinks the database is still in use.&lt;/p&gt;

&lt;p&gt;To solve this, nosqlite checks for stale locks on startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="nb"&gt;AsRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lock_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="nf"&gt;.with_extension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"lock"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lock_path&lt;/span&gt;&lt;span class="nf"&gt;.exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;stale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nf"&gt;read_lock_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lock_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;process_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="py"&gt;.pid&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stale&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nd"&gt;eprintln!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Removing stale lock file"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;remove_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lock_path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;DatabaseLocked&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ... proceed with opening&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When nosqlite creates a lock file, it writes the current process ID into it. On startup, if a lock file exists, the engine reads that PID and asks the operating system: is a process with this ID currently running?&lt;/p&gt;

&lt;p&gt;If the answer is no, the previous process died without cleaning up. The lock is stale. nosqlite deletes the file and continues opening normally. If the process is still running, the database is genuinely in use by another instance, and the engine refuses to open to prevent data corruption.&lt;/p&gt;

&lt;p&gt;It is a simple, pragmatic mechanism that prevents two processes from destroying the same database file while handling unexpected crashes gracefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where We Stand
&lt;/h2&gt;

&lt;p&gt;At this point in the project, nosqlite had a solid foundation.&lt;/p&gt;

&lt;p&gt;It could format a file, stamp its magic bytes, maintain a catalog of collections, slide bytes around safely during deletions, and protect the file from concurrent access with process locking.&lt;/p&gt;

&lt;p&gt;You could open the shell, run &lt;code&gt;create users&lt;/code&gt;, run &lt;code&gt;create orders&lt;/code&gt;, and run &lt;code&gt;list&lt;/code&gt;. The database would print:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;users: 0 documents
orders: 0 documents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything worked. But every single collection had &lt;code&gt;first_document_page&lt;/code&gt; set to 0.&lt;/p&gt;

&lt;p&gt;It was like having a filing cabinet with neatly labeled folders, but every single folder was completely empty. We had built the entire organizational structure of a database without storing a single byte of actual user data.&lt;/p&gt;

&lt;p&gt;That changes in the next article.&lt;/p&gt;

&lt;p&gt;In Part 4, we finally put real data into those folders. We take a JSON string typed into the terminal, break down why storing raw JSON is a terrible idea, convert it into binary BSON format, allocate document pages, link them together in a chain, and write our very first document to disk.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>computerscience</category>
      <category>database</category>
    </item>
    <item>
      <title>The File Format: Pages, Headers, and Magic Bytes</title>
      <dc:creator>Priyanshu Verma</dc:creator>
      <pubDate>Mon, 19 Oct 2026 15:27:59 +0000</pubDate>
      <link>https://dev.to/priyanshuverma/the-file-format-pages-headers-and-magic-bytes-2k78</link>
      <guid>https://dev.to/priyanshuverma/the-file-format-pages-headers-and-magic-bytes-2k78</guid>
      <description>&lt;p&gt;&lt;em&gt;Series: nosqlite devlog, Part 2&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first article was about the why. This one is where things get concrete.&lt;/p&gt;

&lt;p&gt;I remember the exact moment I opened a hex editor on a SQLite file for the first time. The first 16 bytes said &lt;code&gt;53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 00&lt;/code&gt;. That decodes to &lt;code&gt;SQLite format 3&lt;/code&gt;. A literal string. Right there at byte zero. Before any data, before any tables, before anything useful, the file just tells you what it is.&lt;/p&gt;

&lt;p&gt;I thought that was brilliant. And I knew immediately that nosqlite would need the same thing.&lt;/p&gt;

&lt;p&gt;When you open a nosqlite database for the first time, the file does not exist yet. The code creates it. But it does not just create a blank file and start dumping data. It sets up a structure. A layout. A way of knowing where everything is and what it means.&lt;/p&gt;

&lt;p&gt;This is the file format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Structure Matters
&lt;/h2&gt;

&lt;p&gt;Imagine a file that contains two documents, both stored as raw BSON bytes, one after another. How do you know where the first document ends and the second begins? How do you know if the file is a valid nosqlite database and not a corrupted archive or a random binary blob? How do you find a specific document without reading everything from byte zero?&lt;/p&gt;

&lt;p&gt;You cannot answer any of those questions without structure.&lt;/p&gt;

&lt;p&gt;Every database engine, from SQLite to PostgreSQL to LevelDB, has a defined file format. It is as much a part of the project as the code itself. nosqlite is no different.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Page: The Fundamental Unit
&lt;/h2&gt;

&lt;p&gt;The core abstraction in nosqlite is the page. A page is a fixed-size block of bytes, exactly 4096 bytes. This number is not arbitrary.&lt;/p&gt;

&lt;p&gt;Most modern operating systems use a memory page size of 4096 bytes. When you read a file, the OS reads it in chunks that align to this boundary. If your database reads in 4096-byte units, you are playing along with the OS instead of fighting it. One read operation brings in exactly one page.&lt;/p&gt;

&lt;p&gt;Here is how the page is defined in the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PAGE_SIZE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PAGE_HEADER_SIZE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[repr(u8)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;PageType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Free&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CollectionCatalog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CollectionData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every page has a header that occupies the first 16 bytes. The rest of the page is available for data. The page type tells the engine what kind of data is in this page. Right now there are three types: a free page (not in use), a catalog page (which tracks collections), and a data page (which holds actual documents).&lt;/p&gt;

&lt;p&gt;That enum attribute &lt;code&gt;#[repr(u8)]&lt;/code&gt; is a Rust thing worth understanding. It tells the compiler to represent the enum as a single byte. This matters because this value gets written directly into the file. When you read that byte back, you can determine what kind of page you are looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  The File Header
&lt;/h2&gt;

&lt;p&gt;The very first thing in a nosqlite database file is the file header. It lives in the first 4096 bytes, which is a full page. Even though it does not fill all 4096 bytes, it reserves the entire first page so that page numbering is consistent. Page 0 is always the header.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FILE_HEADER_SIZE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MAGIC_BYTES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;b"NOSQLITE_DB___P8"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Clone)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;FileHeader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;total_pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;free_list_head&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;catalog_page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take a look at &lt;code&gt;MAGIC_BYTES&lt;/code&gt;. This 16-byte sequence is the first thing written into every nosqlite database file. When you open a file, the very first check is: do these first 16 bytes match? If they do not, this is not a nosqlite database, and the engine refuses to proceed.&lt;/p&gt;

&lt;p&gt;This is called a magic number or magic bytes. Every well-designed binary format has one. PNG files start with &lt;code&gt;\x89PNG\r\n\x1a\n&lt;/code&gt;. ZIP files start with &lt;code&gt;PK&lt;/code&gt;. SQLite files start with &lt;code&gt;SQLite format 3&lt;/code&gt;. The magic bytes let any reader quickly identify the format before attempting to parse anything.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;P8&lt;/code&gt; at the end is personal. It is a nod to &lt;code&gt;p8labs&lt;/code&gt;, my own project namespace. Every file this engine creates carries that marker.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Header Tracks
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;FileHeader&lt;/code&gt; struct holds four pieces of information:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;version&lt;/code&gt; is a u32 that identifies the format version. This is future-proofing. If the file format changes in a breaking way, the version number changes too. An older reader can detect that the format is newer than it understands and refuse gracefully.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;total_pages&lt;/code&gt; is a u64 counting how many pages exist in the file right now. Every time a new page is allocated, this counter increments.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;free_list_head&lt;/code&gt; is a u64 holding the page number of the first free page. When you delete a collection or its documents, those pages do not disappear from the file. They go onto a linked list of free pages. Next time you need a new page, you can reuse one from this list instead of growing the file. This is 0 when there are no free pages.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;catalog_page&lt;/code&gt; is a u64 holding the page number of the catalog page. The catalog is where the list of all collections is stored. You need to know where it is to do anything useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Binary Encoding
&lt;/h2&gt;

&lt;p&gt;The file header and every other structure in nosqlite is encoded in little-endian format. Little-endian means the least-significant byte comes first.&lt;/p&gt;

&lt;p&gt;For example, the number 1 as a u32 in little-endian is &lt;code&gt;01 00 00 00&lt;/code&gt;. In big-endian it would be &lt;code&gt;00 00 00 01&lt;/code&gt;. Most consumer hardware (x86, ARM in modern Linux defaults) is little-endian, which is why most database formats choose it.&lt;/p&gt;

&lt;p&gt;The codec module handles reading and writing these values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;read_u32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="o"&gt;..*&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.ok_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Corruption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_le_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="nf"&gt;.try_into&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Corruption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;read_u64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="o"&gt;..*&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.ok_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Corruption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_le_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="nf"&gt;.try_into&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Corruption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cursor is a mutable reference to a position. Each &lt;code&gt;read_*&lt;/code&gt; call advances the cursor by the number of bytes consumed. This pattern makes sequential reading clean. You start the cursor at 0, call &lt;code&gt;read_u16&lt;/code&gt;, then &lt;code&gt;read_u32&lt;/code&gt;, then &lt;code&gt;read_u64&lt;/code&gt;, and the cursor tracks your position through the buffer automatically. No manual offset arithmetic.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ok_or(Error::Corruption)&lt;/code&gt; at the end is the defensive part. If the buffer is shorter than expected, the engine returns a &lt;code&gt;Corruption&lt;/code&gt; error instead of panicking. In a storage engine, unexpected short reads almost always mean the file has been truncated or corrupted. Detecting this rather than crashing is the correct behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pager
&lt;/h2&gt;

&lt;p&gt;The structure that manages all page reads and writes is called the Pager. This is a concept borrowed directly from SQLite's architecture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Pager&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FileHeader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Pager&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="nb"&gt;AsRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;OpenOptions&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;.create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="nf"&gt;.metadata&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;initialize_database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;read_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you call &lt;code&gt;Pager::open&lt;/code&gt;, it either initializes a fresh database or reads the existing header. The check is simple: if the file length is 0, the file is brand new and needs to be set up. Otherwise, read the header and trust that the rest of the file is structured correctly.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;OpenOptions&lt;/code&gt; call is standard Rust file I/O. &lt;code&gt;create(true)&lt;/code&gt; means create the file if it does not exist. &lt;code&gt;read(true)&lt;/code&gt; and &lt;code&gt;write(true)&lt;/code&gt; give the handle both read and write permissions.&lt;/p&gt;

&lt;p&gt;The Pager is the only thing in the codebase that directly interacts with the file. Every other module that needs to read or write pages goes through the Pager. This is important for correctness. Centralizing file access means page reads and writes are consistent. In the future, caching pages in memory before writing them to disk will happen inside the Pager, without touching anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bootstrap: First Database Write
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;initialize_database&lt;/code&gt; runs on a fresh file, it does a few things:&lt;/p&gt;

&lt;p&gt;It writes the magic bytes at byte 0. It writes an initial &lt;code&gt;FileHeader&lt;/code&gt;. It allocates the first real page (page 1) as the catalog page and initializes it. It updates the header to point &lt;code&gt;catalog_page&lt;/code&gt; at 1. Then it flushes everything to disk.&lt;/p&gt;

&lt;p&gt;After that, the file is a valid nosqlite database with no collections and no documents, but with the infrastructure in place to hold them.&lt;/p&gt;

&lt;p&gt;On every subsequent open, the Pager reads the header, verifies the magic bytes, and trusts the structure. Nothing needs to be rebuilt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The File Layout So Far
&lt;/h2&gt;

&lt;p&gt;Here is what the file looks like after initialization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Byte 0       : NOSQLITE_DB___P8   (16 bytes, magic)
Byte 16      : version             (u32, 4 bytes)
Byte 20      : total_pages         (u64, 8 bytes)
Byte 28      : free_list_head      (u64, 8 bytes)
Byte 36      : catalog_page        (u64, 8 bytes)
Byte 44-4095 : padding / reserved
Byte 4096    : Catalog page header (16 bytes)
Byte 4112+   : Catalog page data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the foundation. Every feature built on top of nosqlite reads and writes through this structure.&lt;/p&gt;

&lt;p&gt;In the next article, we look at collections. What is a collection in a NoSQL database? How does the catalog page know what collections exist and where their documents live? What happens at the byte level when you create a collection or delete one?&lt;/p&gt;

&lt;p&gt;That is where the data starts to feel real.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>database</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Building a NoSQL Database in Rust, from Scratch: The Why</title>
      <dc:creator>Priyanshu Verma</dc:creator>
      <pubDate>Sat, 17 Oct 2026 14:27:27 +0000</pubDate>
      <link>https://dev.to/priyanshuverma/building-a-nosql-database-in-rust-from-scratch-the-why-4egn</link>
      <guid>https://dev.to/priyanshuverma/building-a-nosql-database-in-rust-from-scratch-the-why-4egn</guid>
      <description>&lt;p&gt;Series: nosqlite devlog, Part 1&lt;/p&gt;

&lt;p&gt;I do this thing sometimes where I get an idea at the wrong time. Not when I have free hours and a clear head, but at 1 AM, mid-week, while I should be doing something else entirely. The idea lands and refuses to leave.&lt;/p&gt;

&lt;p&gt;The idea this time was simple on the surface: what if I built a database?&lt;/p&gt;

&lt;p&gt;Not a production database. Not the next MongoDB or a distributed cloud thing. Just a small, honest, embedded database. One file, one process, document storage. Something I could actually finish understanding from top to bottom, because I was starting to realize that I had been using databases for years without really knowing what they were doing inside.&lt;/p&gt;

&lt;p&gt;That bothered me.&lt;/p&gt;

&lt;p&gt;So I started nosqlite. An experiment. A learning project. And what you are reading now is me trying to document how I built it, what I learned, and what it taught me about databases, data structures, systems programming, and Rust itself.&lt;/p&gt;

&lt;p&gt;What Are We Actually Building&lt;/p&gt;

&lt;p&gt;The description in the README is honest: "trying to build a small embedded nosql database from scratch in rust."&lt;/p&gt;

&lt;p&gt;The name is a mashup of two real things: NoSQL and SQLite. Not because it is trying to replace either, but because it takes inspiration from both.&lt;/p&gt;

&lt;p&gt;SQLite is one of the most deployed software libraries in human history. Every Android phone has it. Every iOS app probably uses it. Browsers use it. It is a self-contained database that lives in a single file. You do not start a server. You just open a file and start reading and writing. That simplicity is profound when you think about it. The whole database is just a file you can cp, mv, or put on a USB drive.&lt;/p&gt;

&lt;p&gt;NoSQL is a broader idea. It basically means "not relational." Instead of tables with fixed columns, you store documents. A document is just a blob of structured data, something like JSON. The shape can vary per record. You do not declare a schema upfront. This model made a lot of sense for applications where data is fluid and hard to predict.&lt;/p&gt;

&lt;p&gt;nosqlite tries to marry these two ideas. SQLite-like single file simplicity. NoSQL-like document storage.&lt;/p&gt;

&lt;p&gt;That is the goal. And it is entirely an experiment.&lt;/p&gt;

&lt;p&gt;Why I Built This in Rust&lt;/p&gt;

&lt;p&gt;I was already shifting toward Rust as my primary language. I wrote about that decision separately. But this project cemented it.&lt;/p&gt;

&lt;p&gt;Rust is the right tool for something like this because a database engine is fundamentally a systems problem. You are dealing with raw bytes. You are managing a file at the byte level. You are thinking about memory layout, about what happens when a process crashes mid-write, about how data is read back and verified. These are not higher-level concerns.&lt;/p&gt;

&lt;p&gt;In TypeScript or Python, you would reach for a library to handle most of this. In Rust, you can actually go down to the level of reading four bytes at a known offset and interpreting them as a little-endian u32. You can describe the exact memory layout of a page. You can write a function that writes bytes into a buffer at a cursor position and have the type system enforce that you do not mess it up.&lt;/p&gt;

&lt;p&gt;And the borrow checker, which every newcomer to Rust finds frustrating, becomes genuinely valuable when you are building something like this. You cannot accidentally have two mutable references to the same page buffer. The compiler forces you to think about ownership at every step, which turns out to be exactly what you want when you are building a storage engine.&lt;/p&gt;

&lt;p&gt;Understanding What a Database Actually Is&lt;/p&gt;

&lt;p&gt;Most developers, including me before this project, have a mental model of a database that looks something like: "you write a query, rows come back." That is the interface. The internals stay hidden.&lt;/p&gt;

&lt;p&gt;Here is the simpler version of what actually happens.&lt;/p&gt;

&lt;p&gt;A database is ultimately a program that manages a file. The file is structured in a very specific way so that the database can find things quickly, can guarantee that data is not corrupted, and can handle the program crashing at any moment without losing committed data.&lt;/p&gt;

&lt;p&gt;Every database, whether it is SQLite or PostgreSQL, has roughly the same set of problems to solve:&lt;/p&gt;

&lt;p&gt;How do you organize bytes in the file? You cannot just write raw data one record after another and hope for the best. You need structure. Databases use the concept of a page. A page is a fixed-size chunk of the file, usually 4096 bytes, which matches the OS's memory page size. The database reads and writes entire pages at once.&lt;/p&gt;

&lt;p&gt;How do you find records? If every lookup required scanning the entire file, a database with millions of records would be unusably slow. This is where data structures like B-trees come in. B-trees let you find records in logarithmic time. nosqlite does not have a full B-tree index yet, but it is on the roadmap, and I will cover it in a later article.&lt;/p&gt;

&lt;p&gt;How do you handle crashes? If you are in the middle of writing when the power goes out, your data should not become corrupt garbage. Most databases solve this with write-ahead logging, abbreviated WAL. You write your intention to a log first, then perform the actual write. On startup, you replay the log. nosqlite plans to implement WAL, but this is also future territory.&lt;/p&gt;

&lt;p&gt;How do you support multiple users accessing the same database? This is the concurrency problem. nosqlite currently takes the simple path: only one process can open the database at a time, enforced with a lock file.&lt;/p&gt;

&lt;p&gt;The Structure of nosqlite&lt;/p&gt;

&lt;p&gt;The project is a Rust workspace with three crates:&lt;/p&gt;

&lt;p&gt;nosqlite-core is the storage engine. Everything that reads from and writes to the database file lives here. The pager, the page layout, the catalog (which tracks collections), the document storage logic, the binary codec.&lt;/p&gt;

&lt;p&gt;nosqlite-ffi is a C ABI bindings layer. It exposes the core engine through a C-compatible API so that other languages can use it. Go and Python bindings already exist. Dart/Flutter bindings are planned.&lt;/p&gt;

&lt;p&gt;nosqlite-shell is a command-line shell. You can open a database file, create collections, insert documents, query them, and delete them, all from the terminal.&lt;/p&gt;

&lt;p&gt;That layered design matters. The core does not know or care about what calls it. The FFI layer wraps it in a way that any language with a C FFI can use. The shell is just one consumer among many.&lt;/p&gt;

&lt;p&gt;NoSQL Concepts You Need to Know&lt;/p&gt;

&lt;p&gt;Before the next articles get into code, it is worth establishing the vocabulary.&lt;/p&gt;

&lt;p&gt;Database is the top-level container. In nosqlite, it maps to a single file.&lt;/p&gt;

&lt;p&gt;Collection is roughly equivalent to a table in a relational database, but without a fixed schema. In MongoDB, you would call this a collection. In nosqlite, that is the term too.&lt;/p&gt;

&lt;p&gt;Document is a single record within a collection. A document is a structured blob of data. nosqlite stores documents in BSON format, which stands for Binary JSON. BSON is what MongoDB uses under the hood. It is like JSON but binary-encoded, which makes it faster to parse and more efficient to store.&lt;/p&gt;

&lt;p&gt;ObjectId is a unique identifier automatically assigned to every document. It is a 12-byte value that encodes a timestamp, a machine identifier, and a counter. This is also borrowed from MongoDB.&lt;/p&gt;

&lt;p&gt;A typical interaction with nosqlite looks like this through its shell:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;create users&lt;br&gt;
insert users { "name": "Priyanshu", "age": 22 }&lt;br&gt;
find users&lt;br&gt;
[&lt;br&gt;
  { "_id": "...", "name": "Priyanshu", "age": 22 }&lt;br&gt;
]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Simple. Intentionally simple. The focus is on understanding what happens under the hood when you type those commands.&lt;/p&gt;

&lt;p&gt;The Experiment Mindset&lt;/p&gt;

&lt;p&gt;I want to be honest about something. This project is not finished. It probably will not be "finished" in a traditional sense. I am building it as I learn, which means some commits introduce bugs, some commits fix them, and some commits just rename things because I changed my mind about the design. All of that is in the public history.&lt;/p&gt;

&lt;p&gt;I am okay with that. This is not a portfolio piece trying to impress someone. It is a record of me figuring things out in public. I find that approach more useful than studying theory alone. When something breaks and I have to debug it at the byte level, I actually understand it. Reading about it in a book does not give me the same thing.&lt;/p&gt;

&lt;p&gt;Each article in this series follows the same pattern: a feature lands, I write about it. What the problem was, what I built, what it taught me, what I am still not sure about.&lt;/p&gt;

&lt;p&gt;The repository is at &lt;a href="https://github.com/PriyanshuPz/nosqlite" rel="noopener noreferrer"&gt;https://github.com/PriyanshuPz/nosqlite&lt;/a&gt; . The commit history is open. You can see exactly what changed and when.&lt;/p&gt;

&lt;p&gt;Now here is the thing that kept me up when I started this project. I had a blank file. A completely empty .nsql file on disk. And I had to decide: what does byte zero mean? What is the very first thing I write? What does the engine look for when it opens that file tomorrow and needs to trust that it is valid?&lt;/p&gt;

&lt;p&gt;That question sounds simple. It is not. And the answer to it is the entire next article.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>database</category>
      <category>learning</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Read Open Source Code, Python Edition</title>
      <dc:creator>Priyanshu Verma</dc:creator>
      <pubDate>Fri, 19 Jun 2026 05:25:15 +0000</pubDate>
      <link>https://dev.to/priyanshuverma/how-to-read-open-source-code-python-edition-16ia</link>
      <guid>https://dev.to/priyanshuverma/how-to-read-open-source-code-python-edition-16ia</guid>
      <description>&lt;p&gt;Hi,&lt;/p&gt;

&lt;p&gt;Today I was reading an open-source project that I found interesting, and it made me realize that reading a project is not easy. There can be multiple entry points and exits. Some files may exist for build tooling, testing, or configuration and may not contain information that is immediately useful when trying to understand how the project works.&lt;/p&gt;

&lt;p&gt;As a beginner in open source, it is important to know how to read and understand a project before contributing. This helps you build a mental model of the codebase, understand how components interact, and identify which files need to be modified when adding a feature or fixing a bug.&lt;/p&gt;

&lt;p&gt;Let's start with the basics.&lt;/p&gt;

&lt;p&gt;This time we are going to read a Python package. Python projects are generally easier to navigate because they often have a straightforward and descriptive file structure.&lt;/p&gt;

&lt;p&gt;When opening a Python repository, the first thing I look for is the root-level files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
├── README.md
├── pyproject.toml
├── requirements.txt
├── LICENSE
├── tests/
└── some_lib/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The README is often the best place to start. It usually provides an overview of the problem the project is trying to solve, how the library or application is intended to be used, and the main features it offers.&lt;/p&gt;

&lt;p&gt;Most maintainers include usage examples, and these examples often reveal the public API of the package and the common workflows users are expected to follow.&lt;/p&gt;

&lt;p&gt;In many projects, you may also find additional documentation such as contribution guides, development setup instructions, or architecture documents. Spending a few minutes reading these files can provide valuable context and make the source code much easier to understand before diving into the implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Find the Package Directory
&lt;/h3&gt;

&lt;p&gt;The package directory usually has the same name as the library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests/
numpy/
fastapi/
your_package/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This folder contains the actual source code.&lt;br&gt;
If you don't see any folder like these and there is &lt;code&gt;__init__.py&lt;/code&gt; file in root directory that mean you are already in that folder&lt;/p&gt;
&lt;h3&gt;
  
  
  Open &lt;code&gt;__init__.py&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;One of the most useful files for understanding a Python package is &lt;code&gt;__init__.py&lt;/code&gt;.&lt;br&gt;
For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.connection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;InternalConnection&lt;/span&gt;

&lt;span class="n"&gt;__all__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Client&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file often acts as the package's public interface. It tells us which classes and functions the author expects users to interact with.&lt;br&gt;
From this single file we can already discover important modules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;client.py&lt;/li&gt;
&lt;li&gt;models.py
&lt;code&gt;__all__&lt;/code&gt; keyword is a list that tells Python which names should be exported when someone uses:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In this example it will export only &lt;code&gt;Client&lt;/code&gt; and &lt;code&gt;User&lt;/code&gt; it does not import any internal functions.&lt;br&gt;
Without &lt;code&gt;__all__&lt;/code&gt;, Python imports all names that don't start with &lt;code&gt;_&lt;/code&gt;. A common misconception is that &lt;code&gt;__all__&lt;/code&gt; makes things private. It does not. Even if it is exporting only &lt;code&gt;Client&lt;/code&gt; and &lt;code&gt;User&lt;/code&gt; you can still access some other class or function like &lt;code&gt;InternalConnection&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When reading real libraries, think of &lt;code&gt;__all__&lt;/code&gt; as the package author saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"These are the names I officially support. Everything else is internal implementation detail."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Follow the Public API
&lt;/h3&gt;

&lt;p&gt;Instead of reading every file, start with the objects exposed in &lt;code&gt;__init__.py&lt;/code&gt;.&lt;br&gt;
For example, if you see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;go to &lt;code&gt;client.py&lt;/code&gt; and inspect the &lt;code&gt;Client&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Constructor (&lt;code&gt;__init__&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Public methods&lt;/li&gt;
&lt;li&gt;External dependencies&lt;/li&gt;
&lt;li&gt;Internal modules it imports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This often leads you through the project's main execution path.&lt;br&gt;
Sometimes you will see &lt;code&gt;*&lt;/code&gt; in &lt;code&gt;__init__&lt;/code&gt; or any function args. If you know python well you will understand what that mean but as this covers beginner let me explain.&lt;br&gt;
There are 3 types in which you can find &lt;code&gt;*&lt;/code&gt; used:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;using only &lt;code&gt;*&lt;/code&gt; like this example:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In this case we can call this function as &lt;code&gt;greet("Priyanshu", age=18)&lt;/code&gt; but not like this &lt;code&gt;greet("Priyanshu", age=18)&lt;/code&gt; what that mean &lt;code&gt;*&lt;/code&gt; by itself marks that args after that will start as keyword-only arguments. This makes code a lot more readable and bug free. &lt;br&gt;
That is why when we use some big libraries we use like this.  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In this case you will find it like
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here &lt;code&gt;*&lt;/code&gt; means collect extra positional arguments into a tuple. This is used where function don't know how may arguments can be passed so using &lt;code&gt;*args&lt;/code&gt; it collects all into a tuple. You can find it used like this too&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;nums&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  
&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# calls like add(1, 2, 3)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;*&lt;/code&gt; spreads the list into tuple like args giving some convenience. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Last is like this example
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case &lt;code&gt;**&lt;/code&gt; mean key value pair arguments that mean a dictionary can be passed in the way &lt;code&gt;*arg&lt;/code&gt; is for tuple. &lt;br&gt;
and it can be called similarly but with dictionary items spreading.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Priyanshu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# or like this 
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Priyanshu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;both ways are valid and common to use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Following Imports
&lt;/h3&gt;

&lt;p&gt;After understanding the public API, the next thing I usually do is follow imports.&lt;br&gt;
Almost every file imports something from somewhere else. These imports are like roads connecting different parts of the project. If you can follow these roads, you can slowly understand how the whole system works.&lt;/p&gt;

&lt;p&gt;For example, suppose you open &lt;code&gt;client.py&lt;/code&gt; and see something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.database&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Database&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.embeddings&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Embedder&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This immediately tells us that the Client class probably depends on a Database and an Embedder. We don't know exactly what they do yet, but now we have some direction. Instead of reading random files, we can follow these imports and see where they lead.&lt;/p&gt;

&lt;p&gt;As you continue doing this, a picture starts forming in your head. You begin to understand which files are responsible for storing data, which files handle business logic, and which files simply provide utility functions.&lt;/p&gt;

&lt;p&gt;One thing I learned while reading open-source projects is that codebases are not just collections of files. They are graphs. Every import creates a connection between two parts of the system. The more connections you follow, the clearer the architecture becomes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding Object Creation
&lt;/h3&gt;

&lt;p&gt;At some point you will encounter a class that creates other objects inside its constructor.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I see code like this, I immediately open &lt;code&gt;database.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The reason is simple. The Client class is telling us that it depends on a Database object. If we want to understand how the Client works, we also need to understand what the Database is doing.&lt;/p&gt;

&lt;p&gt;This process repeats again and again. One file leads to another file, which leads to another file. Gradually you start discovering the major components of the project and how they communicate with each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding Where Execution Starts
&lt;/h3&gt;

&lt;p&gt;A common question beginners have is: "Where does the project actually start?"&lt;br&gt;
The answer depends on the project, but every application has an entry point somewhere.&lt;br&gt;
Sometimes it is a &lt;code&gt;main.py&lt;/code&gt; file. Sometimes it is a CLI command. Sometimes it is a web server startup script.&lt;/p&gt;

&lt;p&gt;You may come across something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is often a good place to begin tracing execution because it shows what happens when the program starts.&lt;/p&gt;

&lt;p&gt;Libraries are slightly different. Instead of looking for a main function, you usually start from the public API exposed through &lt;code&gt;__init__.py&lt;/code&gt; and follow the path from there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Following a Single Feature
&lt;/h3&gt;

&lt;p&gt;When I first started reading open-source projects, I tried to understand everything at once. That approach never worked.&lt;br&gt;
A better approach is to pick a single feature and follow it from beginning to end.&lt;br&gt;
Suppose the package exposes a search function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of reading every file in the repository, focus on tracing a single feature from start to finish.&lt;/p&gt;

&lt;p&gt;In this example, start by locating the implementation of the &lt;code&gt;search&lt;/code&gt; method and follow its execution flow. As you move through the code, observe which functions are called, how data moves between components, whether a database is involved, if embeddings are generated, and how the results are processed before being returned.&lt;/p&gt;

&lt;p&gt;Following a single execution path naturally reveals the most important parts of the project and how they work together. This approach is much easier to manage than trying to understand the entire codebase at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading Configuration Files
&lt;/h3&gt;

&lt;p&gt;Once I have a rough understanding of the source code, I usually go back and look at configuration files.&lt;br&gt;
One of the most useful files in modern Python projects is &lt;code&gt;pyproject.toml&lt;/code&gt;.&lt;br&gt;
At first glance it may look like a boring configuration file, but it often contains a lot of useful information.&lt;br&gt;
It can tell you the package name, version, dependencies, build system, and sometimes even command line entry points.&lt;br&gt;
If you see dependencies such as FastAPI, SQLAlchemy, or Pydantic, you can already make educated guesses about the architecture of the project before reading more code.&lt;/p&gt;

&lt;p&gt;These clues become surprisingly useful when navigating larger repositories.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Not Ignore Tests
&lt;/h3&gt;

&lt;p&gt;For a long time I completely ignored test files because I thought they were only useful for maintainers.&lt;br&gt;
That was a mistake.&lt;br&gt;
Tests are often some of the easiest files to read in a project because they show how the author expects the library to be used.&lt;br&gt;
A complex implementation might take hundreds of lines to understand, but a test can often explain the same behavior in ten lines.&lt;br&gt;
Many times I have understood a feature faster by reading its tests than by reading the implementation itself.&lt;/p&gt;

&lt;p&gt;So whenever you feel lost, open the test directory and see how the library is being used there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Your Editor as a Navigation Tool
&lt;/h3&gt;

&lt;p&gt;Modern editors make reading code significantly easier.&lt;/p&gt;

&lt;p&gt;When you see a function call, use "Go to Definition". When you see a class name, jump to its implementation. When you want to know where something is used, use "Find References".&lt;br&gt;
These features save an enormous amount of time because you do not have to manually search through dozens of files.&lt;/p&gt;

&lt;p&gt;Over time, navigating a codebase becomes less about scrolling through folders and more about jumping directly between related pieces of code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;Reading code is a skill that improves through repetition.&lt;br&gt;
The first few projects you read will probably feel confusing. There will be unfamiliar files, unfamiliar patterns, and many moments where you have no idea what is happening.&lt;/p&gt;

&lt;p&gt;That is completely normal.&lt;/p&gt;

&lt;p&gt;The goal is not to understand every line of code. The goal is to build enough context to understand how the project works, how data moves through the system, and which files are responsible for a particular feature.&lt;/p&gt;

&lt;p&gt;Once you reach that point, the project starts feeling much smaller. Instead of seeing hundreds of unrelated files, you begin to see a set of connected components working together to solve a problem.&lt;/p&gt;

&lt;p&gt;Reading code also makes you a much better developer. Every project teaches you something new. Sometimes you discover a cleaner way to structure code. Sometimes you learn a pattern or technique you had never seen before. Other times you may realize that a certain implementation can be improved and start thinking about alternative approaches yourself.&lt;/p&gt;

&lt;p&gt;This is one of the biggest benefits of reading open-source projects. You are not only learning how a specific project works, but also how experienced developers think, organize code, and solve problems.&lt;/p&gt;

&lt;p&gt;And that is usually the point where contributing stops feeling intimidating and starts becoming enjoyable. Instead of just consuming code, you start learning from it, questioning it, and eventually improving it.&lt;/p&gt;

&lt;p&gt;AND remember this is Priyanshu Verma &lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>opensource</category>
      <category>learning</category>
    </item>
    <item>
      <title>Why I’m Shifting From TypeScript to Rust This Year</title>
      <dc:creator>Priyanshu Verma</dc:creator>
      <pubDate>Thu, 12 Feb 2026 15:57:59 +0000</pubDate>
      <link>https://dev.to/priyanshuverma/why-im-shifting-from-typescript-to-rust-this-year-3o2e</link>
      <guid>https://dev.to/priyanshuverma/why-im-shifting-from-typescript-to-rust-this-year-3o2e</guid>
      <description>&lt;p&gt;This year I made a decision that feels uncomfortable but necessary. I’m shifting my primary focus from TypeScript to Rust.&lt;/p&gt;

&lt;p&gt;This is not a dramatic rage-quit. It’s not “TypeScript is bad.” It’s not hype chasing. I still respect TypeScript. I still use it. The browser understands JavaScript and nothing else, so obviously TypeScript will always be part of modern development. But relying on it as my core long-term skill no longer feels right.&lt;/p&gt;

&lt;p&gt;This shift is about depth. It’s about leverage. It’s about choosing friction now to gain strength later.&lt;/p&gt;

&lt;p&gt;For the last few years, TypeScript has been comfortable. Extremely productive. You can build full-stack apps quickly. The ecosystem is massive. Documentation is everywhere. You hit a problem, search it, and there are already ten answers. AI tools autocomplete entire features in seconds. It feels like flying.&lt;/p&gt;

&lt;p&gt;But that comfort started bothering me.&lt;/p&gt;

&lt;p&gt;When something becomes that smooth, that frictionless, you have to ask yourself what is actually defensible about your skill.&lt;/p&gt;

&lt;p&gt;The reality is this: AI models are exceptionally good at JavaScript and TypeScript. The training data is enormous. Patterns are repetitive. The runtime is forgiving. Even senior-level TypeScript code can be generated in seconds. That doesn’t make TypeScript useless. It just means the barrier to output is extremely low, for both humans and machines.&lt;/p&gt;

&lt;p&gt;And when a skill becomes low-friction for machines, its long-term defensibility changes.&lt;/p&gt;

&lt;p&gt;This is the uncomfortable part people avoid talking about.&lt;/p&gt;

&lt;p&gt;If you are only building in a language that AI understands better than you do, you are competing with a tool that doesn’t get tired and doesn’t need context switches. That’s not a great long-term strategy.&lt;/p&gt;

&lt;p&gt;So I started thinking differently.&lt;/p&gt;

&lt;p&gt;What if instead of optimizing for speed of building, I optimize for depth of understanding?&lt;/p&gt;

&lt;p&gt;That’s where Rust entered seriously.&lt;/p&gt;

&lt;p&gt;Rust is not just another backend framework. It’s not a “Node alternative.” It’s a systems language. That distinction matters. With Rust you can build web servers, sure. But you can also build operating systems. Game engines. Networking stacks. Embedded firmware. Database engines. Compilers. You can compile to Linux, Windows, macOS, ARM, and even bare-metal. You can run without an operating system at all.&lt;/p&gt;

&lt;p&gt;That changes how you think.&lt;/p&gt;

&lt;p&gt;You are no longer just writing API routes. You are dealing with memory layout, ownership, concurrency models, lifetimes, deterministic behavior. You are closer to the machine. That proximity forces you to understand what is actually happening.&lt;/p&gt;

&lt;p&gt;TypeScript is about developer experience. Rust is about system integrity.&lt;/p&gt;

&lt;p&gt;And I’m starting to prefer system integrity.&lt;/p&gt;

&lt;p&gt;One of the biggest reasons is how Rust handles correctness. The Rust compiler is relentless. People joke about the borrow checker, but it’s not just a technical feature. It’s a philosophy. The compiler does not trust you. Even if you are confident your program will work, Rust will question every edge case.&lt;/p&gt;

&lt;p&gt;And this reminds me of something very simple.&lt;/p&gt;

&lt;p&gt;The Rust compiler is like your mom.&lt;/p&gt;

&lt;p&gt;Imagine you say, “I want to go on a road trip.”&lt;/p&gt;

&lt;p&gt;You are confident. You have planned everything. You know how to drive. You are excited.&lt;/p&gt;

&lt;p&gt;And your mom says, “What if the car crashes?”&lt;/p&gt;

&lt;p&gt;You respond, “That won’t happen.”&lt;/p&gt;

&lt;p&gt;She says, “What if it does?”&lt;/p&gt;

&lt;p&gt;You say, “That’s hypothetical.”&lt;/p&gt;

&lt;p&gt;She says, “Still. What if?”&lt;/p&gt;

&lt;p&gt;That is Rust.&lt;/p&gt;

&lt;p&gt;Even when you are sure everything is fine, Rust forces you to think about the hypothetical crash. What if this reference outlives its owner? What if this task shuts down mid-execution? What if this error isn’t handled? What if this memory is accessed concurrently? What if the network drops? What if the process exits early?&lt;/p&gt;

&lt;p&gt;It doesn’t accept optimism. It demands preparation.&lt;/p&gt;

&lt;p&gt;And that’s exhausting at first.&lt;/p&gt;

&lt;p&gt;Rust is not dopamine-friendly. It does not give you the instant satisfaction that scripting languages do. You don’t just write code and watch it magically run. You fight the compiler. You refactor. You restructure. You rethink ownership. You redesign your data flow. Sometimes you feel like you are losing to a machine.&lt;/p&gt;

&lt;p&gt;But something changes after a while.&lt;/p&gt;

&lt;p&gt;You realize the compiler is not fighting you. It’s training you.&lt;/p&gt;

&lt;p&gt;It forces you to think in systems. It forces you to think about failure before failure happens. It forces you to design intentionally. That kind of discipline is uncomfortable but powerful.&lt;/p&gt;

&lt;p&gt;TypeScript gives you fast iteration. Rust gives you strong guarantees.&lt;/p&gt;

&lt;p&gt;TypeScript gives you flexibility at runtime. Rust gives you certainty at compile time.&lt;/p&gt;

&lt;p&gt;TypeScript gives you speed of shipping. Rust gives you durability of shipping.&lt;/p&gt;

&lt;p&gt;And I started asking myself what I want more of.&lt;/p&gt;

&lt;p&gt;Another major factor is deployment philosophy. I’ve grown tired of runtime stacks layered on runtime stacks. Node behind reverse proxies inside containers inside cloud abstractions. Hundreds of megabytes deployed for something that fundamentally just responds to HTTP requests.&lt;/p&gt;

&lt;p&gt;With Rust, you can compile a backend and embed a compiled frontend into a single binary. No Node runtime. No reverse proxy required. No external dependencies. Copy the binary to a Raspberry Pi and run it. It works. It doesn’t ask what version of something is installed. It doesn’t need npm install. It doesn’t need Docker if you don’t want it.&lt;/p&gt;

&lt;p&gt;That simplicity is addictive.&lt;/p&gt;

&lt;p&gt;It feels honest.&lt;/p&gt;

&lt;p&gt;You build something. You compile it. You ship the artifact. That artifact is the product. There is something clean about that model. One more thing is that it is a binary even if someone gets it. they cannot see the code that we can say is a security feature. &lt;/p&gt;

&lt;p&gt;Now, why not Go?&lt;/p&gt;

&lt;p&gt;Go is good. I respect it. It compiles fast. It has strong concurrency primitives. It’s pragmatic. But for me, personally, it never felt expressive enough. The syntax feels procedural in a way that doesn’t match how I think. I come from an object-oriented background, and while Rust isn’t traditional OOP, its type system feels far more expressive and powerful.&lt;/p&gt;

&lt;p&gt;More importantly, Go isn’t a systems language in the same sense. You cannot compile Go to true bare-metal targets the way you can with Rust. Rust’s reach is broader. That matters to me. I don’t want to just write web servers for the rest of my life. I want the option to go deeper.&lt;/p&gt;

&lt;p&gt;This isn’t about language wars. It’s about surface area of capability.&lt;/p&gt;

&lt;p&gt;Rust expands mine.&lt;/p&gt;

&lt;p&gt;There’s also timing. Rust is modern but still relatively young in mainstream adoption. The market is growing. The ecosystem is maturing. But it’s not saturated. Becoming deeply competent in Rust today feels like investing early in something foundational.&lt;/p&gt;

&lt;p&gt;And beyond market thinking, it feels intellectually honest. Rust makes you slower in the beginning. It removes shortcuts. It punishes careless assumptions. It makes you confront complexity directly instead of hiding it under runtime flexibility.&lt;/p&gt;

&lt;p&gt;It is not beginner friendly. It is not easy. And that’s precisely why it’s valuable.&lt;/p&gt;

&lt;p&gt;If someone wants instant feedback, fast feature velocity, and quick wins, Rust will feel like a nightmare. The compiler errors can be brutal. You might write code that logically makes sense, and Rust will reject it for lifetime reasons you didn’t even consider. It feels unfair at first.&lt;/p&gt;

&lt;p&gt;But over time, you realize it’s making you sharper.&lt;/p&gt;

&lt;p&gt;You start anticipating ownership issues before writing code. You design APIs differently. You think about shutdown signals. You think about memory footprint. You think about concurrency boundaries. You think about determinism.&lt;/p&gt;

&lt;p&gt;You stop assuming things will work. You prove that they will.&lt;/p&gt;

&lt;p&gt;That’s a different mindset.&lt;/p&gt;

&lt;p&gt;I am not abandoning TypeScript. That would be unrealistic. The browser speaks JavaScript. Frontend ecosystems are deeply tied to it. Even if you use WebAssembly, JavaScript is still there. So TypeScript will remain part of my workflow.&lt;/p&gt;

&lt;p&gt;But I don’t want it to be my only layer of competence.&lt;/p&gt;

&lt;p&gt;Relying solely on TypeScript feels narrow. Rust feels expansive.&lt;/p&gt;

&lt;p&gt;This shift is not about rejecting comfort for the sake of pain. It’s about choosing growth over convenience. I know this year will be slower in terms of output. I know I will spend more time reading compiler errors than shipping features. But I also know that the understanding I build will compound.&lt;/p&gt;

&lt;p&gt;In a world where AI can generate boilerplate instantly, the real advantage is not how fast you can scaffold an app. It’s how deeply you understand the systems you are building on.&lt;/p&gt;

&lt;p&gt;Rust pushes you toward that depth whether you like it or not.&lt;/p&gt;

&lt;p&gt;And maybe that’s what I need right now.&lt;/p&gt;

&lt;p&gt;Not speed. Not comfort. Not instant validation.&lt;/p&gt;

&lt;p&gt;Depth.&lt;/p&gt;

&lt;p&gt;That’s why I’m shifting.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>programming</category>
    </item>
    <item>
      <title>Homelabing - Local to online setup suppressing CGNAT</title>
      <dc:creator>Priyanshu Verma</dc:creator>
      <pubDate>Thu, 25 Dec 2025 12:44:45 +0000</pubDate>
      <link>https://dev.to/priyanshuverma/homelabing-local-to-online-setup-suppressing-cgnat-mpd</link>
      <guid>https://dev.to/priyanshuverma/homelabing-local-to-online-setup-suppressing-cgnat-mpd</guid>
      <description>&lt;p&gt;In the journey of creating a Hub before we start programming or building website lets setup Pi in a way where we can access it on mobile internet. &lt;br&gt;
For that we can use Cloudflare Tunnel or Tail Scale, but the problem is both are overpowered for this setup I don't want to use Cloudflare as an intercept between Me and my data. I can't trust any 3rd Party. So, not going to use that. I will not use Tail Scale because some features are awesome. But, it to use it I need to make accounts and every new person joins the network need to create an account on Tail Scale and I don't think. I will create an account on 3rd party to access my self-hosted server.&lt;br&gt;
Now, This leads to setup my own VPN on Pi and connect to devices when I want to connect with Mobile network.&lt;/p&gt;

&lt;p&gt;For That, I will be going to use &lt;a href="https://www.wireguard.com/" rel="noopener noreferrer"&gt;WireGuard&lt;/a&gt;.&lt;br&gt;
Why? Because it is open-source light and free.&lt;br&gt;
Let's set it up.&lt;/p&gt;

&lt;p&gt;Installing WireGuard on Pi.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;wireguard &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And verify by printing it's version&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wg &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, for this to work, we need to create Pi's Private and Public Keys. To create, follow this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/wireguard
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;700 /etc/wireguard
&lt;span class="nb"&gt;sudo &lt;/span&gt;wg genkey | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/wireguard/server_private.key | &lt;span class="nb"&gt;sudo &lt;/span&gt;wg pubkey | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/wireguard/server_public.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, you will find,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;-rw-------&lt;/span&gt; 1 root root 45 server_private.key
&lt;span class="nt"&gt;-rw-------&lt;/span&gt; 1 root root 45 server_public.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;inside &lt;code&gt;/etc/wireguard&lt;/code&gt; if not, then regenerate the keys.&lt;/p&gt;

&lt;p&gt;For me, it worked. Now let's add some config.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/wireguard/wg0.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This config&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Interface]&lt;/span&gt;
&lt;span class="py"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.10.0.1/24&lt;/span&gt;
&lt;span class="py"&gt;ListenPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;51820&lt;/span&gt;
&lt;span class="py"&gt;PrivateKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;PASTE server_private.key CONTENT&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;SaveConfig&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I am using &lt;code&gt;10.10.0.1/24&lt;/code&gt; for subnet, as It is easy to remember for me and will not conflict with anything.&lt;br&gt;
Now I will enable IP forwarding&lt;br&gt;
by adding &lt;code&gt;net.ipv4.ip_forward=1&lt;/code&gt; inside &lt;code&gt;/etc/sysctl.conf&lt;/code&gt; and apply the changes with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should output &lt;code&gt;1&lt;/code&gt;&lt;br&gt;
Now, let's enable wireguard&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;wg-quick@wg0
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start wg-quick@wg0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and verify with &lt;code&gt;sudo wg&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now it is time to install WireGuard Official Android app on mobile and add it as a Peer.&lt;br&gt;
Will create a new tunnel generate public and private keys then on Pi in &lt;code&gt;/etc/wireguard/wg0.conf&lt;/code&gt; will add peer in bottom.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Peer]&lt;/span&gt;
&lt;span class="py"&gt;PublicKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;PHONE_PUBLIC_KEY&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.10.0.2/32&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and reload &lt;code&gt;wg&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;wg-quick down wg0
&lt;span class="nb"&gt;sudo &lt;/span&gt;wg-quick up wg0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;finally on mobile WireGuard will add&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Address &lt;span class="o"&gt;=&lt;/span&gt; 10.10.0.2/32
DNS &lt;span class="o"&gt;=&lt;/span&gt; 192.168.1.1 &lt;span class="c"&gt;# your gateway IP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and add a Peer over there&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;PublicKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;SERVER_PUBLIC_KEY&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;YOUR_PUBLIC_IP&amp;gt;:51820&lt;/span&gt;
&lt;span class="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0/0&lt;/span&gt;
&lt;span class="py"&gt;PersistentKeepalive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now at this point I don't have a static Public IP as my ISP don't know me doing homelabing. If you are able to get static IP then use it otherwise there is one workaround.&lt;br&gt;
I will use &lt;strong&gt;DuckDNS&lt;/strong&gt;, this will help me to mimic my dynamic IP to look like static and on top of it. It is free, Lightweight and No vendor lock-in also it is not a proxy but a DNS record. To hook it go to &lt;a href="https://www.duckdns.org" rel="noopener noreferrer"&gt;https://www.duckdns.org&lt;/a&gt;&lt;br&gt;
create an account and add a domain name it whatever you want, it is not a public address just a link to your public IP. &lt;br&gt;
and use that subdomain as an endpoint.&lt;/p&gt;

&lt;p&gt;We need to make a &lt;code&gt;cron&lt;/code&gt; job to update the latest IP on DDNS. For that on Pi will create a Script to do that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;curl &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/duckdns
nano ~/duckdns/update.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;using this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;echo &lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://www.duckdns.org/update?domains={YOUR_SUBDOMAIN}&amp;amp;token={YOUR_TOKEN}&amp;amp;ip="&lt;/span&gt; | curl &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; ~/duckdns/duck.log &lt;span class="nt"&gt;-K&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update &lt;code&gt;YOUR_TOKEN&lt;/code&gt; from DDNS token and &lt;code&gt;YOUR_SUBDOMAIN&lt;/code&gt; with your own subdomain.&lt;br&gt;
Now give some permissions and test it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ~/duckdns/update.sh
./duckdns/update.sh
&lt;span class="nb"&gt;cat&lt;/span&gt; ~/duckdns/duck.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should give &lt;code&gt;OK&lt;/code&gt;. NOT &lt;code&gt;KO&lt;/code&gt; if it says &lt;code&gt;KO&lt;/code&gt; then you are &lt;code&gt;KO&lt;/code&gt; setup failed check your token and subdomain.&lt;br&gt;
Now let's add a &lt;code&gt;cron&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;crontab &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and adding this to run every 5 minutes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*/5 * * * * ~/duckdns/update.sh &amp;gt;/dev/null 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's try to connect turn VPN on in mobile it should connect and by running&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;wg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will see a device connected saying&lt;br&gt;
&lt;code&gt;latest handshake: X seconds ago&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If not then Hello my friend me too. Our ISP uses &lt;code&gt;CGNAT&lt;/code&gt; (Carrier-Grade Network Address Translation) so they can control the router and as a user we cannot forward and port on IPv4.&lt;/p&gt;

&lt;p&gt;Then, What to do??&lt;br&gt;
You know what, after hours of research, I found that I cannot forward port by any mean, so the last option is to use Tail Scale, and so far one of the requirements broke. But, it is not my mistake, I cannot control my ISB behavior so. I have to use Tail Scale.&lt;/p&gt;

&lt;p&gt;So, we don't need DuckDNS now let's remove the &lt;code&gt;cron&lt;/code&gt; and wireguard.&lt;br&gt;
If you followed along, then you will feel what wasting time looks like.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;crontab &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; ~/duckdns
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop wg-quick@wg0
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl disable wg-quick@wg0
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt purge wireguard wireguard-tools &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt autoremove &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /etc/wireguard

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then remove that port forward line from &lt;code&gt;sudo nano /etc/sysctl.conf&lt;/code&gt;&lt;br&gt;
Now, create an account at &lt;a href="https://login.tailscale.com" rel="noopener noreferrer"&gt;https://login.tailscale.com&lt;/a&gt;&lt;br&gt;
Then add the devices and install it on Pi,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://tailscale.com/install.sh | sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;tailscale up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and done online.&lt;br&gt;
You can check it on tailscale dashboard.&lt;/p&gt;

&lt;p&gt;Now, we can focus on building online Pi is on local and online.&lt;br&gt;
Just run a server running and good to go.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>homelab</category>
      <category>networking</category>
      <category>midnightchallenge</category>
    </item>
    <item>
      <title>Homelabing interesting enough to say but why</title>
      <dc:creator>Priyanshu Verma</dc:creator>
      <pubDate>Thu, 25 Dec 2025 12:06:15 +0000</pubDate>
      <link>https://dev.to/priyanshuverma/homelabing-interesting-enough-to-say-but-why-2fhh</link>
      <guid>https://dev.to/priyanshuverma/homelabing-interesting-enough-to-say-but-why-2fhh</guid>
      <description>&lt;p&gt;So far, I tried to not tie myself to big techs for some basic stuff. Like, from starting I switched products for password management, media, IOT management. But stuck with vendor lock and tied to subscriptions and limited control. &lt;br&gt;
Finally, decided to make a home server. &lt;br&gt;
For me, it is not like buying a big rag, Plenty of storage, RAM, and heavy suite like proxmox or other. I started very small from a Raspberry Zero 2W. Why, because &lt;del&gt;money&lt;/del&gt; I am a &lt;strong&gt;professional&lt;/strong&gt; developer, I know to build a system that works in limited resources. This is not a bug but a feature. I will try to build tech from scratch that I can run on it. For Media and other IOT stuff. &lt;br&gt;
Also, This is only for my use, I am not building a robust system for millions of users using simultaneously, It is for me only or my family.&lt;/p&gt;

&lt;p&gt;This homelabing series is going to be technical and not for production systems. I don't care if I fry my Pi, it is just a learning journey, I might upgrade hardware in the future. We can start with Pi only.&lt;/p&gt;

&lt;p&gt;For more context let me tell you what is my ground for now.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Raspberry Pi Zero 2W Board (&lt;a href="https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/" rel="noopener noreferrer"&gt;ORIGNAL&lt;/a&gt;)

&lt;ul&gt;
&lt;li&gt;1GHz quad-core 64-bit Arm Cortex-A53 CPU&lt;/li&gt;
&lt;li&gt;512MB SDRAM&lt;/li&gt;
&lt;li&gt;2.4GHz 802.11 b/g/n wireless LAN&lt;/li&gt;
&lt;li&gt;Bluetooth 4.2, Bluetooth Low Energy (BLE), onboard antenna&lt;/li&gt;
&lt;li&gt;Mini HDMI® port and micro USB On-The-Go (OTG) port&lt;/li&gt;
&lt;li&gt;microSD card slot&lt;/li&gt;
&lt;li&gt;HAT-compatible 40-pin header footprint (unpopulated)&lt;/li&gt;
&lt;li&gt;Micro USB power&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SAMSUNG POWER Prime 256 GB Micro SDXC Class 10 Memory Card

&lt;ul&gt;
&lt;li&gt;capacity: 256 GB&lt;/li&gt;
&lt;li&gt;Read Speed: 160 MB/s&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Power Adapter normal 2A for Powering Pi&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the ground I have installed Ubuntu 64bit lite on memory card and setup Pi with a static IP from home router using it's Mac Address.&lt;br&gt;
So far, nothing is installed, and I can ssh into Pi from my PC that is the bare minimum. Also, One more thing is There are two partitions I have divided unusable ~235GB of storage in ~200GB + rest. So I can deploy stuff on rest part which is ~35GB and can keep the media and all other stuff in that ~200GB part. Safe and simple. &lt;br&gt;
Also, I have installed Samba and did setup so I can mount big 200GB part to my Windows PC and store files. It is easy no big deal.&lt;/p&gt;

&lt;p&gt;This is the final ground from where I will be making it The way I want, which includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Simple IOT controlling Dashboard for remotely switching lights at home and other fun stuff.&lt;/li&gt;
&lt;li&gt;host simple images and videos downloaded and store, no processing or indexing simple static serving. &lt;/li&gt;
&lt;li&gt;Simple authentication so, I can restrict certain things and add my family members to access media.&lt;/li&gt;
&lt;li&gt;Do it in a way so I can add more stuff in future like internal messaging or calling.
These are basic enough right, remember we will be doing it under usable 416M ram.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I do have some rules that I will follow. I don't want to mess things by using software that I feel is not giving me control and heavy on the system.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NO cloud platform for processing or intercepting traffic like Cloudflare tunnel &lt;/li&gt;
&lt;li&gt;No REACT, for building frontend.&lt;/li&gt;
&lt;li&gt;Keep it accessible on remote location, (aka open to use even I am not connected to home network).&lt;/li&gt;
&lt;li&gt;less latency&lt;/li&gt;
&lt;li&gt;Easy to use for non-techy people.&lt;/li&gt;
&lt;li&gt;SECURED!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sounds crazy, but it is doable.&lt;br&gt;
Let's see how it goes…&lt;/p&gt;

</description>
      <category>learning</category>
      <category>homelab</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>You Want a Future in Tech? Stop Building Templates.</title>
      <dc:creator>Priyanshu Verma</dc:creator>
      <pubDate>Tue, 25 Nov 2025 16:33:30 +0000</pubDate>
      <link>https://dev.to/priyanshuverma/you-want-a-future-in-tech-stop-building-templates-4nil</link>
      <guid>https://dev.to/priyanshuverma/you-want-a-future-in-tech-stop-building-templates-4nil</guid>
      <description>&lt;p&gt;Hi&lt;br&gt;
So, Now everything around us has some sort of AI badge. Even on my Washing Machine. Seriously, Now it recommends me to select the mode based on my route. &lt;br&gt;
Let me give you a brief introduction about me. I am a developer who works major in Web. And with AI now, I am able to make beautiful UIs in seconds and write code build my side projects in Hours. I just need a prompt and some &lt;code&gt;&amp;lt;TAB&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;TAB&amp;gt;&lt;/code&gt;.&lt;br&gt;
Done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I wrote this article
&lt;/h2&gt;

&lt;p&gt;If we see it from managing or productive side then it is awesome. You are now able to do Hours of work in minutes. If you are in cooperate or have a job then It fits in very awesome way. &lt;br&gt;
But, if it is easy then anyone can do it. &lt;br&gt;
I observed that &lt;strong&gt;WEB DEVELOPMENT&lt;/strong&gt; is not a hard thing anymore any one that know what to build can make websites.&lt;br&gt;
Well. Now you say building UI with AI is nothing main thing is managing it, sustaining it. I know that. I am not saying any non tech can build or sustain it. I am talking about the competition. Already the competition in Web Development is very high very other person is build an &lt;strong&gt;AI SaaS&lt;/strong&gt; for nothing. &lt;br&gt;
And I think, Now companies and Startups don't need a lot of people for Web Development and which means less Jobs and less opportunities as most of the work is simple. &lt;/p&gt;

&lt;p&gt;As a student or person who want to get in tech or want to learn something to earn money in long term. The path of learning about Web Development not looks safe anymore. &lt;/p&gt;

&lt;h2&gt;
  
  
  What is the future of Web development
&lt;/h2&gt;

&lt;p&gt;If we see the growth of AI and the tools that are available right now like we have IDEs AI integrated and some generic packages or libraries which works very fine with AI like if We need the world of web and intelligence if AI we can conclude at using tech like&lt;a href="https://ui.shadcn.com/" rel="noopener noreferrer"&gt;shadcn/ui&lt;/a&gt; and &lt;a href="https://www.better-auth.com/" rel="noopener noreferrer"&gt;Better Auth&lt;/a&gt; for authentication also for backend serverless functions, ORMs like &lt;a href="https://www.prisma.io/" rel="noopener noreferrer"&gt;Prisma&lt;/a&gt; helps to build any product very easily Everything is open and works fine with AI. Famous AI models knows about these technologies and they work very effectively on these. &lt;/p&gt;

&lt;p&gt;And in future it will get more better because almost 90% of the code of any website is open and there is a lot of data on which AI models can be trained. Every open-source frameworks, public tutorials, GitHub repos, countless blog posts, StackOverflow questions, boilerplates, templates, and design systems are public by default. And the feedback loop of the web makes things even easier for AI. If you write HTML or React, you instantly see the result. If something breaks, the browser reports the exact line.&lt;/p&gt;

&lt;p&gt;AI eating this domain of tech very rapidly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do learn then
&lt;/h2&gt;

&lt;p&gt;Before answering this question I want to ask why you get in tech. The problem with today's generation is that they think I will get in tech build websites and earn money. You and I we both know that the most easiest job in tech is to build websites and people choose it because it is simple. &lt;br&gt;
The harsh truth is people became lazy and they want instant feedbacks that's why nowadays we don't see any projects like Linux, Git or some revolutionary tech because they are depended on AI. and AI knows Web better so defaults to it. &lt;/p&gt;

&lt;p&gt;So, What to do uh? what should i learn. The answer is very simple do what AI cannot do. Programming is not dead. The humours are faking it. AI is not able to build a native app for windows, android, It is not able to build custom software for specific hardware, it cannot do cyber security. Literary it fails to code in system languages like C, C++, or Rust, Even in kotlin. You know why because the information build about these is less and building a solution for hardware or building an native app or doing cyber security stuff needs on demand actions that AI cannot replicate. Fields like DevOps AI cannot replace as it needs to work on demand according to external stimuli.  &lt;/p&gt;

&lt;h2&gt;
  
  
  What is the point of building native
&lt;/h2&gt;

&lt;p&gt;So, my friend why you should build a mobile app with native tech you can use JavaScript to build it. &lt;br&gt;
Technically it works. It shows a screen, buttons click, cool. But then I’m like… bro look at our devices. We literally have super powerful chips now, like phones with GPUs that are better than old consoles, laptops with crazy CPUs, RAM is cheap, SSD is fast… and still I open 20 chrome tabs and the whole thing starts dying. I run a game on my RTX and it can’t even stay on 60 FPS. Why? Why are games 150GB now? Why is a simple chat app using 700MB RAM?&lt;/p&gt;

&lt;p&gt;And the sad part is we all kinda became lazy. We just accept it. No one wants to build their own game engine because “eh too much work.” No one wants to create their own UI library because “just npm install something.” Everything is off-the-shelf now. Zero originality. Every website looks copy-paste same. Every game feels same. Every app has that same template vibe. We’re not optimizing, we’re just piling more stuff on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can AI really Replace you
&lt;/h2&gt;

&lt;p&gt;See, It depends on your skills.&lt;br&gt;
Honestly… depends on what you’re doing. Like, seriously. If your whole skillset is just copying templates, building basic CRUD, making landing pages, doing the same loops everyone is doing, then yeah AI is on your heels. Because AI is literally trained on that stuff. It has seen millions of those patterns.&lt;/p&gt;

&lt;p&gt;But the funny thing is the stuff I talked about above the messy parts, AI can’t touch that. It just can’t. It doesn’t know how to handle real hardware problems, memory issues, native bugs, performance tuning, cybersecurity attacks, networking glitches, weird OS behavior. That stuff is too real. Too unpredictable. Too out of the training data bubble. Only humans can deal with it.&lt;/p&gt;

&lt;p&gt;And this whole “recession because of AI” panic? Bro, half of it is just people sitting in their comfort zone. They open VS Code, type two prompts into an AI agent, feel like a CEO for a minute, and then scroll reels for half an hour. Then they wonder why they’re not growing and blame AI like it’s hunting them personally. It’s not AI. It’s the fact that we all got a bit too comfortable building comfort zone software.&lt;/p&gt;

&lt;p&gt;AI is not replacing the people who actually touch the real stuff the complex systems, the deep logic, the broken pipelines, the debugging nightmares, the places where you can’t just hallucinate a solution. AI is replacing the lazy, repetitive parts, the factory style coding that didn’t need a human in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;At the end of the day it’s actually not that deep.&lt;br&gt;
You wanna stay irreplaceable?&lt;br&gt;
Just stop doing the bare minimum and calling it a “skill.”&lt;br&gt;
Get out of the comfort zone stuff.&lt;br&gt;
Start touching the parts of tech where AI straight up falls on its face.&lt;br&gt;
Systems, hardware, native, security, actual optimisation is the real engineering side of things.&lt;/p&gt;

&lt;p&gt;That’s literally the whole game.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
    <item>
      <title>Self-Hosting a Password Manager on a Pi Zero 2W</title>
      <dc:creator>Priyanshu Verma</dc:creator>
      <pubDate>Sun, 28 Sep 2025 06:37:37 +0000</pubDate>
      <link>https://dev.to/priyanshuverma/self-hosting-a-password-manager-on-a-pi-zero-2w-1c79</link>
      <guid>https://dev.to/priyanshuverma/self-hosting-a-password-manager-on-a-pi-zero-2w-1c79</guid>
      <description>&lt;p&gt;As a developer and tinkerer, I love taking control of my digital environment. This weekend, I decided to host my own password manager. My goal was simple: &lt;strong&gt;keep all my sensitive data private&lt;/strong&gt;, accessible from multiple devices, while learning more about self-hosting, TLS, reverse proxies, and Docker on a low-power device. Here’s how the journey unfolded, along with problems I faced and solutions I discovered.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Choosing the Right Tool&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I started by exploring the options. Cloud-based managers like 1Password or Bitwarden are convenient, but I wanted full control. Among self-hosted options, &lt;strong&gt;Vaultwarden&lt;/strong&gt; stood out. It’s a lightweight, unofficial Bitwarden implementation, supports TOTP, and can run on low-resource devices like a Pi Zero 2W.&lt;/p&gt;

&lt;p&gt;For a solo developer like me, Vaultwarden was ideal: small footprint, actively maintained, and feature-rich.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 1: Preparing the Pi&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I installed &lt;strong&gt;Ubuntu&lt;/strong&gt; on the Pi Zero 2W and set up SSH access to manage it remotely. Since I planned to use Docker, I made sure the system was updated and installed &lt;code&gt;docker&lt;/code&gt; and &lt;code&gt;docker-compose&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After verifying Docker installation with &lt;code&gt;docker --version&lt;/code&gt;, I was ready to pull Vaultwarden.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 2: Running Vaultwarden in Docker&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;My first attempt to run Vaultwarden showed an important warning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No persistent volume!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This meant that if the container stopped or was updated, all data would be lost. Lesson one: &lt;strong&gt;always map persistent volumes&lt;/strong&gt;. I created a &lt;code&gt;vw-data&lt;/code&gt; directory and updated &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;vaultwarden&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vaultwarden/server:latest&lt;/span&gt;
  &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vaultwarden&lt;/span&gt;
  &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ADMIN_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;changeme"&lt;/span&gt;
    &lt;span class="na"&gt;ROCKET_WORKERS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;ROCKET_PROXY_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/pass"&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./vw-data:/data&lt;/span&gt;
  &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ROCKET_PROXY_PATH="/pass"&lt;/code&gt; was critical.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 3: Subpath vs Subdomain&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;At this stage, I faced a design choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Option 1: Subdomain&lt;/strong&gt; (&lt;code&gt;vault.mydomain.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Option 2: Subpath&lt;/strong&gt; (&lt;code&gt;mydomain.com/pass&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Initially, subdomains seemed neat and clean. But there are trade-offs for self-hosted services:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Certificate complexity&lt;/strong&gt;: With a subdomain, I would need a separate certificate for &lt;code&gt;vault.mydomain.com&lt;/code&gt;. With a local or internal CA, managing multiple subdomains could get messy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Future services&lt;/strong&gt;: I wanted to host other tools (like a personal wiki or file server). Using a subdomain for every service could clutter DNS and complicate VPN/local routing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailscale &amp;amp; mobile access&lt;/strong&gt;: VPN solutions sometimes resolve only the main hostname. Subdomains occasionally fail or require additional DNS tweaks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A &lt;strong&gt;subpath&lt;/strong&gt; approach (&lt;code&gt;/pass&lt;/code&gt;) solved these issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One certificate covers the main domain.&lt;/li&gt;
&lt;li&gt;Reverse proxy handles routing internally to different services.&lt;/li&gt;
&lt;li&gt;Makes scaling to multiple services simpler without touching DNS or VPN settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hence, I chose &lt;code&gt;/pass&lt;/code&gt; for Vaultwarden and left &lt;code&gt;/&lt;/code&gt; free for other services. This required careful reverse proxy configuration, which I’ll describe next.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 4: Reverse Proxy with Caddy&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I used &lt;strong&gt;Caddy&lt;/strong&gt; as a reverse proxy to handle HTTPS and route &lt;code&gt;/pass&lt;/code&gt; to Vaultwarden. My first attempt was basic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;panda.local {
    tls internal
    encode zstd gzip
    reverse_proxy vaultwarden:80
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Problems immediately appeared: assets broke, WebSockets failed, and trailing slashes caused the interface to misbehave. Vaultwarden expects requests at the root, so a simple proxy was insufficient.&lt;/p&gt;

&lt;p&gt;After research and testing, I implemented:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;handle_path /pass/* {
    reverse_proxy vaultwarden:80 {
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-Proto {scheme}
        header_up X-Forwarded-Host {host}
        header_up X-Forwarded-For {remote_host}
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;handle_path /pass/*&lt;/code&gt; &lt;strong&gt;strips the &lt;code&gt;/pass&lt;/code&gt; prefix&lt;/strong&gt; before forwarding requests to Vaultwarden. This solved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Asset loading issues&lt;/li&gt;
&lt;li&gt;WebSocket failures&lt;/li&gt;
&lt;li&gt;Trailing slash inconsistencies&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 5: TLS and Certificates&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Since I wanted both desktop and mobile apps to trust my self-signed setup, I used &lt;strong&gt;mkcert&lt;/strong&gt; to generate a local CA and certificates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkcert panda.local
mkcert pass.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copying the root CA to my desktop required careful path handling due to spaces in usernames, and mobile installation required converting &lt;code&gt;.pem&lt;/code&gt; to &lt;code&gt;.crt&lt;/code&gt; and manually trusting the certificate. Once done, browsers and apps stopped showing SSL warnings.&lt;/p&gt;

&lt;p&gt;Lesson learned: TLS is critical, but self-hosting means &lt;strong&gt;you need to manage trust explicitly&lt;/strong&gt; on each device.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 6: Remote Access with VPN&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I didn’t want to open my home network to the internet. Using a VPN-like solution, I could access Vaultwarden remotely without exposing ports. Initially, there were DNS resolution issues with subdomains, but after adding entries to local hosts and configuring certificates correctly, remote access worked smoothly.&lt;/p&gt;

&lt;p&gt;This setup also allowed me to think ahead: by using &lt;code&gt;/pass&lt;/code&gt;, I left the root path free for future services, creating a small personal service ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 7: Troubleshooting Common Issues&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here’s a list of the main problems I faced and how I solved them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No persistent volume&lt;/strong&gt;: Fixed by mapping &lt;code&gt;./vw-data:/data&lt;/code&gt; in Docker Compose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subpath asset/WebSocket issues&lt;/strong&gt;: Resolved with &lt;code&gt;handle_path /pass/*&lt;/code&gt; in Caddy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSL warnings&lt;/strong&gt;: Generated local CA with mkcert, imported into desktop and mobile devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trailing slash problems&lt;/strong&gt;: Also handled by &lt;code&gt;handle_path&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote access quirks&lt;/strong&gt;: Fixed via local DNS entries and correct certificate paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subdomain vs subpath confusion&lt;/strong&gt;: Choosing subpath simplified certificates and routing for multiple services.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Step 8: Final Setup&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;By the end of the day, I had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vaultwarden running under &lt;code&gt;/pass&lt;/code&gt; on the Pi Zero 2W&lt;/li&gt;
&lt;li&gt;Full HTTPS support trusted on desktop and mobile&lt;/li&gt;
&lt;li&gt;Remote access without exposing home network ports&lt;/li&gt;
&lt;li&gt;Persistent storage for all passwords&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Testing everything — creating accounts, logging in from mobile apps, syncing TOTP codes — everything worked flawlessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Reflections and Lessons Learned&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Self-hosting is rewarding, but it comes with challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker volumes prevent accidental data loss.&lt;/li&gt;
&lt;li&gt;Reverse proxies can make or break subpath hosting.&lt;/li&gt;
&lt;li&gt;TLS and certificates are critical; trust must be manually managed for internal or mobile clients.&lt;/li&gt;
&lt;li&gt;Subpath routing simplifies multi-service hosting, while subdomains require additional DNS and cert management.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small fixes like &lt;code&gt;handle_path&lt;/code&gt; or importing a CA may seem trivial, but they &lt;strong&gt;make the difference between a broken interface and a smooth user experience&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Self-hosting Vaultwarden on a Pi Zero 2W might seem overkill for a single user, but for me, it was about &lt;strong&gt;control, privacy, and learning&lt;/strong&gt;. The Pi handled everything surprisingly well, and Vaultwarden proved to be a solid, lightweight solution.&lt;/p&gt;

&lt;p&gt;For developers looking to take control of their digital lives: start small, expect roadblocks, and treat each error as a learning opportunity. In the end, there’s immense satisfaction in seeing your own system, from certificates to WebSockets, working exactly the way you designed it.&lt;/p&gt;

</description>
      <category>devlog</category>
      <category>learning</category>
      <category>linux</category>
      <category>docker</category>
    </item>
    <item>
      <title>Ever wondered why Windows or Android updates are so huge? This isn’t just about code – it’s about an entire hidden world of engineering farms, power consumption, and logistics. My new article opens that black box.</title>
      <dc:creator>Priyanshu Verma</dc:creator>
      <pubDate>Sun, 17 Aug 2025 10:47:14 +0000</pubDate>
      <link>https://dev.to/priyanshuverma/ever-wondered-why-windows-or-android-updates-are-so-huge-this-isnt-just-about-code-its-about-1gk</link>
      <guid>https://dev.to/priyanshuverma/ever-wondered-why-windows-or-android-updates-are-so-huge-this-isnt-just-about-code-its-about-1gk</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/priyanshuverma/the-hidden-engineering-behind-building-massive-software-images-6j6" class="crayons-story__hidden-navigation-link"&gt;The Hidden Engineering Behind Building Massive Software Images&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/priyanshuverma" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F994703%2F071b4abb-44c9-4685-aa5a-04f98019e345.png" alt="priyanshuverma profile" class="crayons-avatar__image" width="500" height="500"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/priyanshuverma" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Priyanshu Verma
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Priyanshu Verma
                
                
              
              &lt;div id="story-author-preview-content-2778758" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/priyanshuverma" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F994703%2F071b4abb-44c9-4685-aa5a-04f98019e345.png" class="crayons-avatar__image" alt="" width="500" height="500"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Priyanshu Verma&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/priyanshuverma/the-hidden-engineering-behind-building-massive-software-images-6j6" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Aug 17 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/priyanshuverma/the-hidden-engineering-behind-building-massive-software-images-6j6" id="article-link-2778758"&gt;
          The Hidden Engineering Behind Building Massive Software Images
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cloud"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cloud&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/architecture"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;architecture&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/learning"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;learning&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/priyanshuverma/the-hidden-engineering-behind-building-massive-software-images-6j6" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;12&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/priyanshuverma/the-hidden-engineering-behind-building-massive-software-images-6j6#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              4&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            7 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>programming</category>
      <category>bts</category>
      <category>ai</category>
    </item>
    <item>
      <title>The Hidden Engineering Behind Building Massive Software Images</title>
      <dc:creator>Priyanshu Verma</dc:creator>
      <pubDate>Sun, 17 Aug 2025 10:46:09 +0000</pubDate>
      <link>https://dev.to/priyanshuverma/the-hidden-engineering-behind-building-massive-software-images-6j6</link>
      <guid>https://dev.to/priyanshuverma/the-hidden-engineering-behind-building-massive-software-images-6j6</guid>
      <description>&lt;p&gt;Hi,&lt;br&gt;
You guys know me I try doing something new always so, recently When I first stared at the AOSP tree and saw the download size, I thought someone was pranking me. Hundreds of gigabytes? Thousands of repos? How could anyone reasonably &lt;em&gt;build&lt;/em&gt; that on a laptop, even if it had more cores than a nuclear reactor? The truth pulled me down a rabbit hole I didn’t expect — one that rewrites what “writing software” even means.&lt;/p&gt;

&lt;p&gt;Let me take you on a short film of how a piece of code becomes a massive, signed, tested, shippable image — whether that image is your phone’s system update, a Windows release, or a billion-dollar AAA game. This isn’t just about compilers and &lt;code&gt;make&lt;/code&gt;; it’s about orchestration, scale, people and machines working together in a choreography most users never see.&lt;/p&gt;




&lt;p&gt;I want you to imagine two scenes.&lt;/p&gt;

&lt;p&gt;Scene one: a lone engineer, late at night, running &lt;code&gt;make&lt;/code&gt; on a workstation. The machine hums. The screen scrolls error messages and warnings. The coffee goes cold. This image is still real in small projects, but it is increasingly a nostalgic relic.&lt;/p&gt;

&lt;p&gt;Scene two: a humming build farm, rack after rack of machines, jobs queued and parallelized across continents, caches consulted in milliseconds, thousands of tests flaring green or red. Keys and signatures are applied in a secure enclave. An image rolls down the pipeline and — eventually — reaches a device on a user’s desk via an OTA update. That is the new reality.&lt;/p&gt;

&lt;p&gt;Why the chasm between these two scenes? Why can’t we just “compile the code” and be done?&lt;/p&gt;

&lt;p&gt;It starts with scale. Modern operating systems and flagship apps aren’t a few thousand files stitched together. They’re an orchestra of components: kernels, drivers, vendor binaries, runtime engines, media libraries, UIs, services, apps, localization packs, and every test harness you can imagine. Where a small embedded project might be measured in thousands of lines, Windows and Android live in the tens-of-millions-of-lines regime. Chrome, Android, and game engines bring along enormous native code, Java or managed runtimes, and gigantic asset pipelines. Each of these pieces has its own build steps, expectations, and quirks.&lt;/p&gt;

&lt;p&gt;But scale alone doesn’t explain the orchestration required. Building a modern image is not just compiling source; it’s a multi-stage process that touches toolchains, packaging systems, signing infrastructure, artifact storage, and certification systems. You don’t merely turn C into binaries; you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;run code generators and preprocessors,&lt;/li&gt;
&lt;li&gt;produce architecture-specific binaries (x86_64, arm64, etc.),&lt;/li&gt;
&lt;li&gt;link enormous binaries (link time optimization and profile-guided optimization can be highly sequential and heavy),&lt;/li&gt;
&lt;li&gt;bundle resources (localization, images, precompiled shaders),&lt;/li&gt;
&lt;li&gt;compress and sign artifacts with secure keys,&lt;/li&gt;
&lt;li&gt;and then subject them to thousands of automated tests on real hardware.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of that introduces latency and makes naive parallelism insufficient. Even on a machine with 64 cores, disk IO, linking, and serialized packaging steps become chokepoints. The first full build, the “clean build,” necessarily touches everything: read tens of thousands of files, compile, link, pack. That’s expensive in time and in energy.&lt;/p&gt;

&lt;p&gt;Which brings me to a second, often-overlooked consequence: the environmental cost. When you multiply full builds by dozens of devs, nightly images, pull-request validations, and continuous fuzzing runs, the energy footprint becomes non-trivial. Companies running nightly images and large CI matrices are effectively operating mini data centers. This reality has pushed teams to optimize for two things: reduce redundant work and reduce waste. Caching (ccache, remote action caches), incremental builds, and smarter CI orchestration are not just developer conveniences — they’re carbon optimizations.&lt;/p&gt;

&lt;p&gt;So, how do teams cope? How does any sane engineering organization ship anything at all?&lt;/p&gt;

&lt;p&gt;They split the problem. Software at scale is &lt;em&gt;modularized&lt;/em&gt; and &lt;em&gt;specialized&lt;/em&gt;. Teams no longer touch the whole tree. An engineer working on the window manager builds the window manager. A kernel engineer builds the kernel. These are unit-of-work boundaries. The full single-piece build is delegated to a build farm — a distributed system of machines that can run the complete set of compilation tasks in parallel, honor dependency graphs, and cache results aggressively.&lt;/p&gt;

&lt;p&gt;Distributed build systems are at the heart of this approach. Google’s Goma (migrated to Reclient in 2025) and remote execution, Bazel’s remote execution, Microsoft’s BuildXL, and similar technologies allow compilation tasks to be farmed out, cached, and reused. Instead of each developer redoing the same heavy work, cached object files are reused across jobs and builds. The result: individual iteration latency drops from hours to minutes, and the full heavy build happens in a controlled, centralized way.&lt;/p&gt;

&lt;p&gt;But distributed builds are only part of the story. There’s also a surprising orchestration layer: artifact variants and packaging. One codebase can produce dozens — even hundreds — of artifacts. AOSP, for instance, doesn’t generate a single image; it generates system images for multiple device families, variants with different Google services or OEM customizations, boot images, vendor images, and more. Windows similarly produces many SKUs, localized images, and platform-specific packages. Games produce builds for multiple consoles, PC configurations, and packaged installers. Each variant multiplies the amount of work and the number of tests.&lt;/p&gt;

&lt;p&gt;Testing is another dimension that explodes resource needs. It is rarely acceptable anymore to ship untested large changes. Integration suites, fuzzers, UI tests, driver compatibility tests — these run on thousands of device instances, sometimes physically — and failures require reproducing on identical hardware. So the engineering pipeline must not only build; it must distribute, schedule, and record results from a massively parallel test farm.&lt;/p&gt;

&lt;p&gt;This reality created an entire discipline: build engineering and release engineering. These teams are the invisible logisticians of software. Their job is not product feature work; it’s to ensure developers can iterate quickly, builds are reproducible, artifacts are traceable and secure, and releases roll out smoothly. Reproducibility is a central obsession: you should be able to rebuild an artifact deterministically so you can trust what went out to users and trace bugs back to a specific build. That means pinning toolchains, hermetic builds, recorded inputs, and deterministic packaging — a lot of boring-but-important work.&lt;/p&gt;

&lt;p&gt;To me, one of the most compelling stories is the contrast between eras. In the ’80s and ’90s, building an OS was something you could conceive on a single workstation. Engineers would go home and let overnight builds run. Today, building Windows or a AAA title is akin to running a rendering farm for a movie: assets measured in terabytes, precompiled shaders, baked global illumination, and platform certification pipelines. Modern AAA titles have asset pipelines that are as much a part of the build — with their own distributed render farms — as the code compilation itself. The product is rarely “code”; it’s code times assets times compatibility matrices.&lt;/p&gt;

&lt;p&gt;Windows is a vivid example. Its codebase has accreted decades of code, multiple languages, legacy interfaces, and vast driver ecosystems. Microsoft’s approach is to carve the monolith into components. Engineers iterate locally on targeted subsystems and push changes. A centralized farm, armed with sophisticated orchestrators, then runs full builds and regression suites. For Windows, the human problem isn’t “write the code”; it’s “ensure that code plays nicely with tens of thousands of hardware permutations and years of backward compatibility.” Microsoft’s build infrastructure is built to manage that complexity — not to make building trivial, but to make shipping possible and predictable.&lt;/p&gt;

&lt;p&gt;And if you’re wondering whether AI will change this equation — write code and let machines assemble everything — the answer is: AI will help, but it won’t replace the choreography. Writing code is only a shard of the whole. The pipeline, the reproducibility, the signing, the compatibility testing, the device certification, the incremental build caches, the keys and signing policies, the human coordination, the rollback strategies — these are socio-technical problems. AI can suggest changes, even generate modules, but someone still has to decide which artifacts to build, how they get tested, what gets signed, and how to remediate a failing rollout. In short, AI can be an assistant, not the orchestra conductor — at least not in the near term.&lt;/p&gt;

&lt;p&gt;If you want to dig deeper, here are a few research and reading touchpoints I keep returning to while exploring this world:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read the Bazel documentation and the philosophy of hermetic builds.&lt;/li&gt;
&lt;li&gt;Explore Google’s papers and blog posts on Goma / remote execution (they explain why distributed compilation matters).&lt;/li&gt;
&lt;li&gt;Look up BuildXL (Microsoft) and the evolution of Git Virtual File System (GVFS) for handling huge repositories.&lt;/li&gt;
&lt;li&gt;Search for the “Reproducible Builds” project — it captures why deterministic builds are so central to security and trust.&lt;/li&gt;
&lt;li&gt;Check out material on remote caching and action caches (how CI avoids doing repeated work).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I find the story irresistible because it reframes what building software means: it’s less about the solitary craft of typing functions and more about designing resilient industrial processes. The magic of a software update isn’t the few lines someone typed last week — it’s the invisible machinery that compiled, tested, signed, and safely delivered that update to millions without breaking everything else.&lt;/p&gt;

&lt;p&gt;If you’re curious, here’s one practical way to feel this under your fingers: don’t attempt a whole AOSP build yet. Instead, pick a subsystem (a kernel module, a small Android service, a game asset pipeline task), build only that module locally, replace it into a prebuilt image, and iterate. You’ll experience first-hand the dramatic speedups that modular work affords, and you’ll see why entire teams focus on making &lt;em&gt;those&lt;/em&gt; iterations as fast and reliable as possible.&lt;/p&gt;

&lt;p&gt;When people ask me what the future of software engineering looks like, I don’t see a world where developers are obsolete. I see a world where “developer” means more — you’ll need to understand not only code, but caches, CI graphs, reproducibility guarantees, artifact signing, and the economics of compute. The boring parts — the build farms, the signing policies, the orchestration — are where trust is forged. And that, I think, is a story worth telling.&lt;/p&gt;

&lt;p&gt;Thanks for reading to the end. I know you have time to waste then also checkout my &lt;a href="https://github.com/PriyanshuPz" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;— Priyanshu Verma&lt;/p&gt;

</description>
      <category>programming</category>
      <category>cloud</category>
      <category>architecture</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
