<?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: Gonzalo Salvador Corvalán</title>
    <description>The latest articles on DEV Community by Gonzalo Salvador Corvalán (@themrcorvy).</description>
    <link>https://dev.to/themrcorvy</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%2F793546%2Fae26dfa3-fd2f-4c99-8ecb-1033a25bf877.png</url>
      <title>DEV Community: Gonzalo Salvador Corvalán</title>
      <link>https://dev.to/themrcorvy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/themrcorvy"/>
    <language>en</language>
    <item>
      <title>A self-propagating system that created itself</title>
      <dc:creator>Gonzalo Salvador Corvalán</dc:creator>
      <pubDate>Mon, 03 Aug 2026 16:39:11 +0000</pubDate>
      <link>https://dev.to/themrcorvy/a-self-propagating-system-that-created-itself-2gk8</link>
      <guid>https://dev.to/themrcorvy/a-self-propagating-system-that-created-itself-2gk8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Misaka Network, the Self-Propagating system that created itself to allow me to automate the processing of new videos and directories for my self-hosted streaming service. How I replaced a monolithic admin script with a webhook-driven, event-based system. A deep dive into event-driven architecture, Strapi V5 webhooks, anime references, and automating media ingestion without writing a single loop.&lt;br&gt;
A technical deep-dive into event-driven architecture and how I replaced a monolithic admin script with a clean, reactive webhook system in Unlimited Blades Work.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What are Webhooks?
&lt;/h2&gt;

&lt;p&gt;Before I tell you about my streaming app and the architectural shift I made, let me establish some common ground. Webhooks are something most developers have heard of, but their full power is often underappreciated until you find the right problem for them to solve.&lt;/p&gt;

&lt;p&gt;At their core, a webhook is a simple idea: instead of your application constantly asking another service "hey, did anything change?", you tell that other service "when something changes, let me know by calling this URL". It is a push-based communication model as opposed to the traditional pull-based polling model.&lt;/p&gt;

&lt;p&gt;More concretely, a webhook is an HTTP endpoint that you expose in your application. When an event happens in an external system, that system sends an HTTP POST request to your endpoint with a payload that describes what just happened. Your server receives that payload, does something with it, and returns a response to acknowledge receipt.&lt;/p&gt;

&lt;p&gt;That is all there is to it. There is no persistent connection, no socket to maintain, no background polling loop. You simply register a URL and wait for the world to call you.&lt;/p&gt;

&lt;p&gt;The payload a webhook sends typically looks like structured JSON data. It contains enough information to understand what happened, what entity changed, and what the new state of that entity is. In the context of a content management system like Strapi V5, a webhook might tell you that a specific entry was just created, that a user updated a record, or that content was published or deleted.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do webhooks work?
&lt;/h2&gt;

&lt;p&gt;The lifecycle of a webhook call is straightforward, but understanding each step helps clarify why this pattern is so powerful.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant User as User / Admin
    participant CMS as Strapi V5 (CMS)
    participant Backend as Private Cloud Backend
    participant Queue as Job Queue

    User-&amp;gt;&amp;gt;CMS: Creates or updates a Directory entry
    CMS-&amp;gt;&amp;gt;Backend: POST /webhooks/entry.create (JSON payload)
    Backend-&amp;gt;&amp;gt;Backend: Validates event type and UID
    Backend-&amp;gt;&amp;gt;Queue: Adds PROCESS_DIRECTORY job
    Backend--&amp;gt;&amp;gt;CMS: 200 OK (acknowledged)
    Note over Backend,Queue: Webhook handler returns immediately
    Queue-&amp;gt;&amp;gt;Backend: Job is picked up by cron engine
    Backend-&amp;gt;&amp;gt;Backend: Processes directory on disk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sequence above illustrates the key insight: the webhook handler does its job quickly and returns. It doesn't block, it doesn't process the full task inline. It simply acknowledges the event and delegates the heavy work to a job queue. This makes the webhook endpoint resilient and fast, and it decouples the moment of notification from the moment of processing.&lt;/p&gt;

&lt;p&gt;The external system, Strapi in this case, does not care whether the actual processing finishes in one second or one hour. It only needs to know that the backend received the signal. From Strapi's perspective, the transaction is complete the moment it gets a 200 OK response.&lt;/p&gt;




&lt;h2&gt;
  
  
  What are Event-Driven Applications?
&lt;/h2&gt;

&lt;p&gt;A webhook is the mechanism, but the broader philosophy behind it is called event-driven architecture. In an event-driven system, the flow of the application is determined by events rather than by a sequential, imperative script that runs from top to bottom.&lt;/p&gt;

&lt;p&gt;The contrast with traditional procedural code is striking. In a traditional script, the author of the code decides the full sequence of operations at the time of writing. The program runs, executes step one, then step two, then step three, and finishes. The entire lifecycle is visible in a single place, and the program is in full control of timing and ordering.&lt;/p&gt;

&lt;p&gt;In an event-driven system, things are different. Individual components do not know about each other in detail. They only know how to react to a specific event. A component receives an event, does its specific job, and as a side effect of that job it might trigger new events for other components to react to. The system is alive. It responds to the world as it changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph LR
    A["Traditional Script"] --&amp;gt; B["Step 1: Scan directories"]
    B --&amp;gt; C["Step 2: Verify parent in DB"]
    C --&amp;gt; D["Step 3: Store directories"]
    D --&amp;gt; E["Step 4: Process episodes"]
    E --&amp;gt; F["Step 5: Run FFmpeg"]
    F --&amp;gt; G["Done (or failed)"]

    H["Event-Driven System"] --&amp;gt; I["Event: directory.created"]
    I --&amp;gt; J["Queue job"]
    J --&amp;gt; K["Process directory"]
    K --&amp;gt; L["Event: child_directory.created"]
    L --&amp;gt; I
    K --&amp;gt; M["Event: episode.created"]
    M --&amp;gt; N["Queue episode job"]
    N --&amp;gt; O["Process episode (FFmpeg)"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event-driven model comes with significant benefits. It is more resilient because each component is isolated and a failure in one does not cascade to everything else. It is more scalable because you can process multiple events concurrently. It is also far more maintainable because each handler has a single, well-defined responsibility.&lt;/p&gt;

&lt;p&gt;The downside is that it requires more upfront design. You need to think in terms of events, producers, and consumers rather than a simple script. And debugging can be harder because the flow of execution is not linear. But for complex, long-running operations like processing a tree of media files, the tradeoffs are absolutely worth it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem I needed to solve
&lt;/h2&gt;

&lt;p&gt;Unlimited Blades Work is a self-hosted streaming application that I run on my private cloud. It allows me to watch movies, anime, and TV series from any device. The content is stored on disk in a hierarchical folder structure: top-level directories contain subdirectories, which in turn contain video files. A directory might be an anime series, and its subdirectories might be seasons, with individual episodes inside each season.&lt;/p&gt;

&lt;p&gt;The application uses Strapi V5 as its headless CMS and database layer. Every directory and every episode exists as a Strapi entry. The streaming backend then reads those entries to know where the files live on disk, what metadata they have, what audio tracks and subtitle tracks they contain, and so on.&lt;/p&gt;

&lt;p&gt;The structural challenge here is fundamental: I needed to map a tree of data on disk into a relational database. A file system is a tree. A relational database is tables with foreign key relationships. These two models do not naturally align, and bridging them requires careful thought (believe me).&lt;/p&gt;




&lt;h2&gt;
  
  
  The original script to intialize the database
&lt;/h2&gt;

&lt;p&gt;My first solution was a single, monolithic TypeScript script called &lt;code&gt;initializeDatabase.ts&lt;/code&gt;. When I wanted to add new content to the library, I would SSH into my server, navigate to the backend project, and run this script manually. It would scan the disk, figure out what needed to be added to Strapi, and do all the work in one sequential pass.&lt;/p&gt;

&lt;p&gt;The approach was clever in its own way. As the name implies, the initializeDatabase.ts script scanned the entire disk using a recirsive strategy, then for each folder/video file it find it would process it or add it to the pendingToScan array that was being iterated with a WHILE loop that checked whether or not the pendingToScan array had anything inside of it.&lt;/p&gt;

&lt;p&gt;That way ensuring that with one single script, the app would be usable and then I'd just have to run a different, smaller script for processing videos. It required a lot of manual work for the new entries but when you have a very large media library to begin with (like me) a script like this is all you need to start watching your movies and stuff.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scanning the disk
&lt;/h3&gt;

&lt;p&gt;The script started by calling &lt;code&gt;scanAndOrganizeDirectories&lt;/code&gt; function, which accepted a list of root paths from environment variables (the &lt;code&gt;INITIAL_PATH&lt;/code&gt; env variable). For each root path, it would call &lt;code&gt;scanSingleFolder&lt;/code&gt; function from the disk service. This function read the filesystem at that path and returned a &lt;code&gt;LocalDirectory&lt;/code&gt; object containing the folder's display name, its path relative to the base, its age rating (derived from naming conventions on the folder itself), a list of its immediate child subdirectories, and a list of its immediate video files as &lt;code&gt;LocalEpisode&lt;/code&gt; objects.&lt;/p&gt;

&lt;h3&gt;
  
  
  The pending queue pattern
&lt;/h3&gt;

&lt;p&gt;The core of the scanning logic used a classic breadth-first traversal pattern. After scanning the root directory, its child directory paths were placed into a &lt;code&gt;pendingToScan&lt;/code&gt; array. Then a &lt;code&gt;while&lt;/code&gt; loop kept running as long as that array had items. Each iteration would pop a path from the array, scan it, add its results to the final list, and push any new child directories it found back into the pending array.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A["Start: Root directories from env"] --&amp;gt; B["scanSingleFolder for each root"]
    B --&amp;gt; C["Collect child directories into pendingToScan"]
    C --&amp;gt; D{{"pendingToScan is empty?"}}
    D -- "No" --&amp;gt; E["Pop a path from pendingToScan"]
    E --&amp;gt; F["scanSingleFolder for this path"]
    F --&amp;gt; G["Add result to finalResult"]
    G --&amp;gt; H["Push child dirs into pendingToScan"]
    H --&amp;gt; D
    D -- "Yes" --&amp;gt; I["All directories collected"]
    I --&amp;gt; J["sortDirectories by depth"]
    J --&amp;gt; K["Begin uploading to Strapi"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This loop meant that after the scan phase, &lt;code&gt;finalResult&lt;/code&gt; contained every single directory in the entire tree, flattened into a single array. The tree structure was represented not by nesting but by each &lt;code&gt;LocalDirectory&lt;/code&gt; holding a &lt;code&gt;parent_directory&lt;/code&gt; path string.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sorting by depth
&lt;/h3&gt;

&lt;p&gt;Once the flat list was assembled, &lt;code&gt;sortDirectories&lt;/code&gt; would sort it by the number of path segments in the &lt;code&gt;parent_directory&lt;/code&gt; field. Directories with fewer levels came last. This was critical for the next phase: you cannot create a child directory in Strapi if its parent does not exist yet. By processing parents before children, the script ensured that every time it needed to link a child to its parent, the parent was already in the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Uploading to Strapi
&lt;/h3&gt;

&lt;p&gt;The main loop then iterated through the sorted array of directories. For each one, it would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check if the directory already existed in Strapi using &lt;code&gt;verifyDirectoryExistance&lt;/code&gt;, to avoidcreating duplicates.&lt;/li&gt;
&lt;li&gt;If the directory had a parent, query Strapi to find the parent's &lt;code&gt;documentId&lt;/code&gt;, since that ID isneeded to create the foreign key relationship.&lt;/li&gt;
&lt;li&gt;Create the directory entry in Strapi using the platform service SDK (my custom automated solution for consuming Strapi's data).&lt;/li&gt;
&lt;li&gt;For each episode inside that directory, call &lt;code&gt;verifyEpisodeExistance&lt;/code&gt; to check if it alreadyexisted.&lt;/li&gt;
&lt;li&gt;If the episode was a V2 file (MKV and similar formats that require processing), invoke theFFmpeg service via &lt;code&gt;processVideoFile&lt;/code&gt; to extract audio tracks, subtitle tracks, and durationmetadata.&lt;/li&gt;
&lt;li&gt;Store or update the episode entry in Strapi with all of that data.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The manual script to process video files
&lt;/h3&gt;

&lt;p&gt;Separate from the database initialization, there was also a &lt;code&gt;testFfmpeg.ts&lt;/code&gt; script (later renamed to a more general video processing script) that I would run to test the FFmpeg pipeline against a specific file. The workflow was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH into the server.&lt;/li&gt;
&lt;li&gt;Navigate to the backend project directory.&lt;/li&gt;
&lt;li&gt;Run the script with the path to the video file as a command-line argument.&lt;/li&gt;
&lt;li&gt;Wait for FFmpeg to finish extracting audio and subtitle tracks.&lt;/li&gt;
&lt;li&gt;Copy the resulting metadata manually.&lt;/li&gt;
&lt;li&gt;Create or update the episode entry in Strapi by hand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was tedious for a single episode, and completely impractical for an entire new anime series with multiple seasons and dozens of episodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the script started to break down
&lt;/h3&gt;

&lt;p&gt;The script worked. It solved the problem. But over time it grew. Edge cases were added: what if a directory had already been partially imported? What if an episode existed but with a different version? What if the parent directory couldn't be found? What if FFmpeg failed for one episode but not others? Each new edge case added more conditional branches, more error-tracking arrays, more logging, more complexity.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;initializeDatabase.ts&lt;/code&gt; file grew past 3000 lines of densely interleaved logic. The utils it depended on added hundreds more. The script had too many responsibilities. It scanned the disk, validated environments, managed Strapi communications, ran FFmpeg, tracked failed and skipped entries, and wrote debug JSON files. It knew too much about everything.&lt;/p&gt;

&lt;p&gt;Even for me, the author, tracing a bug through that script required holding a lot of context in mind at once. And running it required SSH access to the server every single time I downloaded something new.&lt;/p&gt;

&lt;p&gt;There had to be a better way.&lt;/p&gt;




&lt;h2&gt;
  
  
  The new system: Webhooks and the Misaka Network (Yes, thats the name)
&lt;/h2&gt;

&lt;p&gt;The new system is built around a simple principle: instead of the backend doing all the work in one go, Strapi V5 tells the backend what to do as entries change. The backend only needs to handle one directory or one episode at a time, and Strapi's own event system becomes the engine that drives the whole process forward.&lt;/p&gt;

&lt;p&gt;I named the service layer handling this logic the &lt;strong&gt;Misaka Network&lt;/strong&gt;, an internal codename for the subsystem that processes media content reactively. Why? Because it simply delegates the processing of a video (which is hard, takes a lot of time, and calculations) to an external brain that does the heavy lifting. And because it sounds cool.&lt;/p&gt;

&lt;h3&gt;
  
  
  The high-level architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TD
    User["User (via Strapi Admin UI or Smartphone)"]
    Strapi["Strapi V5 CMS"]
    Backend["Express Backend"]
    Queue["Job Queue (SQLite)"]
    Engine["Stateful Cron Engine"]
    Misaka["Misaka Network Service"]
    Disk["Disk (Video Files)"]
    FFmpeg["FFmpeg Service"]

    User --&amp;gt;|"Creates/Updates directory or episode"| Strapi
    Strapi --&amp;gt;|"Webhook POST payload"| Backend
    Backend --&amp;gt;|"Adds job to queue"| Queue
    Engine --&amp;gt;|"Polls queue on schedule"| Queue
    Engine --&amp;gt;|"Dispatches job"| Misaka
    Misaka --&amp;gt;|"Scans filesystem"| Disk
    Misaka --&amp;gt;|"Invokes FFmpeg"| FFmpeg
    Misaka --&amp;gt;|"Creates child directories &amp;amp; episodes"| Strapi
    Strapi --&amp;gt;|"Fires new webhooks for each new entry"| Backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The beauty of this design is that the loop closes itself. When the Misaka Network creates child directories in Strapi as part of processing a parent directory, Strapi fires new &lt;code&gt;entry.create&lt;/code&gt; webhooks for each one. Those webhooks arrive at the backend, get queued, and get processed in turn.&lt;/p&gt;

&lt;p&gt;No explicit loop or recursion is needed. The system is &lt;strong&gt;self-propagating&lt;/strong&gt; &lt;em&gt;(I never though I could create a system that I could describe as Self-Propagating in my entire life LOL)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Believe it or not, this entire system spawned by accident actually. It just started as "What if I could add a movie in strapi, and fire this event to process it automatically?", then "What if I do the same with directories?" voalá the system created itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  The controller layer
&lt;/h3&gt;

&lt;p&gt;The controllers are the first point of contact when Strapi sends a webhook. There is a dedicated controller for each event type that Strapi can emit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;strapiWebhook.entry.create.controller.ts&lt;/code&gt; handles &lt;code&gt;entry.create&lt;/code&gt; events.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;strapiWebhook.entry.update.controller.ts&lt;/code&gt; handles &lt;code&gt;entry.update&lt;/code&gt; events.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;strapiWebhook.entry.delete.controller.ts&lt;/code&gt; handles &lt;code&gt;entry.delete&lt;/code&gt; events.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;strapiWebhook.entry.publish.controller.ts&lt;/code&gt; handles &lt;code&gt;entry.publish&lt;/code&gt; events.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;strapiEngine.controller.ts&lt;/code&gt; is a special admin controller for accelerating and decelerate the cron job engine manually (from Strapi).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each controller's job is intentionally narrow. It receives the payload, validates that the event type matches what it expects, inspects the &lt;code&gt;uid&lt;/code&gt; field to determine which content type the event refers to (directory, episode, social media post, etc.), and then either rejects the request or adds a job to the queue. Then it responds with 200 OK and returns.&lt;/p&gt;

&lt;p&gt;The types for the webhook payload are defined in a file called &lt;code&gt;strapiWebhook.types.ts&lt;/code&gt;. A &lt;code&gt;StrapiWebhookPayload&lt;/code&gt; carries the event name, the timestamp, the model name, the content type UID, and the full entry object. An &lt;code&gt;enum&lt;/code&gt; called &lt;code&gt;StrapiEventName&lt;/code&gt; lists all the possible event strings, and another enum called &lt;code&gt;ModelNames&lt;/code&gt; lists the content type UIDs that the backend cares about.&lt;/p&gt;

&lt;p&gt;For the directory and episode controllers specifically, there is a guard check: a directory will only be queued for processing if its &lt;code&gt;is_processing&lt;/code&gt; field is set to &lt;code&gt;true&lt;/code&gt;. This is the toggle that allows you to create entries in Strapi without triggering processing, which is useful for initial setup or manual data correction.&lt;/p&gt;

&lt;p&gt;Here is the flow through the create controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A["POST /webhooks/entry.create"] --&amp;gt; B{{"event === 'entry.create'?"}}
    B -- "No" --&amp;gt; C["Return 400 Bad Request"]
    B -- "Yes" --&amp;gt; D{{"uid === 'api::b-episode.b-episode'?"}}
    D -- "Yes" --&amp;gt; E{{"version === 'V1'?"}}
    E -- "Yes" --&amp;gt; F["Return 200 OK (no processing needed for mp4)"]
    E -- "No" --&amp;gt; G["addJobToQueue(PROCESS_EPISODE)"]
    G --&amp;gt; H["Return 200 OK"]
    D -- "No" --&amp;gt; I{{"uid === 'api::b-directory.b-directory'?"}}
    I -- "Yes" --&amp;gt; J{{"is_processing === true?"}}
    J -- "No" --&amp;gt; K["Return 200 OK (ignored)"]
    J -- "Yes" --&amp;gt; L["addJobToQueue(PROCESS_DIRECTORY)"]
    L --&amp;gt; H
    I -- "No" --&amp;gt; M["Return 404 Unknown UID"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The job queue and cron engine
&lt;/h3&gt;

&lt;p&gt;When a controller calls &lt;code&gt;addJobToQueue&lt;/code&gt;, it writes a record to a local MySQL database. Each job has a type (&lt;code&gt;PROCESS_DIRECTORY&lt;/code&gt; or &lt;code&gt;PROCESS_EPISODE&lt;/code&gt;), a name for logging purposes, and a JSON payload containing the full Strapi entry.&lt;/p&gt;

&lt;p&gt;The cron engine is managed by &lt;code&gt;strapiEngineController&lt;/code&gt;. An admin can start the engine by sending a request to the engine endpoint with &lt;code&gt;action: start_engine&lt;/code&gt; and specifying how often it should run (&lt;code&gt;every&lt;/code&gt;) and for how long (&lt;code&gt;during&lt;/code&gt;). The engine then picks jobs from the queue on that schedule and dispatches them to the appropriate processor. The default is that a cron job executes once every hour, but some jobs may come in loads and loads of small tasks that don't require 1 hour to process (like processing a video file would) so the engine is able to accelerate this process by setting a cron to process job queues every (1min, 2min, 5min, etc, a custom time leap basically). This engine is also built with an auto-stop ability, if the specified amount of time (set in a &lt;code&gt;during&lt;/code&gt; property) is achieved, or no more jobs are pending to be processed, the engine stops and leaves the rest for the default cron job.&lt;/p&gt;

&lt;p&gt;This design gives full control over when and how aggressively the backend processes media, and other jobs in general like posting this very article in LinkedIn and Dev.to from my personal blog without having to manually write 3 articles every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Misaka Network: processDirectoryWebhook
&lt;/h3&gt;

&lt;p&gt;When the cron engine picks up a &lt;code&gt;PROCESS_DIRECTORY&lt;/code&gt; job, it calls &lt;code&gt;processDirectoryWebhook&lt;/code&gt;. This function is the orchestrator for everything that needs to happen when a directory is processed.&lt;/p&gt;

&lt;p&gt;Its steps, in order, are:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Resolve the path on disk.&lt;/strong&gt;&lt;br&gt;
The function takes the &lt;code&gt;path&lt;/code&gt; field from the Strapi entry and tries to access it on the filesystem. If the path does not exist at that location, it falls back to constructing a path from the parent directory's path and the entry's display name. This handles cases where the path was set slightly differently during entry creation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Scan the directory on disk (&lt;code&gt;scanDirectoryOnDisk&lt;/code&gt;).&lt;/strong&gt;&lt;br&gt;
This function reads the directory and categorizes every item inside it. Video files are separated into episodes with their version determined by file extension (&lt;code&gt;.mp4&lt;/code&gt; is V1, &lt;code&gt;.mkv&lt;/code&gt; and similar formats are V2). Child subdirectory names are collected. The presence of a &lt;code&gt;cover.jpg&lt;/code&gt; file is noted. And if a &lt;code&gt;metadata.json&lt;/code&gt; file is found, it is parsed to extract tags and a description for the directory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Process episodes in the directory (&lt;code&gt;processEpisodesInDirectory&lt;/code&gt;).&lt;/strong&gt;&lt;br&gt;
For each video file found on disk, the function queries Strapi to see if an episode entry already exists. If it does not, it creates one. V2 episodes are created with &lt;code&gt;is_processing: true&lt;/code&gt;, which immediately triggers an &lt;code&gt;entry.create&lt;/code&gt; webhook to Strapi, which the backend will queue as a &lt;code&gt;PROCESS_EPISODE&lt;/code&gt; job. If an episode entry already exists but has incorrect metadata state, the function marks it as &lt;code&gt;is_processing: true&lt;/code&gt; to trigger reprocessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Upload the directory cover (&lt;code&gt;uploadDirectoryCover&lt;/code&gt;).&lt;/strong&gt;&lt;br&gt;
If a &lt;code&gt;cover.jpg&lt;/code&gt; file was found, it is read from disk and uploaded to Strapi's media library via the REST upload endpoint. The returned media ID is stored for the finalization step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Resolve directory tags (&lt;code&gt;resolveDirectoryTags&lt;/code&gt;).&lt;/strong&gt;&lt;br&gt;
Tags from the &lt;code&gt;metadata.json&lt;/code&gt; file are resolved one by one. For each tag name, the function checks if a matching tag entry already exists in Strapi. If it does, its ID is reused. If it does not, a new tag is created. The resulting list of tag document IDs is stored for finalization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Process child directories (&lt;code&gt;processChildDirectories&lt;/code&gt;).&lt;/strong&gt;&lt;br&gt;
For each subdirectory name found on disk, the function checks if a directory entry already exists in Strapi with the matching path. If it does not exist, it creates a new directory entry with &lt;code&gt;is_processing: true&lt;/code&gt; and the current directory as its parent. If a matching entry already exists (perhaps from a previous partial import), it updates it to set &lt;code&gt;is_processing: true&lt;/code&gt; and attach the correct parent.&lt;/p&gt;

&lt;p&gt;The key insight here is that creating these child directory entries in Strapi with &lt;code&gt;is_processing: true&lt;/code&gt; is what fires the next round of webhooks. The backend is not calling itself recursively. It is delegating the continuation of the process to Strapi's event system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Finalize the directory (&lt;code&gt;finalizeDirectory&lt;/code&gt;).&lt;/strong&gt;&lt;br&gt;
Once all the above steps are complete, the function updates the original directory entry in Strapi with the clean display name (stripping the age rating prefixes), the determined age rating, the cover image ID, the tag IDs, the description, and &lt;code&gt;is_processing: false&lt;/code&gt;. This marks the directory as done and applies all the enriched metadata.&lt;/p&gt;

&lt;p&gt;Here is the complete flow of a single directory processing job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A["Job: PROCESS_DIRECTORY picked from queue"] --&amp;gt; B["Resolve real path on disk"]
    B --&amp;gt; C["scanDirectoryOnDisk"]
    C --&amp;gt; D["Episodes found"]
    C --&amp;gt; E["Child directories found"]
    C --&amp;gt; F["Cover found"]
    C --&amp;gt; G["metadata.json found"]

    D --&amp;gt; H["processEpisodesInDirectory"]
    H --&amp;gt; H1["For each episode: create/update in Strapi"]
    H1 --&amp;gt; H2["V2 episodes created with is_processing=true"]
    H2 --&amp;gt;|"Strapi fires entry.create"| Webhook1["New PROCESS_EPISODE job queued"]

    E --&amp;gt; I["processChildDirectories"]
    I --&amp;gt; I1["For each child: create/update in Strapi with is_processing=true"]
    I1 --&amp;gt;|"Strapi fires entry.create"| Webhook2["New PROCESS_DIRECTORY job queued"]

    F --&amp;gt; J["uploadDirectoryCover"]
    J --&amp;gt; J1["Cover uploaded to Strapi media library"]

    G --&amp;gt; K["resolveDirectoryTags"]
    K --&amp;gt; K1["Tags created or reused in Strapi"]

    H1 &amp;amp; I1 &amp;amp; J1 &amp;amp; K1 --&amp;gt; L["finalizeDirectory"]
    L --&amp;gt; L1["Update directory in Strapi with all metadata"]
    L1 --&amp;gt; L2["is_processing set to false"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Misaka Network: processEpisodeWebhook
&lt;/h3&gt;

&lt;p&gt;Episode processing is simpler but equally important. When the engine picks up a &lt;code&gt;PROCESS_EPISODE&lt;/code&gt; job, it calls &lt;code&gt;processEpisodeWebhook&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The function first checks whether processing is actually needed. V1 episodes (&lt;code&gt;.mp4&lt;/code&gt; files) do not need FFmpeg processing, as they are already in a web-friendly format. Episodes where &lt;code&gt;is_processing&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt; are also skipped, since they have already been processed.&lt;/p&gt;

&lt;p&gt;For V2 episodes that do need processing, the function constructs the full file path from the parent directory's path and the episode's display name and file type. It verifies the file exists on disk. Then it calls &lt;code&gt;processVideoFile&lt;/code&gt; from the FFmpeg service.&lt;/p&gt;

&lt;p&gt;The FFmpeg service does the heavy lifting: it uses &lt;code&gt;ffprobe&lt;/code&gt; to extract the stream metadata (audio tracks with their languages and codecs, subtitle tracks with their languages, codecs, and whether they are text-based or bitmap-based), then uses &lt;code&gt;ffmpeg&lt;/code&gt; to extract each audio track to &lt;code&gt;.m4a&lt;/code&gt; files and each subtitle track to &lt;code&gt;.vtt&lt;/code&gt; files, storing them in a mirrored directory structure under a &lt;code&gt;.v2&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;Once FFmpeg has finished, &lt;code&gt;processEpisodeWebhook&lt;/code&gt; updates the episode entry in Strapi with the extracted &lt;code&gt;languages_info&lt;/code&gt; metadata and sets &lt;code&gt;is_processing&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;. This is the episode's final state: it is now fully processed, ready to be streamed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The self-propagating loop in action
&lt;/h2&gt;

&lt;p&gt;To fully appreciate the elegance of this system, it helps to trace through a concrete example. Suppose I download a new anime series called "My Anime Series". The folder structure on disk looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/volumes/anime/My Anime Series/
    Season 1/
        Episode 01.mkv
        Episode 02.mkv
        cover.jpg
    Season 2/
        Episode 01.mkv
        cover.jpg
    cover.jpg
    metadata.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what happens when I create the top-level Directory entry in Strapi for "My Anime Series" and mark &lt;code&gt;is_processing: true&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant Me as Me (Smartphone)
    participant Strapi as Strapi V5
    participant Backend as Backend / Queue
    participant Engine as Cron Engine
    participant Disk as Disk

    Me-&amp;gt;&amp;gt;Strapi: Create "My Anime Series" directory (is_processing: true)
    Strapi-&amp;gt;&amp;gt;Backend: entry.create webhook
    Backend-&amp;gt;&amp;gt;Backend: Queue PROCESS_DIRECTORY job

    Engine-&amp;gt;&amp;gt;Backend: Pick up job
    Backend-&amp;gt;&amp;gt;Disk: scanDirectoryOnDisk("/volumes/anime/My Anime Series/")
    Disk--&amp;gt;&amp;gt;Backend: Episodes: none, Children: [Season 1, Season 2], cover.jpg, metadata.json

    Backend-&amp;gt;&amp;gt;Strapi: Create "Season 1" directory (is_processing: true)
    Strapi-&amp;gt;&amp;gt;Backend: entry.create webhook for Season 1
    Backend-&amp;gt;&amp;gt;Backend: Queue PROCESS_DIRECTORY job for Season 1

    Backend-&amp;gt;&amp;gt;Strapi: Create "Season 2" directory (is_processing: true)
    Strapi-&amp;gt;&amp;gt;Backend: entry.create webhook for Season 2
    Backend-&amp;gt;&amp;gt;Backend: Queue PROCESS_DIRECTORY job for Season 2

    Backend-&amp;gt;&amp;gt;Strapi: Upload cover.jpg for "My Anime Series"
    Backend-&amp;gt;&amp;gt;Strapi: Resolve tags from metadata.json
    Backend-&amp;gt;&amp;gt;Strapi: Finalize "My Anime Series" (is_processing: false)

    Engine-&amp;gt;&amp;gt;Backend: Pick up "Season 1" job
    Backend-&amp;gt;&amp;gt;Disk: scanDirectoryOnDisk("/volumes/anime/My Anime Series/Season 1/")
    Disk--&amp;gt;&amp;gt;Backend: Episodes: [Ep01.mkv, Ep02.mkv], cover.jpg

    Backend-&amp;gt;&amp;gt;Strapi: Create "Episode 01" (version: V2, is_processing: true)
    Strapi-&amp;gt;&amp;gt;Backend: entry.create webhook for Episode 01
    Backend-&amp;gt;&amp;gt;Backend: Queue PROCESS_EPISODE job for Episode 01

    Backend-&amp;gt;&amp;gt;Strapi: Create "Episode 02" (version: V2, is_processing: true)
    Strapi-&amp;gt;&amp;gt;Backend: entry.create webhook for Episode 02
    Backend-&amp;gt;&amp;gt;Backend: Queue PROCESS_EPISODE job for Episode 02

    Backend-&amp;gt;&amp;gt;Strapi: Upload cover.jpg for "Season 1"
    Backend-&amp;gt;&amp;gt;Strapi: Finalize "Season 1" (is_processing: false)

    Engine-&amp;gt;&amp;gt;Backend: Pick up Episode 01 job
    Backend-&amp;gt;&amp;gt;Disk: Run FFmpeg on Episode 01.mkv
    Backend-&amp;gt;&amp;gt;Strapi: Update Episode 01 with languages_info, is_processing: false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And so on for Season 2 and its episodes. The entire tree is processed without me touching anything after the initial entry creation in Strapi. I could be watching something on my phone while the backend quietly works through the queue in the background.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparing the two approaches
&lt;/h2&gt;

&lt;p&gt;Let me be direct about what changed and why it matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Responsibility and scope
&lt;/h3&gt;

&lt;p&gt;The original script was responsible for everything from start to finish: environment validation, disk scanning, Strapi querying, duplicate checking, FFmpeg execution, error tracking, and writing debug output. Each of these concerns was interleaved with the others in a single file. If you wanted to understand how the episode duplicate check worked, you had to read through directory processing logic to find it.&lt;/p&gt;

&lt;p&gt;The new system distributes responsibility cleanly. The controllers handle only webhook routing. The job queue handles only persistence. The cron engine handles only scheduling. The Misaka Network functions handle only specific aspects of media processing. Each file has one job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Maintainability
&lt;/h3&gt;

&lt;p&gt;Adding a new content type to the old script would require opening a 3000-plus line file and carefully threading new logic through the existing flow. In the new system, you add a new &lt;code&gt;uid&lt;/code&gt; check in the relevant controller, write a new service function, and register a new job type. The existing code does not need to change.&lt;/p&gt;

&lt;p&gt;Fixing a bug is similarly easier. If there is an issue with how cover images are uploaded, the problem is isolated to &lt;code&gt;uploadDirectoryCover.ts&lt;/code&gt;. You do not need to reason about the entire processing pipeline to fix it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manual intervention
&lt;/h3&gt;

&lt;p&gt;The old workflow required SSH access, command-line execution, and often manual data entry in the Strapi admin panel. If I was away from my desk, there was nothing I could do.&lt;/p&gt;

&lt;p&gt;With the new system, my smartphone is sufficient. I open the Strapi admin panel in my browser, create or update an entry, and the backend handles the rest. I can add a full new anime series while commuting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scalability and resilience
&lt;/h3&gt;

&lt;p&gt;The old script was an all-or-nothing operation. If it failed halfway through because FFmpeg crashed or the network blipped, you had to figure out what had already been processed and re-run the script with the right settings to avoid duplicating work. The state was entirely in memory during the run.&lt;/p&gt;

&lt;p&gt;The new system is naturally resilient. Each job in the queue is an independent unit. If the FFmpeg process for Episode 03 crashes, the job is marked as failed and can be retried. Episodes 01, 02, 04, and 05 are not affected. The job queue persists across restarts, so if the server goes down mid-processing, the remaining jobs are still there when it comes back up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code volume vs. complexity
&lt;/h3&gt;

&lt;p&gt;It might seem counterintuitive that I described the new webhook system as "much more complex in terms of code" while also calling it easier to maintain. More files, more interfaces, more type definitions. But the complexity in the new system is the right kind of complexity. Each file is small, focused, and understandable in isolation. The overall system is more sophisticated, but no single piece of it is overwhelming.&lt;/p&gt;

&lt;p&gt;The old script had its complexity compressed into one place. The new system has its complexity spread across many small, decoupled pieces. Reading the old script meant reading everything. Reading the new system means reading only the piece you care about.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Rewriting the initialization system as a webhook-driven, event-based pipeline taught me several things that I think are worth sharing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Events are a natural fit for hierarchical data.&lt;/strong&gt; A tree is inherently recursive. You process a node, and processing it produces children that need to be processed. An event-driven system handles this naturally because each new child entry fires a new event, which queues a new job. You never need an explicit recursion stack or a pending-items array. The system's own feedback loop handles it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fast webhook handlers are not optional.&lt;/strong&gt; A webhook handler that does too much work inline is a footgun. If the handler times out, Strapi (or any other system) might retry the webhook, leading to duplicate jobs. By keeping the handler lightweight (validate, queue, respond), you avoid this problem entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A job queue is the right bridge between events and work.&lt;/strong&gt; Without the queue, you would need to process everything inline in the webhook handler, which defeats the purpose. The queue absorbs the event at network speed and lets the processing happen at disk speed, which is necessarily slower when FFmpeg is involved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Naming and file organization carry enormous cognitive weight.&lt;/strong&gt; The &lt;code&gt;processChildDirectories&lt;/code&gt;, &lt;code&gt;finalizeDirectory&lt;/code&gt;, &lt;code&gt;resolveDirectoryTags&lt;/code&gt;, and &lt;code&gt;scanDirectoryOnDisk&lt;/code&gt; functions are not just code. Their names are documentation. When I open the &lt;code&gt;processDirectoryWebhook.ts&lt;/code&gt; orchestrator and see those function calls laid out in order, I understand the full processing pipeline at a glance. That is something the original monolithic script could never offer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://chaldea.foundation/blog/unlimited-blades-work" rel="noopener noreferrer"&gt;Unlimited Blades Work&lt;/a&gt; started as a personal project to watch anime and movies from my own server. The first version of everything was built to work, not to last. The initialization script was a perfect example: it solved the immediate problem, but it was not designed to grow.&lt;/p&gt;

&lt;p&gt;The webhook system is better in every dimension that matters for a long-lived project. It is faster to debug, easier to extend, more resilient to failure, and more pleasant to use from a purely operational standpoint. Being able to add new content from my smartphone, without opening a terminal or running a script, feels like the difference between driving a car and pushing one.&lt;/p&gt;

&lt;p&gt;If you are building a system that needs to react to changes in a CMS, a database, or any external service, I strongly encourage you to look at webhooks and event-driven design before reaching for the procedural script. The upfront cost of thinking in terms of events and handlers pays itself back quickly, and the resulting architecture is one you can actually be proud of six months later.&lt;/p&gt;

&lt;p&gt;The Misaka Network is not perfect. There are edge cases I have not handled, and the cron engine could be smarter about prioritizing jobs. But it is a system I can reason about, a system I can extend, and a system I can operate from anywhere. For a personal project running on a self-hosted server, that is exactly what I need.&lt;/p&gt;

&lt;p&gt;Find me on:&lt;br&gt;
&lt;a href="https://chaldea.foundation/blog" rel="noopener noreferrer"&gt;Chaldea Foundation News&lt;/a&gt;&lt;br&gt;
&lt;a href="https://corvalangonzalo.com" rel="noopener noreferrer"&gt;My Portfolio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webhooks</category>
      <category>eventdrivenarchitecture</category>
      <category>softwareengineering</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>2013 "Trash Can" Mac Pro: The Ultimate Home Server</title>
      <dc:creator>Gonzalo Salvador Corvalán</dc:creator>
      <pubDate>Sun, 26 Jul 2026 13:54:44 +0000</pubDate>
      <link>https://dev.to/themrcorvy/2013-trash-can-mac-pro-the-ultimate-home-server-3ih</link>
      <guid>https://dev.to/themrcorvy/2013-trash-can-mac-pro-the-ultimate-home-server-3ih</guid>
      <description>&lt;p&gt;Look at this machine.&lt;/p&gt;

&lt;p&gt;Seriously, look at it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;          .-----------------.
         /     _________     \
        /    .'         '.    \
       |    /             \    |
       |   |               |   |
       |   |   Mac Pro     |   |
       |   |   6,1         |   |
       |   |  "Trash Can"  |   |
       |    \             /    |
        \    '._________.'    /
         \                   /
          '-----------------'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is black. It is shiny. It is a perfect cylinder of aluminum. It is, without a doubt, one of the most stunning pieces of industrial design ever to sit on a desk. This is the &lt;strong&gt;Mac Pro 6,1&lt;/strong&gt;, launched in late 2013 and famously dubbed the "Trash Can." Jony Ive’s design team wanted to completely reinvent the professional workstation. They packed it with dual workstation GPUs, a single massive thermal core, and a chassis so compact it was barely larger than a coffee pot.&lt;/p&gt;

&lt;p&gt;And... it was a monumental commercial disaster.&lt;/p&gt;

&lt;p&gt;Let's be completely honest: by today's standards, this machine is not very capable. We are thirteen years past its launch date. A modern $150 mini PC can easily outrun it while drawing a fraction of the power. Under load, this cylinder consumes so much electricity that it basically doubles as a small space heater in my room.&lt;/p&gt;

&lt;p&gt;But I do not care. I absolutely love it.&lt;/p&gt;

&lt;p&gt;I still prefer running my entire homelab on this machine over any boring gray box or cloud instance. Why? Because of how &lt;em&gt;incredible&lt;/em&gt; it looks sitting right there on my desk or next to the TV. When it hums in the corner, with its soft light illuminating the ports on the back, it feels like I have a piece of hardware from the future—a miniature, sleek datacenter right inside my living room.&lt;/p&gt;

&lt;p&gt;Apple boxed themselves into a corner. The thermal design assumed the industry would move toward dual, lower-power graphics cards. Instead, the industry went the exact opposite direction: single, massive, power-hungry GPUs. The Mac Pro couldn't handle the heat. It couldn't be upgraded. It sat frozen in time for six years while Apple stayed silent. It got so bad that in 2017, Apple executives held an unprecedented meeting to apologize to professional users, admitting the thermal core design was a dead end.&lt;/p&gt;

&lt;p&gt;But here is the beautiful irony of technology: &lt;strong&gt;a failed workstation makes for a legendary home server.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I bought mine for around &lt;strong&gt;$200 USD&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the story of how I took a piece of Jony Ive's sculpture, wiped macOS, installed a headless Linux OS, upgraded the hardware to its absolute physical limits, and turned it into an automated, Dockerized app-hosting powerhouse.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hardware: Swapping Xeons and Shoving in 64GB of RAM
&lt;/h2&gt;

&lt;p&gt;When this shiny cylinder first arrived on my desk, the specs were looking pretty dated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU:&lt;/strong&gt; 4-core Intel Xeon E5 processor (v2)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAM:&lt;/strong&gt; 12GB of DDR3 RAM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; A proprietary 196GB Apple PCIe SSD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That might be fine for a basic file server, but I'm hosting complex web applications, media streaming platforms, databases, and monitoring tools. I need &lt;em&gt;power&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So, I did what Apple said you couldn't do: &lt;strong&gt;I upgraded it myself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Taking apart the Mac Pro 6,1 is an absolute joy. You slide a release switch on the back, and the outer aluminum sleeve slides right off. Underneath, you are greeted by the triangular thermal core. It is a masterpiece of compact engineering. The motherboard, graphics cards, and CPU are mounted to three sides of a triangular heatsink, with a single large fan at the top drawing heat upward.&lt;/p&gt;

&lt;p&gt;Here is how I upgraded the guts of this machine:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The CPU swap: 4 cores to 10 cores
&lt;/h3&gt;

&lt;p&gt;I removed the heatsink mounts, cleaned off the old thermal paste, and replaced the weak 4-core processor with a massive &lt;strong&gt;10-core Intel Xeon CPU&lt;/strong&gt;. Suddenly, I went from 8 execution threads to &lt;strong&gt;20 execution threads&lt;/strong&gt;. For multitasking and handling background jobs, this is a night-and-day difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The RAM: 64GB of ECC memory
&lt;/h3&gt;

&lt;p&gt;I replaced the old 12GB of RAM with &lt;strong&gt;64GB of ECC DDR3 RAM&lt;/strong&gt;. Here is a pro-tip: because this machine uses older DDR3 ECC server memory, you can buy it on eBay for next to nothing. Corporate data centers decommission their old servers and dump this high-grade ECC RAM in bulk. I got 64GB of enterprise-grade, error-correcting memory for the price of a cheap dinner. ECC is vital because it prevents silent data corruption, keeping my databases and files safe from memory-level bit flips.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The cache SSD: 2TB NVMe
&lt;/h3&gt;

&lt;p&gt;The proprietary Apple SSD connector is a pain, but you can buy a simple &lt;strong&gt;Sintech M.2 NVMe adapter&lt;/strong&gt; online for about $15. I plugged that adapter in, and installed a fast &lt;strong&gt;2TB NVMe M.2 SSD&lt;/strong&gt;. Now we have high-speed, local solid-state storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The GPUs: Dual AMD FirePro D300s
&lt;/h3&gt;

&lt;p&gt;These graphics cards are integrated into the proprietary board design and cannot be upgraded. But for this build? &lt;strong&gt;I don’t care.&lt;/strong&gt; I'm running a headless server, not rendering 3D models or playing games. The GPUs sit completely idle, drawing minimal power (well, what can be called minimal for this pc), while the Xeon CPU does all the heavy lifting.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Storage monster: 24TB array + NVMe caching
&lt;/h3&gt;

&lt;p&gt;Of course, 2TB of internal storage isn't enough to hold an entire digital life. To expand it, I hooked up a high-speed USB hub loaded with a massive &lt;strong&gt;24TB Hard Drive array&lt;/strong&gt; &lt;em&gt;(which I plan to expand to 48 since the hub has 2 slots ;)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;But there’s a catch. Reading high-definition media files from spinning hard drives over a USB hub introduces latency. If multiple users are accessing files, it can cause buffering. To solve this, I designed a caching system: the massive files live on the 24TB USB storage, but I use the ultra-fast internal 2TB NVMe SSD to cache active files, making sure &lt;strong&gt;Unlimited Blades Work&lt;/strong&gt; respond instantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I rejected Jellyfin
&lt;/h2&gt;

&lt;p&gt;When developers start setting up a home server, the first thing they usually do is download ready-made solutions. If you want a media server, you install Plex or Jellyfin. If you want a video conferencing tool, you use Discord or Zoom. If you want photo storage, you pay for iCloud or Google Photos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But I opted out&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Using off-the-shelf software is great if your only goal is utility. But if your goal is learning, personal growth, and becoming a top-tier engineer, using someone else's pre-packaged service is a missed opportunity."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of using Jellyfin, I decided to build my own media streaming platform from scratch: &lt;strong&gt;Unlimited Blades Work&lt;/strong&gt;. Instead of using Zoom or Jitsi, I built my own videochat platform: &lt;strong&gt;Conference Room&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why? Because writing your own custom apps forces you to solve real-world system architecture problems. You have to learn:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How to stream raw video files over HTTP chunk by chunk.&lt;/li&gt;
&lt;li&gt;How to implement low-latency WebRTC connections and manage STUN/TURN servers.&lt;/li&gt;
&lt;li&gt;How to write custom caching systems to balance slow HDD storage with fast NVMe drives.&lt;/li&gt;
&lt;li&gt;How to manage data synchronization pipelines, API gateways, and microservices.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By rejecting the "easy path," I turned my home server into a personal software engineering laboratory. Every feature I build teaches me something that applies directly to corporate, production-grade applications.&lt;/p&gt;




&lt;h2&gt;
  
  
  Raw, headless Ubuntu 24.04 Server
&lt;/h2&gt;

&lt;p&gt;If you are setting up a home server, you might be tempted to install a desktop environment or a heavy hypervisor with a graphical dashboard.&lt;/p&gt;

&lt;p&gt;Don't do it.&lt;/p&gt;

&lt;p&gt;I installed &lt;strong&gt;Ubuntu 24.04 LTS (Server Edition)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means there is &lt;strong&gt;no graphical user interface (GUI)&lt;/strong&gt;. No desktop, no taskbar, no mouse pointer. The graphics cards are completely asleep. When I boot the machine, it is just a black screen with a login prompt.&lt;/p&gt;

&lt;p&gt;I access the Mac Pro almost exclusively via &lt;strong&gt;SSH&lt;/strong&gt; from my personal PC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh user@mac-pro.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Managing a server entirely through the command line is the absolute best way to learn systems administration. Everything I do is handled through commands, custom shell pipelines, or analyzing log files.&lt;/p&gt;

&lt;p&gt;If something goes wrong, I don't look for a button to click. I search the logs using &lt;code&gt;grep&lt;/code&gt;, analyze system performance with &lt;code&gt;htop&lt;/code&gt;, and write automation scripts to fix the issue. It forces you to understand the Linux file system, user permissions, and network sockets at a deep level.&lt;/p&gt;




&lt;h2&gt;
  
  
  The caching pipeline
&lt;/h2&gt;

&lt;p&gt;To make video streaming blazing fast without overloading the USB connection to the 24TB HDD array, I wrote a custom caching mechanism inside the Express.js app.&lt;/p&gt;

&lt;p&gt;See the mermaid diagram for this at my personal blog: &lt;a href="https://chaldea.foundation/blog/homelab" rel="noopener noreferrer"&gt;Chaldea Foundation News: Homelab&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The container strategy: Docker "Pods"
&lt;/h2&gt;

&lt;p&gt;To keep this server clean and automated, I containerized all my projects using &lt;strong&gt;Docker&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I don't install software directly on the host operating system. If you install Node.js, databases, and CMS systems directly on the host, you end up with conflicting dependencies, security risks, and a system that is impossible to migrate if the hardware dies.&lt;/p&gt;

&lt;p&gt;With Docker, every app lives in its own isolated environment. I organize these containers into what I call &lt;strong&gt;"pods."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While these aren't official Kubernetes pods (which are groups of containers sharing network namespaces), I use the term because I group isolated, small containers together to fulfill a single task. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My database "pod" runs MySQL alongside Adminer.&lt;/li&gt;
&lt;li&gt;My streaming "pod" runs the Express backend and cache scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the entrance of this setup sits the &lt;strong&gt;Pangolin reverse-proxy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Pangolin is the gatekeeper. It listens to the ports exposed to the internet, manages SSL/TLS certificates, and routes incoming web requests to the correct internal Docker container based on the subdomain. This keeps my internal network completely hidden from the public internet while making my apps accessible worldwide.&lt;/p&gt;

&lt;p&gt;Earlier I said that these are not official Kubernetes pods. Currently I'm not in that face... Yet. But I'm planning to migrate this container chaos into a well-orchestrated Kubernetes just for fun :)&lt;/p&gt;




&lt;h2&gt;
  
  
  The application stack: What's running in the Cylinder?
&lt;/h2&gt;

&lt;p&gt;This Mac Pro is currently hosting a mix of custom-built software and open-source infrastructure:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Unlimited Blades Work (&lt;a href="https://unlimitedblades.work" rel="noopener noreferrer"&gt;unlimitedblades.work&lt;/a&gt;)
&lt;/h3&gt;

&lt;p&gt;This is my custom-built streaming application. It consists of three separate applications working in harmony:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Frontend:&lt;/strong&gt; A Next.js app utilizing Server-Side Rendering (SSR) for instant load times and SEO optimization, hosted on Vercel's global CDN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The CMS:&lt;/strong&gt; A &lt;strong&gt;Strapi V5 headless CMS&lt;/strong&gt; hosted on the Mac Pro. It manages all the database relationships, video metadata, user logins, and application settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Streaming Backend:&lt;/strong&gt; An &lt;strong&gt;Express.js app&lt;/strong&gt; (which lives in my codebase as &lt;code&gt;private-cloud-backend&lt;/code&gt;) hosted on the Mac Pro. This app handles the heavy lifting: video streaming, offline video processing/transcoding, and processing webhooks to automate tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Conference Room (&lt;a href="https://conference.chaldea.foundation" rel="noopener noreferrer"&gt;conference.chaldea.foundation&lt;/a&gt;)
&lt;/h3&gt;

&lt;p&gt;This is a videochat app I built so I could call my girlfriend when we were in a long-distance relationship. I didn't want to rely on third-party platforms that track your data or have call limits. It consists of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A client-side &lt;strong&gt;React.js&lt;/strong&gt; web application.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;PeerJS server&lt;/strong&gt; hosted on the Mac Pro to handle WebRTC signaling, allowing direct peer-to-peer browser connections for encrypted, low-latency audio/video.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;Express.js&lt;/strong&gt; app to manage active rooms and API routes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s completely private, self-hosted, and runs beautifully on my 10-core piece of art called Tris Megistus (Yes, that what I'm calling my trash can mac pro).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Log management: Grafana + Loki
&lt;/h3&gt;

&lt;p&gt;Because there is no graphical interface on the OS, monitoring logs across multiple Docker containers can be a nightmare. I don't want to run &lt;code&gt;docker logs&lt;/code&gt; on ten different containers manually.&lt;br&gt;
Instead, I deploy &lt;strong&gt;Loki&lt;/strong&gt; and &lt;strong&gt;Grafana&lt;/strong&gt;. Loki acts as a centralized log aggregator that scrapes logs from all running Docker containers. Grafana pulls data from Loki and displays it in a beautiful, real-time dashboard. If a database query fails or a webhook fails, I see it immediately on a visual graph.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Database: MySQL + Adminer
&lt;/h3&gt;

&lt;p&gt;The data engine for my custom apps. I run MySQL for data storage and Adminer as a lightweight web interface to manage tables. They run together in the same container network, isolated from the rest of the system for security.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Photos: Immich
&lt;/h3&gt;

&lt;p&gt;I use &lt;strong&gt;Immich&lt;/strong&gt; to backup and store all of our photos. It is a self-hosted alternative to Google Photos and iCloud. It features a mobile app that automatically backs up our photos to the server when we connect to our home network. It even runs local machine learning models on the Mac Pro's CPU for facial recognition and object search.&lt;/p&gt;
&lt;h3&gt;
  
  
  6. The Future: Lossless Music Streaming
&lt;/h3&gt;

&lt;p&gt;I’m planning to deploy a high-quality, lossless music streaming application in the future to stream FLAC files directly to my audio systems. The 64GB of RAM and massive storage pool make this hardware ideal for handling high-resolution audio streaming.&lt;/p&gt;


&lt;h2&gt;
  
  
  The custom backup engine: GCP &amp;amp; Google Drive API
&lt;/h2&gt;

&lt;p&gt;A home server is only as good as its backup strategy. If a hard drive crashes, or if there is a power outage that corrupts the filesystem, you stand to lose years of work.&lt;/p&gt;

&lt;p&gt;I didn't want to rely on complex, proprietary backup tools. Instead, I built a custom backup pipeline directly into the Express.js backend (&lt;code&gt;private-cloud-backend&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Here is an example of how the weekly automated backup script looks like. It dumps the database, packages the files, and uses the GCP Google Drive API to push the archive to the cloud:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;exec&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;googleapis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cron&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node-cron&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BACKUP_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/tmp/backups&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STRAPI_UPLOADS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/mnt/usb-hdd/strapi/uploads&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LOG_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/var/log/docker-apps&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GOOGLE_DRIVE_FOLDER_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your_gdrive_folder_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Authenticate with GCP Service Account&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GoogleAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;keyFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gcp-credentials.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.googleapis.com/auth/drive.file&lt;/span&gt;&lt;span class="dl"&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runBackup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[BACKUP] Starting weekly backup pipeline...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;:.&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dbDumpFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`db-dump-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.sql`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tarballFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`backup-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 1. MySQL Dump via Docker execution&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;executeCmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;`docker exec mysql-container mysqldump -u root -p"" chaldea_foundation &amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dbDumpFile&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[BACKUP] MySQL Database dump complete.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 2. Compress Database, Strapi uploads, and logs into a tarball&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;executeCmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;`tar -czf &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tarballFile&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dbDumpFile&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;STRAPI_UPLOADS&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;LOG_DIR&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[BACKUP] Compression of assets and logs complete.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 3. Upload to Google Drive&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;drive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drive&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;media&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/gzip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createReadStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tarballFile&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileMetadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`macpro-backup-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;parents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_DRIVE_FOLDER_ID&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;drive&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&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="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fileMetadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;media&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;media&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;`[BACKUP] Successfully uploaded backup to Google Drive. File ID: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 4. Cleanup local temp files&lt;/span&gt;
        &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dbDumpFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tarballFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[BACKUP] Local cleanup finished.&lt;/span&gt;&lt;span class="dl"&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[BACKUP CRITICAL ERROR] Pipeline failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;executeCmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;stderr&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Run every Sunday at 3:00 AM&lt;/span&gt;
&lt;span class="nx"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0 3 * * 0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;runBackup&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;This custom pipeline ensures that even if the physical Mac Pro is destroyed, my databases, media, and configurations are safe in the cloud, completely automated and free of charge.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Grand Plan: Migrating to Kubernetes (the career cheat code)
&lt;/h2&gt;

&lt;p&gt;Remember earlier when I told you that I was planning about migrating to Kubernetes?&lt;/p&gt;

&lt;p&gt;Right now, my Docker setup is incredibly stable, lightweight, and easy to maintain. It does everything I need.&lt;/p&gt;

&lt;p&gt;But I am planning to migrate the entire stack to &lt;strong&gt;Kubernetes (K8s)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let's be completely real for a moment. Running Kubernetes on a single-node home server is absolute, ridiculous overkill. It introduces massive complexity, forces you to write endless lines of YAML manifest files, and requires you to manage K8s networking, persistent volume claims, and ingress controllers.&lt;/p&gt;

&lt;p&gt;So why do it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resume-Driven Development.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are looking to advance your career as a backend developer or DevOps engineer, landing a high-salary job requires more than just knowing how to write code. Companies want engineers who understand cloud infrastructure, microservices, and container orchestration.&lt;/p&gt;

&lt;p&gt;When you sit in a job interview and they ask about container orchestration, you don't want to give a textbook answer. You want to look them in the eye and say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I manage my own bare-metal Kubernetes node on an upgraded 2013 Mac Pro. I architected my applications into separate deployments, configured persistent volume claims to bridge my local NVMe caching SSD with a 24TB storage array, and write custom manifests to manage network ingress and database pods."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Boom. That instantly sets you apart from 99% of other candidates. It proves you aren't afraid of complex infrastructure, networking, or systems administration. It turns your hobby homelab into a professional talking point that commands respect—and a higher salary.&lt;/p&gt;




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

&lt;p&gt;Sure, the 2013 Mac Pro is 13 years old. Sure, it is objectively underpowered by today’s standards and consumes enough electricity to heat my apartment in the winter. But every single time I walk into the living room and see that sleek, futuristic black cylinder sitting next to the TV, I smile. It doesn't look like a standard, boring PC. It looks like a high-tech artifact from a sci-fi movie—a miniature personal datacenter humming quietly under my roof.&lt;/p&gt;

&lt;p&gt;It failed as a professional workstation, but it succeeds spectacularly as a homelab. By opening it up, upgrading the CPU and RAM, installing a headless Linux OS, and dockerizing your applications, you can build a private cloud that is unique, beautiful, and completely under your control.&lt;/p&gt;

&lt;p&gt;Stop renting virtual machines in the cloud for simple projects. Find some cheap, recycled hardware, open up the terminal, write a Dockerfile, and start building.&lt;/p&gt;

&lt;p&gt;The skills you gain by configuring, breaking, and fixing your own infrastructure will pay off for the rest of your engineering career.&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>2013macpro</category>
      <category>ubuntuserver</category>
      <category>docker</category>
    </item>
    <item>
      <title>2013 "Trash Can" Mac Pro: The Ultimate Home Server</title>
      <dc:creator>Gonzalo Salvador Corvalán</dc:creator>
      <pubDate>Sun, 26 Jul 2026 11:37:44 +0000</pubDate>
      <link>https://dev.to/themrcorvy/2013-trash-can-mac-pro-the-ultimate-home-server-498c</link>
      <guid>https://dev.to/themrcorvy/2013-trash-can-mac-pro-the-ultimate-home-server-498c</guid>
      <description>&lt;p&gt;Look at this machine.&lt;/p&gt;

&lt;p&gt;Seriously, look at it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;          .-----------------.
         /     _________     \
        /    .'         '.    \
       |    /             \    |
       |   |               |   |
       |   |   Mac Pro     |   |
       |   |   6,1         |   |
       |   |  "Trash Can"  |   |
       |    \             /    |
        \    '._________.'    /
         \                   /
          '-----------------'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is black. It is shiny. It is a perfect cylinder of aluminum. It is, without a doubt, one of the most stunning pieces of industrial design ever to sit on a desk. This is the &lt;strong&gt;Mac Pro 6,1&lt;/strong&gt;, launched in late 2013 and famously dubbed the "Trash Can." Jony Ive’s design team wanted to completely reinvent the professional workstation. They packed it with dual workstation GPUs, a single massive thermal core, and a chassis so compact it was barely larger than a coffee pot.&lt;/p&gt;

&lt;p&gt;And... it was a monumental commercial disaster.&lt;/p&gt;

&lt;p&gt;Let's be completely honest: by today's standards, this machine is not very capable. We are thirteen years past its launch date. A modern $150 mini PC can easily outrun it while drawing a fraction of the power. Under load, this cylinder consumes so much electricity that it basically doubles as a small space heater in my room.&lt;/p&gt;

&lt;p&gt;But I do not care. I absolutely love it.&lt;/p&gt;

&lt;p&gt;I still prefer running my entire homelab on this machine over any boring gray box or cloud instance. Why? Because of how &lt;em&gt;incredible&lt;/em&gt; it looks sitting right there on my desk or next to the TV. When it hums in the corner, with its soft light illuminating the ports on the back, it feels like I have a piece of hardware from the future—a miniature, sleek datacenter right inside my living room.&lt;/p&gt;

&lt;p&gt;Apple boxed themselves into a corner. The thermal design assumed the industry would move toward dual, lower-power graphics cards. Instead, the industry went the exact opposite direction: single, massive, power-hungry GPUs. The Mac Pro couldn't handle the heat. It couldn't be upgraded. It sat frozen in time for six years while Apple stayed silent. It got so bad that in 2017, Apple executives held an unprecedented meeting to apologize to professional users, admitting the thermal core design was a dead end.&lt;/p&gt;

&lt;p&gt;But here is the beautiful irony of technology: &lt;strong&gt;a failed workstation makes for a legendary home server.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I bought mine for around &lt;strong&gt;$200 USD&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the story of how I took a piece of Jony Ive's sculpture, wiped macOS, installed a headless Linux OS, upgraded the hardware to its absolute physical limits, and turned it into an automated, Dockerized app-hosting powerhouse.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Farticles%2Fbdhofi4om9xgylum0f12.jpeg" class="article-body-image-wrapper"&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%2Farticles%2Fbdhofi4om9xgylum0f12.jpeg" alt=" " width="800" height="1066"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hardware: Swapping Xeons and Shoving in 64GB of RAM
&lt;/h2&gt;

&lt;p&gt;When this shiny cylinder first arrived on my desk, the specs were looking pretty dated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU:&lt;/strong&gt; 4-core Intel Xeon E5 processor (v2)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAM:&lt;/strong&gt; 12GB of DDR3 RAM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; A proprietary 196GB Apple PCIe SSD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That might be fine for a basic file server, but I'm hosting complex web applications, media streaming platforms, databases, and monitoring tools. I need &lt;em&gt;power&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So, I did what Apple said you couldn't do: &lt;strong&gt;I upgraded it myself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Taking apart the Mac Pro 6,1 is an absolute joy. You slide a release switch on the back, and the outer aluminum sleeve slides right off. Underneath, you are greeted by the triangular thermal core. It is a masterpiece of compact engineering. The motherboard, graphics cards, and CPU are mounted to three sides of a triangular heatsink, with a single large fan at the top drawing heat upward.&lt;/p&gt;

&lt;p&gt;Here is how I upgraded the guts of this machine:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The CPU swap: 4 cores to 10 cores
&lt;/h3&gt;

&lt;p&gt;I removed the heatsink mounts, cleaned off the old thermal paste, and replaced the weak 4-core processor with a massive &lt;strong&gt;10-core Intel Xeon CPU&lt;/strong&gt;. Suddenly, I went from 8 execution threads to &lt;strong&gt;20 execution threads&lt;/strong&gt;. For multitasking and handling background jobs, this is a night-and-day difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The RAM: 64GB of ECC memory
&lt;/h3&gt;

&lt;p&gt;I replaced the old 12GB of RAM with &lt;strong&gt;64GB of ECC DDR3 RAM&lt;/strong&gt;. Here is a pro-tip: because this machine uses older DDR3 ECC server memory, you can buy it on eBay for next to nothing. Corporate data centers decommission their old servers and dump this high-grade ECC RAM in bulk. I got 64GB of enterprise-grade, error-correcting memory for the price of a cheap dinner. ECC is vital because it prevents silent data corruption, keeping my databases and files safe from memory-level bit flips.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The cache SSD: 2TB NVMe
&lt;/h3&gt;

&lt;p&gt;The proprietary Apple SSD connector is a pain, but you can buy a simple &lt;strong&gt;Sintech M.2 NVMe adapter&lt;/strong&gt; online for about $15. I plugged that adapter in, and installed a fast &lt;strong&gt;2TB NVMe M.2 SSD&lt;/strong&gt;. Now we have high-speed, local solid-state storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The GPUs: Dual AMD FirePro D300s
&lt;/h3&gt;

&lt;p&gt;These graphics cards are integrated into the proprietary board design and cannot be upgraded. But for this build? &lt;strong&gt;I don’t care.&lt;/strong&gt; I'm running a headless server, not rendering 3D models or playing games. The GPUs sit completely idle, drawing minimal power (well, what can be called minimal for this pc), while the Xeon CPU does all the heavy lifting.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Storage monster: 24TB array + NVMe caching
&lt;/h3&gt;

&lt;p&gt;Of course, 2TB of internal storage isn't enough to hold an entire digital life. To expand it, I hooked up a high-speed USB hub loaded with a massive &lt;strong&gt;24TB Hard Drive array&lt;/strong&gt; &lt;em&gt;(which I plan to expand to 48 since the hub has 2 slots ;)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;But there’s a catch. Reading high-definition media files from spinning hard drives over a USB hub introduces latency. If multiple users are accessing files, it can cause buffering. To solve this, I designed a caching system: the massive files live on the 24TB USB storage, but I use the ultra-fast internal 2TB NVMe SSD to cache active files, making sure &lt;strong&gt;Unlimited Blades Work&lt;/strong&gt; respond instantly.&lt;/p&gt;




&lt;p&gt;&lt;a href="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%2Farticles%2Fwzcy3psw7piysjyzw93l.jpeg" class="article-body-image-wrapper"&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%2Farticles%2Fwzcy3psw7piysjyzw93l.jpeg" alt=" " width="800" height="1066"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I rejected Jellyfin
&lt;/h2&gt;

&lt;p&gt;When developers start setting up a home server, the first thing they usually do is download ready-made solutions. If you want a media server, you install Plex or Jellyfin. If you want a video conferencing tool, you use Discord or Zoom. If you want photo storage, you pay for iCloud or Google Photos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But I opted out&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Using off-the-shelf software is great if your only goal is utility. But if your goal is learning, personal growth, and becoming a top-tier engineer, using someone else's pre-packaged service is a missed opportunity."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of using Jellyfin, I decided to build my own media streaming platform from scratch: &lt;strong&gt;Unlimited Blades Work&lt;/strong&gt;. Instead of using Zoom or Jitsi, I built my own videochat platform: &lt;strong&gt;Conference Room&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why? Because writing your own custom apps forces you to solve real-world system architecture problems. You have to learn:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How to stream raw video files over HTTP chunk by chunk.&lt;/li&gt;
&lt;li&gt;How to implement low-latency WebRTC connections and manage STUN/TURN servers.&lt;/li&gt;
&lt;li&gt;How to write custom caching systems to balance slow HDD storage with fast NVMe drives.&lt;/li&gt;
&lt;li&gt;How to manage data synchronization pipelines, API gateways, and microservices.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By rejecting the "easy path," I turned my home server into a personal software engineering laboratory. Every feature I build teaches me something that applies directly to corporate, production-grade applications.&lt;/p&gt;




&lt;h2&gt;
  
  
  Raw, headless Ubuntu 24.04 Server
&lt;/h2&gt;

&lt;p&gt;If you are setting up a home server, you might be tempted to install a desktop environment or a heavy hypervisor with a graphical dashboard.&lt;/p&gt;

&lt;p&gt;Don't do it.&lt;/p&gt;

&lt;p&gt;I installed &lt;strong&gt;Ubuntu 24.04 LTS (Server Edition)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means there is &lt;strong&gt;no graphical user interface (GUI)&lt;/strong&gt;. No desktop, no taskbar, no mouse pointer. The graphics cards are completely asleep. When I boot the machine, it is just a black screen with a login prompt.&lt;/p&gt;

&lt;p&gt;I access the Mac Pro almost exclusively via &lt;strong&gt;SSH&lt;/strong&gt; from my personal PC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh user@mac-pro.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Managing a server entirely through the command line is the absolute best way to learn systems administration. Everything I do is handled through commands, custom shell pipelines, or analyzing log files.&lt;/p&gt;

&lt;p&gt;If something goes wrong, I don't look for a button to click. I search the logs using &lt;code&gt;grep&lt;/code&gt;, analyze system performance with &lt;code&gt;htop&lt;/code&gt;, and write automation scripts to fix the issue. It forces you to understand the Linux file system, user permissions, and network sockets at a deep level.&lt;/p&gt;




&lt;h2&gt;
  
  
  The caching pipeline
&lt;/h2&gt;

&lt;p&gt;To make video streaming blazing fast without overloading the USB connection to the 24TB HDD array, I wrote a custom caching mechanism inside the Express.js app.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Farticles%2F8gfv4r018t7hym3frzcp.png" class="article-body-image-wrapper"&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%2Farticles%2F8gfv4r018t7hym3frzcp.png" alt=" " width="799" height="463"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The container strategy: Docker "Pods"
&lt;/h2&gt;

&lt;p&gt;To keep this server clean and automated, I containerized all my projects using &lt;strong&gt;Docker&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I don't install software directly on the host operating system. If you install Node.js, databases, and CMS systems directly on the host, you end up with conflicting dependencies, security risks, and a system that is impossible to migrate if the hardware dies.&lt;/p&gt;

&lt;p&gt;With Docker, every app lives in its own isolated environment. I organize these containers into what I call &lt;strong&gt;"pods."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While these aren't official Kubernetes pods (which are groups of containers sharing network namespaces), I use the term because I group isolated, small containers together to fulfill a single task. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My database "pod" runs MySQL alongside Adminer.&lt;/li&gt;
&lt;li&gt;My streaming "pod" runs the Express backend and cache scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the entrance of this setup sits the &lt;strong&gt;Pangolin reverse-proxy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Pangolin is the gatekeeper. It listens to the ports exposed to the internet, manages SSL/TLS certificates, and routes incoming web requests to the correct internal Docker container based on the subdomain. This keeps my internal network completely hidden from the public internet while making my apps accessible worldwide.&lt;/p&gt;

&lt;p&gt;Earlier I said that these are not official Kubernetes pods. Currently I'm not in that face... Yet. But I'm planning to migrate this container chaos into a well-orchestrated Kubernetes just for fun :)&lt;/p&gt;




&lt;p&gt;&lt;a href="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%2Farticles%2Fyd5j4w3wet8z6xyo8525.jpeg" class="article-body-image-wrapper"&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%2Farticles%2Fyd5j4w3wet8z6xyo8525.jpeg" alt=" " width="800" height="1066"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The application stack: What's running in the Cylinder?
&lt;/h2&gt;

&lt;p&gt;This Mac Pro is currently hosting a mix of custom-built software and open-source infrastructure:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Unlimited Blades Work (&lt;a href="https://unlimitedblades.work" rel="noopener noreferrer"&gt;unlimitedblades.work&lt;/a&gt;)
&lt;/h3&gt;

&lt;p&gt;This is my custom-built streaming application. It consists of three separate applications working in harmony:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Frontend:&lt;/strong&gt; A Next.js app utilizing Server-Side Rendering (SSR) for instant load times and SEO optimization, hosted on Vercel's global CDN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The CMS:&lt;/strong&gt; A &lt;strong&gt;Strapi V5 headless CMS&lt;/strong&gt; hosted on the Mac Pro. It manages all the database relationships, video metadata, user logins, and application settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Streaming Backend:&lt;/strong&gt; An &lt;strong&gt;Express.js app&lt;/strong&gt; (which lives in my codebase as &lt;code&gt;private-cloud-backend&lt;/code&gt;) hosted on the Mac Pro. This app handles the heavy lifting: video streaming, offline video processing/transcoding, and processing webhooks to automate tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Conference Room (&lt;a href="https://conference.chaldea.foundation" rel="noopener noreferrer"&gt;conference.chaldea.foundation&lt;/a&gt;)
&lt;/h3&gt;

&lt;p&gt;This is a videochat app I built so I could call my girlfriend when we were in a long-distance relationship. I didn't want to rely on third-party platforms that track your data or have call limits. It consists of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A client-side &lt;strong&gt;React.js&lt;/strong&gt; web application.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;PeerJS server&lt;/strong&gt; hosted on the Mac Pro to handle WebRTC signaling, allowing direct peer-to-peer browser connections for encrypted, low-latency audio/video.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;Express.js&lt;/strong&gt; app to manage active rooms and API routes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s completely private, self-hosted, and runs beautifully on my 10-core piece of art called Tris Megistus (Yes, that what I'm calling my trash can mac pro).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Log management: Grafana + Loki
&lt;/h3&gt;

&lt;p&gt;Because there is no graphical interface on the OS, monitoring logs across multiple Docker containers can be a nightmare. I don't want to run &lt;code&gt;docker logs&lt;/code&gt; on ten different containers manually.&lt;br&gt;
Instead, I deploy &lt;strong&gt;Loki&lt;/strong&gt; and &lt;strong&gt;Grafana&lt;/strong&gt;. Loki acts as a centralized log aggregator that scrapes logs from all running Docker containers. Grafana pulls data from Loki and displays it in a beautiful, real-time dashboard. If a database query fails or a webhook fails, I see it immediately on a visual graph.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Database: MySQL + Adminer
&lt;/h3&gt;

&lt;p&gt;The data engine for my custom apps. I run MySQL for data storage and Adminer as a lightweight web interface to manage tables. They run together in the same container network, isolated from the rest of the system for security.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Photos: Immich
&lt;/h3&gt;

&lt;p&gt;I use &lt;strong&gt;Immich&lt;/strong&gt; to backup and store all of our photos. It is a self-hosted alternative to Google Photos and iCloud. It features a mobile app that automatically backs up our photos to the server when we connect to our home network. It even runs local machine learning models on the Mac Pro's CPU for facial recognition and object search.&lt;/p&gt;
&lt;h3&gt;
  
  
  6. The Future: Lossless Music Streaming
&lt;/h3&gt;

&lt;p&gt;I’m planning to deploy a high-quality, lossless music streaming application in the future to stream FLAC files directly to my audio systems. The 64GB of RAM and massive storage pool make this hardware ideal for handling high-resolution audio streaming.&lt;/p&gt;



&lt;p&gt;&lt;a href="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%2Farticles%2Ftnjef96xf60wdtpynen8.jpeg" class="article-body-image-wrapper"&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%2Farticles%2Ftnjef96xf60wdtpynen8.jpeg" alt=" " width="800" height="1066"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The custom backup engine: GCP &amp;amp; Google Drive API
&lt;/h2&gt;

&lt;p&gt;A home server is only as good as its backup strategy. If a hard drive crashes, or if there is a power outage that corrupts the filesystem, you stand to lose years of work.&lt;/p&gt;

&lt;p&gt;I didn't want to rely on complex, proprietary backup tools. Instead, I built a custom backup pipeline directly into the Express.js backend (&lt;code&gt;private-cloud-backend&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Here is an example of how the weekly automated backup script looks like. It dumps the database, packages the files, and uses the GCP Google Drive API to push the archive to the cloud:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;exec&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;googleapis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cron&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node-cron&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BACKUP_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/tmp/backups&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STRAPI_UPLOADS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/mnt/usb-hdd/strapi/uploads&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LOG_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/var/log/docker-apps&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GOOGLE_DRIVE_FOLDER_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your_gdrive_folder_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Authenticate with GCP Service Account&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GoogleAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;keyFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gcp-credentials.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.googleapis.com/auth/drive.file&lt;/span&gt;&lt;span class="dl"&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runBackup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[BACKUP] Starting weekly backup pipeline...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;:.&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dbDumpFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`db-dump-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.sql`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tarballFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`backup-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 1. MySQL Dump via Docker execution&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;executeCmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;`docker exec mysql-container mysqldump -u root -p"" chaldea_foundation &amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dbDumpFile&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[BACKUP] MySQL Database dump complete.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 2. Compress Database, Strapi uploads, and logs into a tarball&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;executeCmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;`tar -czf &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tarballFile&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dbDumpFile&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;STRAPI_UPLOADS&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;LOG_DIR&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[BACKUP] Compression of assets and logs complete.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 3. Upload to Google Drive&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;drive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drive&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;media&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/gzip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createReadStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tarballFile&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileMetadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`macpro-backup-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;parents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_DRIVE_FOLDER_ID&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;drive&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&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="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fileMetadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;media&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;media&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;`[BACKUP] Successfully uploaded backup to Google Drive. File ID: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 4. Cleanup local temp files&lt;/span&gt;
        &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dbDumpFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tarballFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[BACKUP] Local cleanup finished.&lt;/span&gt;&lt;span class="dl"&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[BACKUP CRITICAL ERROR] Pipeline failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;executeCmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;stderr&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Run every Sunday at 3:00 AM&lt;/span&gt;
&lt;span class="nx"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0 3 * * 0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;runBackup&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;This custom pipeline ensures that even if the physical Mac Pro is destroyed, my databases, media, and configurations are safe in the cloud, completely automated and free of charge.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Grand Plan: Migrating to Kubernetes (the career cheat code)
&lt;/h2&gt;

&lt;p&gt;Remember earlier when I told you that I was planning about migrating to Kubernetes?&lt;/p&gt;

&lt;p&gt;Right now, my Docker setup is incredibly stable, lightweight, and easy to maintain. It does everything I need.&lt;/p&gt;

&lt;p&gt;But I am planning to migrate the entire stack to &lt;strong&gt;Kubernetes (K8s)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let's be completely real for a moment. Running Kubernetes on a single-node home server is absolute, ridiculous overkill. It introduces massive complexity, forces you to write endless lines of YAML manifest files, and requires you to manage K8s networking, persistent volume claims, and ingress controllers.&lt;/p&gt;

&lt;p&gt;So why do it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resume-Driven Development.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are looking to advance your career as a backend developer or DevOps engineer, landing a high-salary job requires more than just knowing how to write code. Companies want engineers who understand cloud infrastructure, microservices, and container orchestration.&lt;/p&gt;

&lt;p&gt;When you sit in a job interview and they ask about container orchestration, you don't want to give a textbook answer. You want to look them in the eye and say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I manage my own bare-metal Kubernetes node on an upgraded 2013 Mac Pro. I architected my applications into separate deployments, configured persistent volume claims to bridge my local NVMe caching SSD with a 24TB storage array, and write custom manifests to manage network ingress and database pods."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Boom. That instantly sets you apart from 99% of other candidates. It proves you aren't afraid of complex infrastructure, networking, or systems administration. It turns your hobby homelab into a professional talking point that commands respect—and a higher salary.&lt;/p&gt;




&lt;p&gt;&lt;a href="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%2Farticles%2F87zs03ym8ri9zvn6wolp.jpeg" class="article-body-image-wrapper"&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%2Farticles%2F87zs03ym8ri9zvn6wolp.jpeg" alt=" " width="800" height="1066"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Sure, the 2013 Mac Pro is 13 years old. Sure, it is objectively underpowered by today’s standards and consumes enough electricity to heat my apartment in the winter. But every single time I walk into the living room and see that sleek, futuristic black cylinder sitting next to the TV, I smile. It doesn't look like a standard, boring PC. It looks like a high-tech artifact from a sci-fi movie—a miniature personal datacenter humming quietly under my roof.&lt;/p&gt;

&lt;p&gt;It failed as a professional workstation, but it succeeds spectacularly as a homelab. By opening it up, upgrading the CPU and RAM, installing a headless Linux OS, and dockerizing your applications, you can build a private cloud that is unique, beautiful, and completely under your control.&lt;/p&gt;

&lt;p&gt;Stop renting virtual machines in the cloud for simple projects. Find some cheap, recycled hardware, open up the terminal, write a Dockerfile, and start building.&lt;/p&gt;

&lt;p&gt;The skills you gain by configuring, breaking, and fixing your own infrastructure will pay off for the rest of your engineering career.&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>2013macpro</category>
      <category>ubuntuserver</category>
      <category>docker</category>
    </item>
    <item>
      <title>Streaming 7GB movies on the fly with HLS</title>
      <dc:creator>Gonzalo Salvador Corvalán</dc:creator>
      <pubDate>Tue, 21 Jul 2026 09:20:01 +0000</pubDate>
      <link>https://dev.to/themrcorvy/streaming-7gb-movies-on-the-fly-with-hls-3b0c</link>
      <guid>https://dev.to/themrcorvy/streaming-7gb-movies-on-the-fly-with-hls-3b0c</guid>
      <description>&lt;p&gt;Have you ever tried building your own Netflix?&lt;/p&gt;

&lt;p&gt;It is one of those projects that starts out sounding so simple. You think: "Hey, I have a bunch of high-quality video files on my server, and I want to watch them on my laptop, my phone, and my TV. How hard can it be?"&lt;/p&gt;

&lt;p&gt;Then you actually try to do it, and you run face-first into a brick wall of network protocols, video containers, audio tracks, and hardware constraints, and so on...&lt;/p&gt;

&lt;p&gt;Today I'm going to deep dive into streaming massive, 7GB+ movie files on the fly, without duplicating files, without hogging my disk space, and most importantly, making sure that seeking actually works on device setups like Chromecast and Smart TVs in general.&lt;/p&gt;




&lt;h2&gt;
  
  
  The landscape: Three ways to stream video
&lt;/h2&gt;

&lt;p&gt;Before I talk about HLS, you need to understand the history of how video gets from my server to the frontend client. There are three primary ways we can handle this. Let's break down each approach, look at how they work under the hood, and see where they succeed and where they fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 1: Partial HTTP requests (Static Byte-Range MP4)
&lt;/h3&gt;

&lt;p&gt;This is the simplest way to stream. I have a pre-compiled, browser-compatible MP4 file sitting on my 24TB disk.&lt;/p&gt;

&lt;p&gt;When I play this video in an HTML &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; tag, the browser does not download the entire 7GB file at once (it would take years just to get to start the movie). Instead, it sends a GET request to the server, and the server responds with two critical headers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;**Content-Length: 7500000000**&lt;/code&gt; telling the browser the exact file size (it could have even more 0s in it).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;**Accept-Ranges: bytes**&lt;/code&gt; telling the browser that it supports byte-range requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the browser knows the server supports ranges, it sends a request with a header like &lt;code&gt;Range: bytes=0-1048576&lt;/code&gt; to fetch the first megabyte. As you watch, the browser keeps requesting subsequent byte ranges.&lt;/p&gt;

&lt;p&gt;When the user seeks to the middle of the timeline, the browser looks at the index metadata inside the MP4 container (called the &lt;code&gt;moov&lt;/code&gt; atom), calculates which byte offset that corresponds to that timestamp, and requests &lt;code&gt;Range: bytes=3500000000-&lt;/code&gt;, and the server streams it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why it is great:&lt;/strong&gt; It is incredibly simple, requires no server-side logic, other than serving static files, and seeking is handled natively by the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it fails for me:&lt;/strong&gt; It requires a single, static file. If the video is an MKV file containing multiple audio tracks (English, Spanish, Japanese, etc), the browsers cannot play it natively. If I pre-transcode it into multiple MP4 files to support different audio options, I would duplicate my 7GB files (which I have plenty) and destroy the hard drive storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Approach 2: Remuxing and chunking by seconds (On-the-Fly Progressive MP4)
&lt;/h3&gt;

&lt;p&gt;MKV is an incredible container. It can hold multiple video tracks, multiple audio tracks in different languages (English, Spanish, Japanese), and multiple subtitle tracks.&lt;/p&gt;

&lt;p&gt;But web browsers cannot play MKV files natively. They need MP4.&lt;/p&gt;

&lt;p&gt;If I want to stream these MKV movies, I have two choices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-transcode everything:&lt;/strong&gt; Convert every MKV file to an MP4 file beforehand. But wait! If we do that, we lose the ability to easily select different audio tracks on the fly, and we double our disk usage. Storing two versions of a 7GB movie means 14GB of storage. If you have hundreds of movies, your hard drives will cry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-the-fly remuxing:&lt;/strong&gt; When the user requests a video, we extract the video track and their selected audio track, package them into an MP4 container on the fly, and stream that stream directly to the browser.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To avoid duplicating files, the server tries on-the-fly remuxing, &lt;strong&gt;we are going with option number 2&lt;/strong&gt;. We keep the original MKV file intact (this is a &lt;strong&gt;must&lt;/strong&gt; for me) and when the user selects an audio track, the server spawns an FFmpeg process.&lt;/p&gt;

&lt;p&gt;FFmpeg then reads the original MKV file, extracts the H.264 video track, combines it with the selected pre-extracted AAC audio track, and repackages them into an MP4 container stream. Because we are only copying the streams (&lt;code&gt;-c copy&lt;/code&gt;) and not re-encoding the video frames, this process uses almost no CPU (yay!).&lt;/p&gt;

&lt;p&gt;But since we are generating this stream on the fly, the video player doesn't know what the final file size will be... So now the server streams the data using &lt;code&gt;Transfer-Encoding: chunked&lt;/code&gt; and sets &lt;code&gt;Accept-Ranges: none&lt;/code&gt;. And the total duration of the video is sent to the frontend by a different backend app (one that stores all the metadata about the movie).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;video/mp4&lt;/span&gt;
&lt;span class="na"&gt;Transfer-Encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chunked&lt;/span&gt;
&lt;span class="na"&gt;Accept-Ranges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To allow seeking, the client player must encode the seek target in the URL (like &lt;code&gt;?start=600&lt;/code&gt; for starting on the minute 00:10:00). When the user seeks, the client kills the old stream, sends a new request with the new start time, and the server spawns a new FFmpeg process seeking to that second.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why it is great:&lt;/strong&gt; It keeps my original files intact, supports multiple audio tracks dynamically, and takes zero extra storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it fails for me:&lt;/strong&gt; While it works on desktop and Android browsers, it breaks on Chromecast and Smart TVs and IOS devices. These devices have rigid hardware media decoders. Because the stream has no content length and range requests are disabled, the TV treats it as a live broadcast. The timeline shows no duration. Furthermore, when the TV tries to seek, it issues byte-range requests. Since the server cannot fulfill them, or if the client forces a reload with a new container header, the TV's decoder crashes and resets the video to 00:00:00.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why seeking back to 00:00 happens
&lt;/h3&gt;

&lt;p&gt;If the user attempts to seek, the Chromecast media engine calculates a byte offset based on what it thinks the file layout is. It sends a request like &lt;code&gt;Range: bytes=45000000-&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But our server does not support byte ranges on this dynamic endpoint!&lt;/p&gt;

&lt;p&gt;If we try to bypass this by intercepting the seek in our app and restarting the stream from a new time (e.g. &lt;code&gt;?start=600&lt;/code&gt;), the server spawns a new FFmpeg process. This process outputs a &lt;strong&gt;new set of fMP4 headers&lt;/strong&gt; (&lt;code&gt;ftyp&lt;/code&gt; and &lt;code&gt;moov&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;When the Chromecast's hardware media decoder receives this new header data mid-stream, it panics. It was expecting raw movie data from byte offset &lt;code&gt;45000000&lt;/code&gt; of the old file layout, not a brand-new container index. The player crashes, buffers infinitely, or resets back to 00:00:00.&lt;/p&gt;

&lt;p&gt;If the user pauses the video and the connection times out, the player tries to resume by sending a range request. The server cannot fulfill it, and the movie starts over from the beginning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 3: Dynamic HLS (Virtual Segmented Streaming)
&lt;/h3&gt;

&lt;p&gt;This is the modern industry standard, and it is the solution we are implementing.&lt;/p&gt;

&lt;p&gt;HLS is an industry-standard streaming protocol. It works by breaking a movie down into a text playlist manifest (&lt;code&gt;.m3u8&lt;/code&gt;) and a large number of tiny, self-contained video segments (usually &lt;code&gt;.ts&lt;/code&gt; or &lt;code&gt;.mp4&lt;/code&gt; files), each containing about 10 seconds of video.&lt;/p&gt;

&lt;p&gt;When a player loads an HLS stream, it first downloads the manifest file. The manifest contains the URLs of all the segments and the duration of each segment.&lt;/p&gt;

&lt;p&gt;Instead of treating the movie as one large progressive file, HTTP Live Streaming (HLS) breaks the movie down into a text playlist manifest (&lt;code&gt;.m3u8&lt;/code&gt;) and a large number of tiny, 10-second video segments (usually &lt;code&gt;.ts&lt;/code&gt; files).&lt;/p&gt;

&lt;p&gt;Instead of slicing the 7GB file into physical files on disk, we do it virtually.&lt;/p&gt;

&lt;p&gt;When the user seeks to 00:10:00, the player simply calculates that it needs segment 60 by using the manifest generated, cancels the current segment request, and asks the server for segment 60. The server spawns a fast FFmpeg copy process, extracts that specific 10-second chunk using input seeking, and streams it back.&lt;/p&gt;

&lt;p&gt;Here is what the manifest 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;#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10.000000,
/video/segment?index=0
#EXTINF:10.000000,
/video/segment?index=1
#EXTINF:10.000000,
/video/segment?index=2
#EXT-X-ENDLIST
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TV player reads this list, and because the manifest ends with the &lt;code&gt;#EXT-X-ENDLIST&lt;/code&gt; tag, the player knows this is a completed Movie (Video-on-Demand) rather than a live stream. The timeline displays the full length of the film, and seeking is fully enabled.&lt;/p&gt;

&lt;p&gt;If the user seeks to 00:10:00 (600 seconds), the player looks at the manifest. Since each segment is 10 seconds, it realizes that 10 minutes corresponds to segment 60. It cancels the current segment request and requests &lt;code&gt;/video/segment?index=60&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is a standard, simple HTTP request. The server does not need to handle byte ranges, and the TV's decoder gets a clean, self-contained video segment. Seeking works perfectly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why it is great:&lt;/strong&gt; Seeking works natively on Chromecast and Smart TVs because the device handles the seeking calculations at the segment level. No byte-range requests are sent to the dynamic copier, the timeline displays the correct duration, and server overhead remains extremely low.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it fails for me:&lt;/strong&gt; I don't actually know yet since I haven't finished implementing this solution. I don't even know if its going to work at all or not, but with a bit of Claudio's help (if you know what I'm talking about) maybe I will be able to finally find the perfect solution for my problem.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Detailed comparison matrix
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Partial HTTP Requests (V1):&lt;/strong&gt; Excellent seeking, correct timeline, zero CPU overhead, but has high disk storage cost (no dynamic audio tracks).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progressive MP4 Remuxing (V2):&lt;/strong&gt; Poor seeking on TVs, broken timeline, low CPU, low storage, able to be scaled up with the other server app's metadata, but fails on Chromecast (kinda).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic HLS (V3):&lt;/strong&gt; Excellent seeking on TVs, correct timeline, low CPU, low storage. It is the best of both worlds... Or at least I hope so.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The dream: keep it simple idio...
&lt;/h2&gt;

&lt;p&gt;Let's set the stage.&lt;/p&gt;

&lt;p&gt;I have a collection of high-definition movies. Some are saved as standard web-friendly MP4 files, and others are saved as MKV files. These files are huge—roughly 7GB each.&lt;/p&gt;

&lt;p&gt;If you are playing a standard MP4 file in a modern web browser on your laptop, things are easy. Modern web browsers have a native video player that knows how to read these files. But instead of sending the entire 7GB file over the network at once (which would destroy your bandwidth and make the video load forever).&lt;/p&gt;

&lt;p&gt;Now, if you are playing an MKV file, things change. That why I need this solution.&lt;/p&gt;

&lt;p&gt;The idea behind all of this hypothetical V3 migration is to be able to keep my current infrastructure untouched. Everything already works fine, so why modify it to begin with?&lt;/p&gt;

&lt;p&gt;This hypothetical V3 HLS solution will be primarily implemented for Chromecast devices and Smart TVs and if it works there I'll try it on IOS devices (I'm going to try it on them anyways, but thats the initial plan).&lt;/p&gt;




&lt;h2&gt;
  
  
  Implementing dynamic HLS
&lt;/h2&gt;

&lt;p&gt;We do not want to slice our 7GB movies into thousands of small files on disk. That would consume massive disk space and degrade performance.&lt;/p&gt;

&lt;p&gt;Instead, we generate the playlist manifest and the segments &lt;strong&gt;dynamically&lt;/strong&gt; (the magic word).&lt;/p&gt;

&lt;h3&gt;
  
  
  The manifest generator
&lt;/h3&gt;

&lt;p&gt;When the user requests the manifest, our server reads a tiny, pre-generated JSON metadata file containing the exact duration of the movie.&lt;/p&gt;

&lt;p&gt;We generate a virtual manifest mapping out the entire video into 10-second segments.&lt;/p&gt;

&lt;p&gt;Here is a hypothetical implementation of the manifest controller in Node.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Hypothetical Express handler for the HLS Playlist&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/video/playlist.m3u8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;movieName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;audioTrack&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;segmentDuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// seconds&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Read pre-extracted metadata containing the total movie duration&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readMetadataFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;movieName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#EXTM3U&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#EXT-X-VERSION:3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;`#EXT-X-TARGETDURATION:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;segmentDuration&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#EXT-X-MEDIA-SEQUENCE:0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#EXT-X-PLAYLIST-TYPE:VOD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalSegments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;segmentDuration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;totalSegments&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isLast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;totalSegments&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentDuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isLast&lt;/span&gt;
                &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;segmentDuration&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;segmentDuration&lt;/span&gt;
                &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;segmentDuration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#EXTINF:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;currentDuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;,`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s2"&gt;`/video/segment?movieName=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;movieName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;audioTrack=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;audioTrack&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;segment=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#EXT-X-ENDLIST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/x-mpegURL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Credentials&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-cache, no-store, must-revalidate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt; n&lt;/span&gt;&lt;span class="dl"&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error generating manifest&lt;/span&gt;&lt;span class="dl"&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;When this is sent to the Chromecast, it parses the manifest and renders the full timeline immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Spawning the segment generator
&lt;/h2&gt;

&lt;p&gt;When the TV requests a segment (e.g. &lt;code&gt;/video/segment?segment=60&lt;/code&gt;), the server must extract that specific 10-second slice.&lt;/p&gt;

&lt;p&gt;To achieve this, the server spawns an FFmpeg process. We map two inputs: the original video container (our 7GB MKV file) and the pre-extracted audio file.&lt;/p&gt;

&lt;p&gt;Here is the flow diagram of this architecture:&lt;/p&gt;

&lt;p&gt;To make sure the TV receives this segment instantly without buffering, we apply two optimizations:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Fast input seeking
&lt;/h3&gt;

&lt;p&gt;We place the seek parameter &lt;code&gt;-ss&lt;/code&gt; before the input file &lt;code&gt;-i&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This tells FFmpeg to perform input seeking. Instead of decoding frames from the start of the movie, FFmpeg reads the container index and jumps directly to the target timestamp. For segment 60, this seek takes less than 1ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Stream copying
&lt;/h3&gt;

&lt;p&gt;We use &lt;code&gt;-c copy&lt;/code&gt; to copy the video stream without re-encoding.&lt;/p&gt;

&lt;p&gt;We map the original video track and the pre-extracted AAC audio track, packaging them into an MPEG-TS container (&lt;code&gt;.ts&lt;/code&gt;). MPEG-TS is the standard transport container for HLS. It has small packet headers every 188 bytes, making it incredibly resilient to network drops and easy for player hardware to parse.&lt;/p&gt;

&lt;p&gt;Here is a hypothetical implementation of the segment controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;spawn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/video/segment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;movieName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;audioTrack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;segment&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;segmentDuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;segmentIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;segmentIndex&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;segmentDuration&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Map file paths&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;videoFilePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getMoviePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;movieName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audioFilePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAudioTrackPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;movieName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;audioTrack&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;video/MP2T&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Credentials&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;public, max-age=3600&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Segments are static, cache them!&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Run FFmpeg: fast input seeking, stream copy, export as mpegts&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ffmpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-ss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Seek video input&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-i&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;videoFilePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-ss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Seek audio input&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-i&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;audioFilePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-t&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;segmentDuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// Output duration limit (10s)&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-map&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0:v:0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Map video&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-map&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1:a:0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Map audio&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-c&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;copy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Stream copy (fast!)&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-avoid_negative_ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;make_zero&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-f&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mpegts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Output format&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pipe:1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Output to stdout pipe&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// If client closes connection early, kill the process&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;killed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SIGKILL&lt;/span&gt;&lt;span class="dl"&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because of stream copying and input seeking, this entire operation takes under 100ms and consumes almost no CPU on the server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Caching and chromecast performance
&lt;/h2&gt;

&lt;p&gt;One of the biggest advantages of HLS is caching.&lt;/p&gt;

&lt;p&gt;With progressive MP4 streaming, caching is difficult because the requests are made as variable byte ranges.&lt;/p&gt;

&lt;p&gt;With HLS, each 10-second segment is a distinct file request. Segment 60 will always contain the exact same video data. By sending a &lt;code&gt;Cache-Control: public, max-age=3600&lt;/code&gt; header, we allow browser caches, CDNs, and Chromecast hardware caches to store these files.&lt;/p&gt;

&lt;p&gt;If a user seeks back to a section they already watched, the player loads the segment from its local cache instantly, without placing any load on our server.&lt;/p&gt;

&lt;p&gt;Furthermore, Chromecast receiver shells require Cross-Origin Resource Sharing (CORS) permissions. Without returning &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; in the playlist and segment headers, the Chromecast receiver will refuse to play the stream, throwing a CORS block error.&lt;/p&gt;




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

&lt;p&gt;By migrating from progressive MP4 remuxing to Dynamic HLS, we resolved our streaming issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The timeline displays correctly&lt;/strong&gt; on Smart TVs and Chromecast because the dynamic &lt;code&gt;.m3u8&lt;/code&gt; manifest lists all segment lengths upfront.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seeking is flawless&lt;/strong&gt; because the player requests specific 10-second chunks, avoiding complex byte-range requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server CPU overhead is zero&lt;/strong&gt; because we copy the tracks without transcoding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk storage is minimized&lt;/strong&gt; because we store only the original video file and lightweight pre-extracted audio tracks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are building your own media server, go straight to Dynamic HLS. It is the most robust and scalable way to stream high-definition content to any device.&lt;/p&gt;

&lt;p&gt;If you made this far, thank you. I'm writting these posts because I'm trying to demonstrate a point to myself: I am a capable developer, and AI is not going to replace me any time soon.&lt;/p&gt;

&lt;p&gt;View more of my content at &lt;a href="https://chaldea.foundation/blog" rel="noopener noreferrer"&gt;Chaldea Foundation News&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks again.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>hls</category>
      <category>customstreamingplatform</category>
      <category>videoprocessing</category>
    </item>
    <item>
      <title>From `.env` string arrays to database-backed, permission-scoped API keys</title>
      <dc:creator>Gonzalo Salvador Corvalán</dc:creator>
      <pubDate>Wed, 15 Jul 2026 11:00:02 +0000</pubDate>
      <link>https://dev.to/themrcorvy/from-env-string-arrays-to-database-backed-permission-scoped-api-keys-3o6h</link>
      <guid>https://dev.to/themrcorvy/from-env-string-arrays-to-database-backed-permission-scoped-api-keys-3o6h</guid>
      <description>&lt;h2&gt;
  
  
  1. The core problem with the original design
&lt;/h2&gt;

&lt;p&gt;The existing system for Unlimited Blades Work streaming backend stores raw key strings in the &lt;code&gt;.env&lt;/code&gt; file in a JSON array of strings and checks every incoming key against every value in that array with a plain equality check, presenting two structural problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No granularity&lt;/strong&gt; — any valid key unlocks the entire application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lifecycle&lt;/strong&gt; — revoking a key means editing a file and restarting the process; there's no audit trail, no expiry, and no way to revoke a key instantly.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  2. The correct abstraction: Permission tuples
&lt;/h2&gt;

&lt;p&gt;The pattern used by AWS IAM, GitHub fine-grained tokens, Stripe, and Twilio is to model each grant as an explicit &lt;strong&gt;&lt;code&gt;(method, route)&lt;/code&gt; pair&lt;/strong&gt;, called a &lt;strong&gt;permission tuple&lt;/strong&gt; or &lt;strong&gt;policy entry&lt;/strong&gt;. Allowing access to specific routes with specific HTTP methods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ApiKey → 1-to-many → ApiKeyPermission { method, route }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each row is an atomic, indivisible grant. Your example becomes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Row&lt;/th&gt;
&lt;th&gt;method&lt;/th&gt;
&lt;th&gt;route&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/resource-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/resource-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/resource-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;(no row)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/resource-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ denied&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  3. Data model (I'm using Prisma for this project so here is the schema)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enum HttpMethod {
  GET
  POST
  PUT
  PATCH
  DELETE
  HEAD
  OPTIONS
}

model ApiKey {
  id           Int                @id @default(autoincrement())
  name         String             @db.VarChar(100)   // e.g. "Unlimited Blades Work Client"
  key_prefix   String             @db.VarChar(16)    // e.g. "apc_" — plaintext for fast DB lookup
  hash         String             @db.VarChar(255)   // bcrypt hash — the raw key is never stored
  is_active    Boolean            @default(true)
  created_at   DateTime           @default(now())
  updated_at   DateTime           @updatedAt
  expires_at   DateTime?                             // optional TTL; null = never expires
  last_used_at DateTime?                             // updated on every successful auth (fire-and-forget)

  permissions  ApiKeyPermission[]

  @@index([key_prefix], name: "idx_key_prefix")
  @@map("api_keys")
}

model ApiKeyPermission {
  id         Int        @id @default(autoincrement())
  api_key_id Int
  method     HttpMethod
  route      String     @db.VarChar(255)             // exact path e.g. "/api/v2/serve-episode"

  api_key    ApiKey     @relation(fields: [api_key_id], references: [id], onDelete: Cascade)

  @@unique([api_key_id, method, route])              // DB-level constraint: no duplicate grants
  @@index([api_key_id], name: "idx_permission_key")
  @@map("api_key_permissions")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why each field exists
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;key_prefix&lt;/code&gt; plaintext&lt;/td&gt;
&lt;td&gt;Narrows DB candidates before bcrypt — see Phase 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;hash&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;Raw key shown once at creation, never persisted — mirrors GitHub / Stripe / npm token UX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@@unique([api_key_id, method, route])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DB-level guard against accidental duplicate grants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Soft-disable without deleting audit history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expires_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Supports short-lived keys; middleware rejects expired keys without any extra step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;last_used_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enables rotation policy ("revoke keys unused for 90 days")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;onDelete: Cascade&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deleting a key removes all its permissions atomically&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  4. The full API Key lifecycle
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Phase 1 — Creation (refactored key generation script)
&lt;/h3&gt;

&lt;p&gt;The script needs to become a proper interactive CLI tool. It should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Accept arguments (or prompt interactively) for:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--name&lt;/code&gt; — human-readable name for the consuming service&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--prefix&lt;/code&gt; — key prefix (default: &lt;code&gt;apc_&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--permissions&lt;/code&gt; — one or more &lt;code&gt;METHOD:ROUTE&lt;/code&gt; pairs, e.g. &lt;code&gt;GET:/api/v2/serve-episode&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--expires&lt;/code&gt; — optional ISO date string or duration (e.g. &lt;code&gt;90d&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Generate the random key using &lt;code&gt;randomBytes&lt;/code&gt; as today.&lt;/li&gt;
&lt;li&gt;Hash it with &lt;code&gt;bcrypt&lt;/code&gt; at cost 12.&lt;/li&gt;
&lt;li&gt;Open a Prisma DB connection and run a transaction:

&lt;ul&gt;
&lt;li&gt;Insert one row in &lt;code&gt;api_keys&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Insert one row per &lt;code&gt;(method, route)&lt;/code&gt; pair in &lt;code&gt;api_key_permissions&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Close the DB connection.&lt;/li&gt;
&lt;li&gt;Print the raw key &lt;strong&gt;once&lt;/strong&gt; to stdout and exit. The key cannot be recovered after this point.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Example invocation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx ts-node scripts/generateApiKey.ts &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"Unlimited Blades Work Client"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--prefix&lt;/span&gt; &lt;span class="s2"&gt;"apc_"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions&lt;/span&gt; &lt;span class="s2"&gt;"GET:/api/v1/get-resource"&lt;/span&gt; &lt;span class="s2"&gt;"POST:/api/v3/create-resource"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--expires&lt;/span&gt; &lt;span class="s2"&gt;"2027-01-01"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Terminal output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🔑 Generating API Key for "Unlimited Blades Work Client"...

✅ API Key created and stored successfully!

📋 Details:
   Name:        Unlimited Blades Work Client
   DB ID:       7
   Prefix:      apc_
   Expires:     2027-01-01T00:00:00.000Z
   Created:     2026-07-14T10:22:01.000Z

🔐 Permissions granted:
   GET  /api/v1/get-resource
   POST /api/v3/create-resource

⚠️  RAW KEY (copy now — it will NOT be shown again):

   apc_3f7a9c2e1b84d0f6...

💡 The hash is stored in the database. The raw key is not.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Phase 2 — Usage (middleware flow)
&lt;/h3&gt;

&lt;p&gt;Every request passes through the auth middleware. Non of the routes of this app are exposed to the public without an API key. Here is the exact sequence of operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Incoming request
      │
      ▼
1. Extract raw key
   - req.headers['x-api-key']
   - req.headers['authorization'] (strip "Bearer ")
   - req.query.apiKey (This is because video players on the client-side can't send headers in the request so we must add it in the URL as a query parameter)

   Missing? → 401 Unauthorized
      │
      ▼

2. Parse prefix from raw key
   - e.g. "apc_3f7a9c2e..." → prefix = "apc_"
      │
      ▼

3. Cache lookup ← O (1), in-memory
   - Key: SHA-256(rawKey)  ← never store the raw key as a cache key
   - Hit? → Go to step 6
   - Miss? → Continue
      │
      ▼

4. DB query — Find candidates by prefix
   - WHERE key_prefix = "apc_" AND is_active = true
     AND (expires_at IS NULL OR expires_at &amp;gt; NOW())
   - Include: permissions[]
   - Result is a small set (usually 1–3 rows)
      │
      ▼

5. bcrypt.compare(rawKey, candidate.hash) for each candidate
   - No match after all candidates? → 403 Forbidden (log the attempt)
   - Match found? → store result in cache with TTL
      │
      ▼

6. Permission check
   - Does matchedKey.permissions contain { method: req.method, route: req.path }?
   - No? → 403 Forbidden (log: "key X attempted unauthorized route")
   - Yes? → continue
      │
      ▼

7. Non-blocking side effects (fire-and-forget, don't await)
   - prisma.apiKey.update({ last_used_at: new Date() })
      │
      ▼

8. Attach to request context
   - req.apiKeyId = matchedKey.id
   - req.apiKeyName = matchedKey.name
      │
      ▼

9. next()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Phase 3 — Caching (full deep-dive)
&lt;/h3&gt;

&lt;h4&gt;
  
  
  The problem bcrypt creates
&lt;/h4&gt;

&lt;p&gt;bcrypt at cost factor 12 takes approximately &lt;strong&gt;250–350ms&lt;/strong&gt; per comparison. Without caching, every single authenticated request burns that time &lt;strong&gt;before&lt;/strong&gt; any application logic runs. This is non-viable in production.&lt;/p&gt;

&lt;h4&gt;
  
  
  The cache design
&lt;/h4&gt;

&lt;p&gt;Use &lt;a href="https://www.npmjs.com/package/lru-cache" rel="noopener noreferrer"&gt;&lt;code&gt;lru-cache&lt;/code&gt;&lt;/a&gt; for a single-process app (zero infrastructure), or Redis for a multi-process / multi-instance deployment.&lt;/p&gt;

&lt;p&gt;Why I'm using lru-cache. I could make the caching solution service myself with node.js native APIs, adding more complexity to the project, learning how to manually manage cached data, and leaving the app light-weight without extra dependencies but there is a problem with this approach and that is that I'm trying to create a real-world solution and in the real world you don't always create your own solution. Its a problem of balancing light-weight, complexity, and risks (because if you are making your own solutions, most likely you'll unwillingly introduce some bugs if the solution is too complex).&lt;/p&gt;

&lt;p&gt;For this app I could use Redis, but it would be a clear overkill as I don't have that many users and recurrent requests. I could also create my own service to manually manage cache, but I would spend too much time on this migration and most likely introduce bugs into the workflow. Hence why I'm going with lru-cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What gets cached:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CachedApiKey&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;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;// Cache: SHA-256(rawKey) → CachedApiKey&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cache configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LRUCache&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lru-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKeyCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;LRUCache&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CachedApiKey&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// max 7 unique keys in memory at once&lt;/span&gt;
    &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 5-minute TTL per entry&lt;/span&gt;
    &lt;span class="na"&gt;allowStale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// expired entries are never served&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;TTL trade-off:&lt;/strong&gt;&lt;br&gt;
| TTL | Pro | Con |&lt;br&gt;
|---|---|---|&lt;br&gt;
| Short (1–2 min) | Revoked keys stop working quickly | More DB hits under sustained load |&lt;br&gt;
| Medium (5 min) | Good balance — standard industry practice | A revoked key remains valid for up to 5 minutes |&lt;br&gt;
| Long (30+ min) | Minimal DB pressure | Revocation takes too long to propagate |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5 minutes is going to be my election for this app.&lt;/strong&gt; This is the same TTL used by GitHub's server-to-server token validation and AWS STS session token caching.&lt;/p&gt;
&lt;h4&gt;
  
  
  Cache invalidation on revocation
&lt;/h4&gt;

&lt;p&gt;When a key is soft-disabled (&lt;code&gt;is_active = false&lt;/code&gt;), the cache entry for that key is &lt;strong&gt;not immediately evicted&lt;/strong&gt;. The middleware has no way to map a DB record id back to its SHA-256 cache key without storing the raw key — which is deliberately never stored.&lt;/p&gt;

&lt;p&gt;My chosen strategy is to &lt;strong&gt;accept the TTL window&lt;/strong&gt;: a revoked key continues to work for at most the configured TTL (5 minutes) until its cache entry naturally expires. On the next cache miss after expiry, the cold path runs, the DB returns &lt;code&gt;is_active = false&lt;/code&gt;, and the key is permanently denied.&lt;/p&gt;

&lt;p&gt;This is the same trade-off accepted by GitHub (server-to-server tokens), Stripe (secret key validation), and AWS STS (session token caching). The TTL window is a known, documented, and acceptable behavior — not a security flaw. The 5-minute window is short enough to be operationally safe while eliminating all the complexity of a secondary re-validation step on every cache hit.&lt;/p&gt;

&lt;p&gt;Instead of adding more complexity to the middleware I decided to let the API key live for 5 more minutes. There are only 2 apps really that are going to have API keys so any suspicious activity is going to be easy to spot.&lt;/p&gt;

&lt;p&gt;The revocation flow is simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In an admin script — no cache interaction needed&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;keyId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// The key will stop working within the next TTL window (≤ 5 minutes)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or simply modify the record manually with something like Strapi V5 ;).&lt;/p&gt;




&lt;h3&gt;
  
  
  Phase 4 — Disabling a Key
&lt;/h3&gt;

&lt;p&gt;Disabling (&lt;code&gt;is_active = false&lt;/code&gt;) is a &lt;strong&gt;reversible&lt;/strong&gt;, &lt;strong&gt;non-destructive&lt;/strong&gt; operation. It is used when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A key is suspected compromised but you want to investigate before deleting.&lt;/li&gt;
&lt;li&gt;A service is temporarily taken offline.&lt;/li&gt;
&lt;li&gt;A key has expired and you want to retain the audit trail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What happens when a key is disabled:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An admin script updates &lt;code&gt;is_active = false&lt;/code&gt; in the DB.&lt;/li&gt;
&lt;li&gt;If the key has a live cache entry, it continues to pass authentication for up to the remaining TTL (at most 5 minutes). This is the accepted TTL-window trade-off described in Phase 3.&lt;/li&gt;
&lt;li&gt;Once the cache entry expires, the cold path runs on the next request. The DB query filters &lt;code&gt;is_active = true&lt;/code&gt;, so the disabled key is no longer returned as a candidate — bcrypt is never even reached. The request receives &lt;code&gt;403 Forbidden&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;All subsequent requests from that key are denied indefinitely.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Re-enabling:&lt;/strong&gt; set &lt;code&gt;is_active = true&lt;/code&gt;. The key works again on the next request after its cache entry expires. No script regeneration or code change needed.&lt;/p&gt;




&lt;h3&gt;
  
  
  Phase 5 — Deleting a Key
&lt;/h3&gt;

&lt;p&gt;Hard deletion (&lt;code&gt;DELETE FROM api_keys WHERE id = ?&lt;/code&gt;) is &lt;strong&gt;irreversible&lt;/strong&gt; and should only be used when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The key was generated by mistake.&lt;/li&gt;
&lt;li&gt;The key is confirmed compromised and the audit trail is no longer needed.&lt;/li&gt;
&lt;li&gt;A consuming service is permanently decommissioned.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What happens on deletion:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;onDelete: Cascade&lt;/code&gt; constraint automatically removes all rows in &lt;code&gt;api_key_permissions&lt;/code&gt; for that key.&lt;/li&gt;
&lt;li&gt;No orphaned permission data remains.&lt;/li&gt;
&lt;li&gt;Any cache entry for that key expires naturally within the TTL window. On the next cold-path DB query, the key's row no longer exists, so bcrypt is never reached and the request is denied.&lt;/li&gt;
&lt;li&gt;The key cannot be re-enabled. If the service needs access again, a new key must be generated via the script.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Since the raw key is never stored, there is no way to recover it after creation. If a key is deleted, the consuming service must be updated with a newly generated key.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Updated Middleware Pseudocode
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LRUCache&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lru-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;bcrypt&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bcrypt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../database/prisma&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextFunction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RequestHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CachedApiKey&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;LRUCache&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CachedApiKey&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sha256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authenticateApiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;RequestHandler&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;return&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextFunction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
            &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;rawKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API key required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CachedApiKey&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Cache hit — trust the cached result for the remaining TTL.&lt;/span&gt;
            &lt;span class="c1"&gt;// If the key was revoked in the DB, this entry will stop being served&lt;/span&gt;
            &lt;span class="c1"&gt;// once the TTL expires and the cold path re-runs (≤ 5 minutes).&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="c1"&gt;// Cold path: prefix lookup + bcrypt&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rawKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// "apc_"&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;key_prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;OR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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="p"&gt;},&lt;/span&gt;
                &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;

            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;matched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;candidates&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="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bcrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;matched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="k"&gt;break&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Log the attempt without exposing the key&lt;/span&gt;
                &lt;span class="nf"&gt;logData&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`[auth] Invalid API key attempt`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="na"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth_middleware&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;addSeparatorAfter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;addSpaceAfter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;timeStamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&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;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid API key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nx"&gt;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&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="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Permission check&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasPermission&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hasPermission&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[auth] Key "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" denied: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Access denied for this route&lt;/span&gt;&lt;span class="dl"&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;// Non-blocking audit update&lt;/span&gt;
        &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;last_used_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;

        &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKeyId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKeyName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;next&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;h2&gt;
  
  
  6. Refactored script for generating a new API key
&lt;/h2&gt;

&lt;p&gt;The script needs to accept CLI arguments and connect to the database. The structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@prisma/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generateApiKeyWithPrefix&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../src/services/apiKey.service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Permission&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// "GET", "POST", etc.&lt;/span&gt;
  &lt;span class="nl"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// "/api/v1/get-resource"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsePermissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;Permission&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Parse "METHOD:ROUTE" pairs, e.g. ["GET:/api/v1/get-resource", "POST:/api/v3/create-resource"]&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;colonIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;colonIndex&lt;/span&gt; &lt;span class="o"&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;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Invalid permission format: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;". Expected "METHOD:ROUTE"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;colonIndex&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;colonIndex&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="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="c1"&gt;// Parse --name, --prefix, --permissions, --expires from args&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* parse --name */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* parse --prefix, default "apc_" */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parsePermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* parse --permissions list */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expiresAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* parse --expires, default null */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--name is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;At least one --permissions entry is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateApiKeyWithPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Atomic transaction: insert key + all permissions together&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;key_prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
              &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HttpMethod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&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="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;// Print the raw key — only time it will ever be visible&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\n✅ API Key created for "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`   Permissions:`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`   &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\n⚠️  RAW KEY (copy now):\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`   &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$disconnect&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;main&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;br&gt;
Using a &lt;code&gt;$transaction&lt;/code&gt; for both inserts ensures that if the permissions insert fails for any reason (e.g. a duplicate route entry), the &lt;code&gt;api_keys&lt;/code&gt; row is also rolled back so that I never end up with a key in the DB that has no permissions.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  7. Route matching: Exact vs. Wildcard
&lt;/h2&gt;

&lt;p&gt;All current routes are exact paths, so exact string matching (&lt;code&gt;p.route === req.path&lt;/code&gt;) is enogh. If later I add parameterized routes (e.g., &lt;code&gt;/api/v2/resources/:id&lt;/code&gt;), I'll make the switch to &lt;code&gt;path-to-regexp&lt;/code&gt; — the same library Express.js uses internally — without needing to change the DB schema. The &lt;code&gt;route&lt;/code&gt; column would store patterns like &lt;code&gt;/api/v2/resources/:id&lt;/code&gt; and the middleware would match against them.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Compromise blast radius analysis
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Without this system&lt;/th&gt;
&lt;th&gt;With this system&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Key for Service A is leaked&lt;/td&gt;
&lt;td&gt;Attacker can hit all routes with any method&lt;/td&gt;
&lt;td&gt;Attacker can only hit the 2–3 granted routes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET-only service key is leaked&lt;/td&gt;
&lt;td&gt;Attacker can POST to webhooks, triggering side effects&lt;/td&gt;
&lt;td&gt;Attacker can only GET — POST returns &lt;code&gt;403&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key is revoked&lt;/td&gt;
&lt;td&gt;Requires editing &lt;code&gt;.env&lt;/code&gt; and restarting the process&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;is_active = false&lt;/code&gt; → denied on next request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key expires&lt;/td&gt;
&lt;td&gt;No concept of expiry&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;expires_at&lt;/code&gt; enforced by middleware automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attacker brute-forces a valid key&lt;/td&gt;
&lt;td&gt;Full access&lt;/td&gt;
&lt;td&gt;Scoped to minimum declared permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developer hardcodes a key in source&lt;/td&gt;
&lt;td&gt;Full exposure if repo is leaked&lt;/td&gt;
&lt;td&gt;Exposure limited to that service's minimal surface&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  9. What I'll Keep from the current system
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;code&gt;bcrypt&lt;/code&gt; hashing via &lt;code&gt;bcrypt.hash()&lt;/code&gt; / &lt;code&gt;bcrypt.compare()&lt;/code&gt; — correct algorithm, keep cost factor 12&lt;/li&gt;
&lt;li&gt;✅ The &lt;code&gt;apc_&lt;/code&gt; prefix convention — formalize it as &lt;code&gt;key_prefix&lt;/code&gt; in the schema&lt;/li&gt;
&lt;li&gt;✅ The &lt;code&gt;generateApiKeyWithPrefix()&lt;/code&gt; service function — reuse as-is&lt;/li&gt;
&lt;li&gt;✅ The &lt;code&gt;x-api-key&lt;/code&gt; / &lt;code&gt;Authorization&lt;/code&gt; / &lt;code&gt;req.query.apiKey&lt;/code&gt; extraction logic&lt;/li&gt;
&lt;li&gt;✅ The &lt;code&gt;ApiKey&lt;/code&gt; interface&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  10. Conclusion
&lt;/h2&gt;

&lt;p&gt;This migration is probably one of the most meaningful security changes I have made in this backend app so far.&lt;/p&gt;

&lt;p&gt;I started with a simple &lt;code&gt;.env&lt;/code&gt;-based API key list because it was fast to ship, but as the project grew, that model became too risky: one leaked key meant broad access, and revocation depended on manual file edits and restarts. Moving to database-backed keys with permission tuples forced me to treat access as something explicit and intentional, not implicit and global.&lt;/p&gt;

&lt;p&gt;With this new approach keys can be scoped, disabled, expired, audited, and deleted with clear behavior. The cache introduces a small revocation window, but that trade-off is documented, controlled, and acceptable for the current scale. It is a conscious engineering decision, not an accident.&lt;/p&gt;

&lt;p&gt;The goal is not to build a perfect fortress on day one, but to build a system that is safer today and still easy to evolve tomorrow. With this foundation in place, I can now add rotation workflows, richer admin tooling, and pattern-based route matching without tearing everything down again.&lt;/p&gt;

&lt;p&gt;This is the point where API keys in Private Cloud Backend stop being shared secrets and start being real security boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thanks for reading
&lt;/h2&gt;

&lt;p&gt;Comment "REST" to get some rest after reading all that, that way I'll know that you're actually a human.&lt;/p&gt;

&lt;p&gt;If this topic interests you, read the full post with nice background music at &lt;a href="https://corvalangonzalo.com/blog/securing-apis" rel="noopener noreferrer"&gt;https://corvalangonzalo.com/blog/securing-apis&lt;/a&gt; and share your thoughts in the comments section here so we can discuss API security decisions, trade-offs, and better approaches together.&lt;/p&gt;

</description>
      <category>apisecurity</category>
      <category>backendarchitecture</category>
      <category>node</category>
      <category>express</category>
    </item>
    <item>
      <title>How I built my own Netflix at home for Anime</title>
      <dc:creator>Gonzalo Salvador Corvalán</dc:creator>
      <pubDate>Thu, 09 Jul 2026 15:00:05 +0000</pubDate>
      <link>https://dev.to/themrcorvy/how-i-built-my-own-netflix-for-anime-from-burning-subtitles-to-on-demand-remuxing-4fh8</link>
      <guid>https://dev.to/themrcorvy/how-i-built-my-own-netflix-for-anime-from-burning-subtitles-to-on-demand-remuxing-4fh8</guid>
      <description>&lt;p&gt;Have you ever had a project start because of a minor technical annoyance, only to look back later and realize you built a mini-engineering system?&lt;/p&gt;

&lt;p&gt;For me, that annoyance was trying to watch high-quality anime on a legacy Smart TV.&lt;/p&gt;

&lt;p&gt;Most high-quality anime files are distributed in &lt;code&gt;.mkv&lt;/code&gt; containers with complex dual-audio tracks (Japanese/English) and heavy, styled text subtitles (&lt;code&gt;.ass&lt;/code&gt; format). Older Smart TVs have very limited built-in media players—they simply refuse to open &lt;code&gt;.mkv&lt;/code&gt; files, and they have no idea what to do with styled subtitles.&lt;/p&gt;

&lt;p&gt;To solve this, I started a journey that went from manual desktop editing to a full-scale homelab streaming server. Here is how that progression looked:&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Manual Era: HandBrake &amp;amp; CPU Melting
&lt;/h2&gt;

&lt;p&gt;My first solution was simple but painful: transcoding. I loaded my media library into HandBrake, selected H.264 video, transcoded the audio into standard AC3 format, and checked the box to "hard-burn" Spanish subtitles directly onto the video frames.&lt;/p&gt;

&lt;p&gt;To keep the highest video fidelity, I set HandBrake to a Constant Rate Factor (&lt;strong&gt;RF 18.5&lt;/strong&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Pro:&lt;/strong&gt; The output &lt;code&gt;.mp4&lt;/code&gt; file played flawlessly on the oldest smart TVs, and the video quality was pristine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Con:&lt;/strong&gt; Hard-burning subtitles means decoding and re-encoding every single video frame. This put a massive load on my CPU, keeping it pegged at 100% for hours to transcode a single season. It also created massive, duplicated files that wasted storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. The Automation Era: Discovering the Power of FFMPEG
&lt;/h2&gt;

&lt;p&gt;Clicking buttons in a GUI for hundreds of episodes became unsustainable. That’s when I discovered &lt;strong&gt;FFMPEG&lt;/strong&gt;, the command-line media engine that HandBrake actually runs under the hood.&lt;/p&gt;

&lt;p&gt;I wrote custom scripts (using PowerShell and Bash) to automate the entire folder-scanning process. The scripts would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scan a directory for &lt;code&gt;.mkv&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;ffprobe&lt;/code&gt; to scan the streams and automatically detect which track index contained Spanish (&lt;code&gt;SPA&lt;/code&gt;/&lt;code&gt;ESP&lt;/code&gt;) subtitles.&lt;/li&gt;
&lt;li&gt;Feed that stream index into the FFMPEG &lt;code&gt;subtitles&lt;/code&gt; filter and run the H.264 encode automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now I could convert entire folders with a single command line. But the CPU bottleneck was still there—my computer was still acting as a heater for days on end.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Homelab Era: On-Demand Remuxing (My "Netflix" Competitor)
&lt;/h2&gt;

&lt;p&gt;When I finally migrated my setup into a dedicated homelab server, I knew I had to stop the CPU-melting H.264 transcodes.&lt;/p&gt;

&lt;p&gt;The breakthrough came when I realized: &lt;strong&gt;Why transcode the video stream at all if the modern browser can decode it natively?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I redesigned my system to perform &lt;strong&gt;On-Demand Remuxing&lt;/strong&gt; instead of full transcoding:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Video Passthrough:&lt;/strong&gt; The server copies the H.264 video stream directly (&lt;code&gt;-c:v copy&lt;/code&gt;) without decoding/re-encoding. This is a pure copy-paste operation that takes almost 0% CPU.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Audio Transcoding:&lt;/strong&gt; It transcodes the audio on the fly to standard stereo AAC—a very light operation.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Subtitles in the Browser:&lt;/strong&gt; I pulled the &lt;code&gt;.ass&lt;/code&gt; subtitles out and served them as text. In the browser player, I integrated WebAssembly (&lt;code&gt;SubtitlesOctopus&lt;/code&gt;), compiling the native subtitle rendering library to WASM. The browser draws the styled text overlays in real-time over the HTML5 &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; player, keeping the beautiful typography without server-side transcoding!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To make this feel like a premium streaming platform, I implemented support for &lt;strong&gt;HTTP Range Requests&lt;/strong&gt;. This allows the browser to request specific byte chunks on the fly. If you click to seek to the middle of an episode, the server immediately jumps to that byte offset and streams from there—instantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Engineering Lessons
&lt;/h2&gt;

&lt;p&gt;What started as a workaround for an old TV taught me the core fundamentals of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Containers vs. Codecs:&lt;/strong&gt; The difference between a file wrapper (&lt;code&gt;.mkv&lt;/code&gt;/&lt;code&gt;.mp4&lt;/code&gt;) and the encoding algorithm (&lt;code&gt;H.264&lt;/code&gt;/&lt;code&gt;H.265&lt;/code&gt;/&lt;code&gt;Opus&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text vs. Image-Based Subtitles:&lt;/strong&gt; Why image-based PGS subtitles from Blu-rays cannot be easily exported to plain text (SRT) without running them through an OCR (Optical Character Recognition) engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser Media Delivery:&lt;/strong&gt; Balancing browser compatibility (H.264/AAC/WebVTT) with backend performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to read the full, deep-dive technical breakdown—complete with the exact FFMPEG commands, automation scripts, and system architecture diagrams—check out my latest blog post:&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://chaldea.foundation/blog/anime-transcoding" rel="noopener noreferrer"&gt;Read the full technical breakdown on my blog&lt;/a&gt;&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>systemdesign</category>
      <category>ffmpeg</category>
      <category>videoengineering</category>
    </item>
  </channel>
</rss>
