[{"content":"","date":"6 July 2026","externalUrl":null,"permalink":"/tags/amazon-sqs/","section":"Tags","summary":"","title":"Amazon SQS","type":"tags"},{"content":"","date":"6 July 2026","externalUrl":null,"permalink":"/tags/aws/","section":"Tags","summary":"","title":"AWS","type":"tags"},{"content":"","date":"6 July 2026","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":" ","date":"6 July 2026","externalUrl":null,"permalink":"/","section":"David Cajio | AWS, DevOps \u0026 Platform Reliability","summary":"Practical engineering notes on AWS, DevOps, e-commerce infrastructure, Linux automation, and platform reliability.","title":"David Cajio | AWS, DevOps \u0026 Platform Reliability","type":"page"},{"content":"","date":"6 July 2026","externalUrl":null,"permalink":"/tags/devops/","section":"Tags","summary":"","title":"DevOps","type":"tags"},{"content":"","date":"6 July 2026","externalUrl":null,"permalink":"/tags/distributed-systems/","section":"Tags","summary":"","title":"Distributed Systems","type":"tags"},{"content":"","date":"6 July 2026","externalUrl":null,"permalink":"/categories/e-commerce/","section":"Categories","summary":"","title":"E-Commerce","type":"categories"},{"content":"","date":"6 July 2026","externalUrl":null,"permalink":"/series/ecommerce-architecture/","section":"Series","summary":"","title":"Ecommerce Architecture","type":"series"},{"content":"","date":"6 July 2026","externalUrl":null,"permalink":"/tags/magento-2/","section":"Tags","summary":"","title":"Magento 2","type":"tags"},{"content":"","date":"6 July 2026","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"6 July 2026","externalUrl":null,"permalink":"/tags/sap/","section":"Tags","summary":"","title":"SAP","type":"tags"},{"content":"","date":"6 July 2026","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","date":"6 July 2026","externalUrl":null,"permalink":"/tags/system-integration/","section":"Tags","summary":"","title":"System Integration","type":"tags"},{"content":"","date":"6 July 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":" Why Queues Belong Between Systems That Don’t Fail the Same Way # In e-commerce, the order lifecycle almost never lives in one system.\nMagento may own the storefront. SAP may own financials, inventory, or offline order entry. A WMS may own fulfillment. AfterShip may own tracking, delivery visibility, and AI delivery estimates. Customer emails may depend on all of the above being correct enough to send something useful.\nThat sounds clean on an architecture diagram.\nIn reality, each system fails differently.\nSAP can accept an order with missing address data. Magento may require a country code before it can safely present that order to a customer. A WMS may be temporarily unavailable. AfterShip may receive tracking data later than expected. A retry job may keep hammering the same bad record forever because no one taught it the difference between “try again later” and “this payload is invalid.”\nThat is where queues belong.\nNot because queues are trendy.\nNot because every integration needs an event-driven architecture diagram with twelve boxes and a keynote-friendly arrow cloud.\nQueues belong between systems that do not fail the same way.\nIn my case, the practical answer is AWS SQS FIFO with dead-letter queues, validation, per-order sequencing, idempotent consumers, and clear ownership boundaries between SAP, Magento 2, AfterShip, and the rest of the e-commerce stack.\nThe Problem: Direct Integrations Assume Too Much # A lot of internal system integrations start simple.\nOne system has data. Another system needs data. So we move the data.\nMaybe that starts as an API call. Maybe it is a database table. Maybe it is a scheduled job. Maybe it is a temporary MongoDB collection because the need appeared late in a project and MongoDB could accept simple JSON quickly without forcing a schema discussion in the middle of a deadline.\nThat kind of decision can be completely reasonable at the time.\nThe problem is what happens after the deadline passes.\nOne of the flows this applies to is offline orders. These are orders created outside of Magento’s normal storefront path, such as phone orders or EDI orders. SAP knows about them first. Magento still needs to know about them so the customer can see those orders in their account and have a consistent experience.\nThe rough flow looks like this:\n1 2 3 4 SAP -\u0026gt; Offline orders: phone orders, EDI orders -\u0026gt; Magento 2 -\u0026gt; AfterShip behind the scenes for tracking and delivery visibility We also have the reverse direction:\n1 2 3 Magento 2 -\u0026gt; SAP order export -\u0026gt; AfterShip for tracking and AI delivery estimates That second flow matters because SAP needs to be aware of orders and inventory-impacting activity originating from the e-commerce side. AfterShip then helps handle the delivery communication layer: tracking, shipment visibility, and estimated delivery intelligence.\nThe issue is not that these flows are complicated.\nThe issue is that these systems have different rules.\nSAP may allow data that Magento should reject. Magento may be stricter about customer-facing order presentation. AfterShip may need tracking context that does not exist yet. One service may be down temporarily while another continues accepting data. One error may be retryable. Another may require a human to fix the source record.\nWhen those cases are all treated the same, the integration becomes fragile.\nThe Failure Mode That Made This Obvious # The clearest example was bad order data.\nSAP allowed orders to exist with incomplete address information. In some cases, addresses were missing a country. In worse cases, the address itself was not usable.\nThat may be acceptable, or at least possible, in SAP depending on how the order was created. But Magento is customer-facing. It cannot safely present every malformed record as if it were a valid order. It needs enough structure to display the order, associate it with the customer, support downstream logic, and avoid breaking account pages or fulfillment-related processes.\nThe temporary integration path used a MongoDB database as a holding area. That made sense during the original project because the need came up late, MongoDB is schemaless, and accepting JSON quickly is one of the things MongoDB is very good at.\nBut over time, the side effect became painful:\nBad data made it into the integration layer. The database became another operational component to manage. There was no clean dead-letter queue concept. There was no first-class distinction between retryable failures and invalid payloads. Without a proper cleanup script, Magento kept retrying bad rows. Eventually, thousands of invalid records could be retried over and over again. That last part is the real problem.\nA retry system that does not know when to stop is not resilience. It is an infinite loop with infrastructure around it.\nDatabases Are Not Queues # This is one of those lessons that sounds obvious until you are staring at a table full of stuck integration records.\nA database can hold work.\nThat does not make it a queue.\nA real queue gives you behavior that a normal database table or document collection does not give you by default.\nWith SQS, I get:\nA managed buffer between producers and consumers. Visibility timeouts so one worker owns a message temporarily. Automatic message reappearance if the worker crashes or fails. Dead-letter queues after a defined number of failed receives. Message retention without building cleanup logic from scratch. CloudWatch metrics for queue depth, age, and failure behavior. Redrive workflows for replaying messages after fixes. Decoupling between systems that may be up, down, slow, or malformed independently. A database table can approximate some of that, but then I have to build the queue behavior myself.\nThat usually means inventing fields like:\n1 2 3 4 5 6 7 8 9 status retry_count last_attempt_at locked_by locked_until error_message created_at updated_at processed_at Then I need cleanup jobs, lock expiration logic, stuck record detection, retry backoff, alerting, concurrency handling, and a way to stop poison records from being retried forever.\nAt that point, I am not using a database because it is the best tool.\nI am building a worse version of a queue.\nWhy I Would Use AWS SQS FIFO Here # There are plenty of queue systems that can work here.\nRabbitMQ, Kafka, Redis streams, database-backed job systems, and managed event buses all have legitimate use cases.\nFor this particular problem, I would use AWS SQS FIFO.\nThe requirement is not stream analytics. It is not massive fanout. It is not a high-throughput clickstream. It is not an excuse to run Kafka just because the architecture diagram would look more impressive.\nThe requirement is durable, boring, operationally simple message buffering between business systems where order lifecycle events matter.\nThat last part is why FIFO matters.\nFor generic background jobs, standard SQS queues are often fine. But SAP-to-Magento offline orders and Magento-to-SAP order exports are not anonymous background jobs. They represent customer-visible order state and inventory-impacting business activity.\nFor these flows, I care about:\nKeeping events for the same order in sequence. Preventing obvious duplicate messages from being introduced into the queue. Avoiding race conditions between order-created, order-updated, shipment, cancellation, or correction events. Making each order its own ordered processing lane. Still allowing different orders to process in parallel. Keeping the operational model simple enough to run without building a science project. That is a strong fit for SQS FIFO.\nThe important nuance is this:\nFIFO improves the queue contract. It does not remove the need for idempotent consumers.\nA FIFO queue can help with message ordering and deduplication at the queue layer, but it cannot guarantee that SAP, Magento, AfterShip, or any other downstream system did not already perform a business action before a worker crashed.\nSo the architecture needs both:\n1 2 SQS FIFO for per-order sequencing and queue-level deduplication. Idempotent consumers for business-level safety. Those are not competing ideas. They solve different parts of the reliability problem.\nFIFO Does Not Replace Idempotency # This is worth being direct about.\nFIFO queues reduce duplicate and ordering problems, but consumers still need discipline.\nImagine this flow:\n1 2 3 4 5 6 7 8 1. Magento publishes an order export message. 2. The SAP export worker receives the message. 3. The worker sends the order to SAP. 4. SAP accepts the order. 5. The worker crashes before deleting the SQS message. 6. The visibility timeout expires. 7. The message becomes available again. 8. Another worker receives the same message. From SQS’s point of view, the message was not completed because it was not deleted.\nFrom SAP’s point of view, the order may already exist.\nThat means the consumer still needs to ask:\n1 Have I already exported Magento order 100012345 to SAP? For SAP-to-Magento, the same principle applies:\n1 Have I already imported SAP offline order 90012345 into Magento? FIFO helps prevent certain duplicate messages from being accepted into the queue. It helps preserve order within a message group. It helps make the integration more predictable.\nBut the consumer still has to protect the business operation.\nThat means unique idempotency keys, processed-message tracking, downstream lookup checks, or some combination of those.\nThe rule is simple:\nThe queue can protect message flow. The consumer must protect business side effects.\nThe Architecture I Would Use # For this kind of integration, I would split the flows into two FIFO queues.\nOne FIFO queue handles SAP-to-Magento offline orders.\nThe other FIFO queue handles Magento-to-SAP order export.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ┌────────────────────┐ │ SAP │ │ Phone / EDI Orders │ └─────────┬──────────┘ │ ▼ ┌────────────────────┐ │ Validation Layer │ │ Normalize Payloads │ └─────────┬──────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ SQS FIFO: sap-to-magento-offline-orders │ │ MessageGroupId: sap-order-{order_number}│ └─────────────────┬───────────────────────┘ │ ▼ ┌────────────────────┐ │ Magento Consumer │ │ Import / Present │ └─────────┬──────────┘ │ ▼ ┌────────────────────┐ │ Magento 2 │ └────────────────────┘ And the reverse direction:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ┌────────────────────┐ │ Magento 2 │ │ Ecommerce Orders │ └─────────┬──────────┘ │ ▼ ┌────────────────────┐ │ Export Builder │ │ Validate / Shape │ └─────────┬──────────┘ │ ▼ ┌─────────────────────────────────────┐ │ SQS FIFO: magento-to-sap-orders │ │ MessageGroupId: magento-order-{id} │ └─────────────────┬───────────────────┘ │ ▼ ┌────────────────────┐ │ SAP Export Worker │ │ Send / Confirm │ └─────────┬──────────┘ │ ▼ ┌────────────────────┐ │ SAP │ └─────────┬──────────┘ │ ▼ ┌────────────────────┐ │ AfterShip │ │ Tracking / ETA │ └────────────────────┘ Each FIFO queue gets its own dead-letter queue.\n1 2 3 4 5 sap-to-magento-offline-orders.fifo -\u0026gt; sap-to-magento-offline-orders-dlq.fifo magento-to-sap-orders.fifo -\u0026gt; magento-to-sap-orders-dlq.fifo That matters because failures in one direction should not block the other direction.\nA bad offline phone order from SAP should not interfere with Magento exporting valid e-commerce orders back to SAP. A temporary SAP API issue should not prevent Magento from continuing to accept orders. A malformed payload should not be retried forever just because the consumer sees “not processed yet.”\nEach flow deserves its own contract, queue, failure policy, and replay story.\nMessage Group Design Matters # The biggest mistake with FIFO queues is using one global message group.\nDo not do this:\n1 MessageGroupId = ecommerce-integrations Or this:\n1 MessageGroupId = sap-to-magento That technically works, but it serializes too much work. Every message in the group must be processed in order. If one message is slow or stuck, everything behind it in that same group waits.\nFor order workflows, the better approach is to group by order.\nFor SAP-to-Magento offline orders:\n1 MessageGroupId = sap-order-{sap_order_number} For Magento-to-SAP order export:\n1 MessageGroupId = magento-order-{increment_id} That gives me the ordering I actually care about:\n1 2 Events for order 90012345 are processed in order. Events for order 90012346 can process independently. This is the key to using FIFO without creating an unnecessary bottleneck.\nI do not need every order in the entire business to process one at a time.\nI need each individual order’s lifecycle to be consistent.\nDeduplication ID Design # The second important FIFO decision is the deduplication ID.\nFor SAP-to-Magento, a simple offline order created event might use:\n1 MessageDeduplicationId = sap-offline-order-{sap_order_number}-created-v1 For Magento-to-SAP order export:\n1 MessageDeduplicationId = magento-order-export-{increment_id}-created-v1 If the same order can emit multiple meaningful events, the event type should be part of the deduplication ID:\n1 magento-order-{increment_id}-{event_type}-{event_version} For example:\n1 2 3 4 magento-order-100012345-created-v1 magento-order-100012345-updated-v2 magento-order-100012345-cancelled-v1 magento-order-100012345-shipped-v1 The goal is to deduplicate accidental duplicate submissions without collapsing distinct business events into one message.\nThat distinction matters.\nIf two messages represent the same business action, they should deduplicate.\nIf two messages represent different business actions for the same order, they should both be processed in order.\nThe Most Important Rule: Validate Before the Queue # One mistake I would avoid is treating SQS as a junk drawer.\nThe queue should not become the place where completely unvalidated payloads go to die.\nFor SAP-to-Magento offline orders, the validation layer should happen before the message is accepted as real work. That does not mean the payload has to be perfect in every business sense, but it should meet a minimum contract.\nFor example:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 { \u0026#34;schemaVersion\u0026#34;: \u0026#34;1.0\u0026#34;, \u0026#34;eventType\u0026#34;: \u0026#34;offline_order.created\u0026#34;, \u0026#34;sourceSystem\u0026#34;: \u0026#34;sap\u0026#34;, \u0026#34;destinationSystem\u0026#34;: \u0026#34;magento\u0026#34;, \u0026#34;correlationId\u0026#34;: \u0026#34;01J2RC8A7P8K9WQ4JZP6N3N2HY\u0026#34;, \u0026#34;idempotencyKey\u0026#34;: \u0026#34;sap-offline-order-90012345-created-v1\u0026#34;, \u0026#34;occurredAt\u0026#34;: \u0026#34;2026-07-06T12:45:00Z\u0026#34;, \u0026#34;payload\u0026#34;: { \u0026#34;sapOrderNumber\u0026#34;: \u0026#34;90012345\u0026#34;, \u0026#34;customerEmail\u0026#34;: \u0026#34;customer@example.com\u0026#34;, \u0026#34;billingAddress\u0026#34;: { \u0026#34;firstName\u0026#34;: \u0026#34;Jane\u0026#34;, \u0026#34;lastName\u0026#34;: \u0026#34;Doe\u0026#34;, \u0026#34;street1\u0026#34;: \u0026#34;123 Example Street\u0026#34;, \u0026#34;city\u0026#34;: \u0026#34;Knoxville\u0026#34;, \u0026#34;region\u0026#34;: \u0026#34;TN\u0026#34;, \u0026#34;postalCode\u0026#34;: \u0026#34;37901\u0026#34;, \u0026#34;countryCode\u0026#34;: \u0026#34;US\u0026#34; }, \u0026#34;shippingAddress\u0026#34;: { \u0026#34;firstName\u0026#34;: \u0026#34;Jane\u0026#34;, \u0026#34;lastName\u0026#34;: \u0026#34;Doe\u0026#34;, \u0026#34;street1\u0026#34;: \u0026#34;123 Example Street\u0026#34;, \u0026#34;city\u0026#34;: \u0026#34;Knoxville\u0026#34;, \u0026#34;region\u0026#34;: \u0026#34;TN\u0026#34;, \u0026#34;postalCode\u0026#34;: \u0026#34;37901\u0026#34;, \u0026#34;countryCode\u0026#34;: \u0026#34;US\u0026#34; }, \u0026#34;items\u0026#34;: [ { \u0026#34;sku\u0026#34;: \u0026#34;RC-12345\u0026#34;, \u0026#34;quantity\u0026#34;: 1 } ] } } Before that message goes to SQS, I want to know basic things:\nIs there a SAP order number? Is there an idempotency key? Is there a customer identifier, such as email or customer ID? Is there an address? Is the country present? Are required address fields present? Are line items present? Are quantities valid? Is the schema version supported? If the payload fails that contract, it should not enter the normal processing queue.\nDepending on the business need, it can go to a separate invalid-payload queue, validation error table, or operational review process. But it should not sit in the main queue pretending to be retryable work.\nThat distinction is critical.\nA missing country code is probably not a transient failure.\nRetrying it 3,000 times will not magically create a country.\nRetryable Failure vs. Invalid Payload # This is where a lot of integrations go wrong.\nNot all failures are the same.\nA retryable failure is something like:\nSAP API timeout. Magento database connection issue. Temporary network error. AfterShip API rate limit. Worker crashes mid-processing. Downstream service is unavailable. Lock contention or temporary resource exhaustion. An invalid payload is something like:\nMissing country. Missing address. Missing order number. Invalid SKU. Unsupported country code. Missing customer association. Quantity is zero or negative. Payload does not match the expected schema. Those two categories should not use the same recovery path.\nRetryable failures should retry.\nInvalid payloads should be rejected, quarantined, or routed to a place where humans or cleanup automation can fix the source data.\nA queue with a DLQ helps, but the consumer still needs to be smart enough to classify errors.\nFor example:\n1 2 3 4 5 6 7 8 9 Receive message -\u0026gt; Validate schema -\u0026gt; Invalid? Send to validation failure path and delete from queue -\u0026gt; Check idempotency -\u0026gt; Already processed? Delete from queue -\u0026gt; Process downstream -\u0026gt; Success? Mark processed and delete from queue -\u0026gt; Retryable failure? Do not delete; allow retry -\u0026gt; Permanent failure? Send to DLQ or failure store and delete That one distinction prevents the system from becoming a retry machine for bad data.\nTerraform Example: Two FIFO Queues, Two FIFO DLQs # This is a simplified Terraform example of how I would define the queues.\nThe exact values should be tuned to the workload, but the important pieces are:\nSeparate FIFO queues per direction. Separate FIFO DLQs per direction. Content-based deduplication disabled so the application controls deduplication IDs. A clear maxReceiveCount. Message retention long enough to investigate issues. Visibility timeout long enough for normal processing. Server-side encryption enabled. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 resource \u0026#34;aws_sqs_queue\u0026#34; \u0026#34;sap_to_magento_offline_orders_dlq\u0026#34; { name = \u0026#34;sap-to-magento-offline-orders-dlq.fifo\u0026#34; fifo_queue = true content_based_deduplication = false message_retention_seconds = 1209600 # 14 days sqs_managed_sse_enabled = true } resource \u0026#34;aws_sqs_queue\u0026#34; \u0026#34;sap_to_magento_offline_orders\u0026#34; { name = \u0026#34;sap-to-magento-offline-orders.fifo\u0026#34; fifo_queue = true content_based_deduplication = false visibility_timeout_seconds = 300 # 5 minutes message_retention_seconds = 1209600 # 14 days sqs_managed_sse_enabled = true redrive_policy = jsonencode({ deadLetterTargetArn = aws_sqs_queue.sap_to_magento_offline_orders_dlq.arn maxReceiveCount = 3 }) } resource \u0026#34;aws_sqs_queue\u0026#34; \u0026#34;magento_to_sap_orders_dlq\u0026#34; { name = \u0026#34;magento-to-sap-orders-dlq.fifo\u0026#34; fifo_queue = true content_based_deduplication = false message_retention_seconds = 1209600 # 14 days sqs_managed_sse_enabled = true } resource \u0026#34;aws_sqs_queue\u0026#34; \u0026#34;magento_to_sap_orders\u0026#34; { name = \u0026#34;magento-to-sap-orders.fifo\u0026#34; fifo_queue = true content_based_deduplication = false visibility_timeout_seconds = 300 # 5 minutes message_retention_seconds = 1209600 # 14 days sqs_managed_sse_enabled = true redrive_policy = jsonencode({ deadLetterTargetArn = aws_sqs_queue.magento_to_sap_orders_dlq.arn maxReceiveCount = 3 }) } I like starting with maxReceiveCount = 3 for business integrations because it keeps poison messages from living forever in the hot path.\nThat does not mean every message only gets three total attempts forever. It means after three failed receives, the system admits, “This needs a different recovery path.”\nThat is healthy.\nProducer Responsibility # The producer should not just throw raw data into the queue.\nFor SAP-to-Magento, the producer or ingestion layer should normalize the SAP payload into a contract Magento understands.\nFor Magento-to-SAP, the export process should build a clean message that SAP can process predictably.\nA producer should be responsible for:\nCreating the message envelope. Setting the schema version. Creating a stable idempotency key. Creating a stable FIFO message group ID. Creating a stable FIFO deduplication ID. Including a correlation ID. Performing basic validation. Avoiding sensitive data unless it is absolutely required. Sending only the data needed for the consumer to do its job. Example producer behavior:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 \u0026lt;?php $sapOrderNumber = \u0026#39;90012345\u0026#39;; $eventType = \u0026#39;offline_order.created\u0026#39;; $eventVersion = \u0026#39;v1\u0026#39;; $message = [ \u0026#39;schemaVersion\u0026#39; =\u0026gt; \u0026#39;1.0\u0026#39;, \u0026#39;eventType\u0026#39; =\u0026gt; $eventType, \u0026#39;sourceSystem\u0026#39; =\u0026gt; \u0026#39;sap\u0026#39;, \u0026#39;destinationSystem\u0026#39; =\u0026gt; \u0026#39;magento\u0026#39;, \u0026#39;correlationId\u0026#39; =\u0026gt; $correlationId, \u0026#39;idempotencyKey\u0026#39; =\u0026gt; sprintf( \u0026#39;sap-offline-order-%s-created-%s\u0026#39;, $sapOrderNumber, $eventVersion ), \u0026#39;occurredAt\u0026#39; =\u0026gt; gmdate(\u0026#39;c\u0026#39;), \u0026#39;payload\u0026#39; =\u0026gt; [ \u0026#39;sapOrderNumber\u0026#39; =\u0026gt; $sapOrderNumber, \u0026#39;customerEmail\u0026#39; =\u0026gt; $customerEmail, \u0026#39;billingAddress\u0026#39; =\u0026gt; $billingAddress, \u0026#39;shippingAddress\u0026#39; =\u0026gt; $shippingAddress, \u0026#39;items\u0026#39; =\u0026gt; $items, ], ]; $validator-\u0026gt;assertValidOfflineOrder($message); $sqsClient-\u0026gt;sendMessage([ \u0026#39;QueueUrl\u0026#39; =\u0026gt; $queueUrl, \u0026#39;MessageBody\u0026#39; =\u0026gt; json_encode($message, JSON_THROW_ON_ERROR), // FIFO ordering lane: all events for this SAP order process in order. \u0026#39;MessageGroupId\u0026#39; =\u0026gt; sprintf(\u0026#39;sap-order-%s\u0026#39;, $sapOrderNumber), // FIFO deduplication key: duplicate submissions of this exact event collapse. \u0026#39;MessageDeduplicationId\u0026#39; =\u0026gt; sprintf( \u0026#39;sap-offline-order-%s-created-%s\u0026#39;, $sapOrderNumber, $eventVersion ), \u0026#39;MessageAttributes\u0026#39; =\u0026gt; [ \u0026#39;eventType\u0026#39; =\u0026gt; [ \u0026#39;DataType\u0026#39; =\u0026gt; \u0026#39;String\u0026#39;, \u0026#39;StringValue\u0026#39; =\u0026gt; $eventType, ], \u0026#39;schemaVersion\u0026#39; =\u0026gt; [ \u0026#39;DataType\u0026#39; =\u0026gt; \u0026#39;String\u0026#39;, \u0026#39;StringValue\u0026#39; =\u0026gt; \u0026#39;1.0\u0026#39;, ], ], ]); The exact implementation can vary, but the principle matters:\nThe queue should contain intentional messages, not mystery JSON.\nConsumer Responsibility # The consumer is where discipline really matters.\nA Magento consumer importing offline orders should be able to answer these questions every time it receives a message:\nIs this message valid? Have I already processed it? Can I safely process it now? If processing fails, is the failure retryable? Should this message be deleted, retried, or sent to a failure path? A simplified worker loop looks like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 \u0026lt;?php while (true) { $result = $sqsClient-\u0026gt;receiveMessage([ \u0026#39;QueueUrl\u0026#39; =\u0026gt; $queueUrl, \u0026#39;MaxNumberOfMessages\u0026#39; =\u0026gt; 10, \u0026#39;WaitTimeSeconds\u0026#39; =\u0026gt; 20, // long polling \u0026#39;VisibilityTimeout\u0026#39; =\u0026gt; 300, ]); foreach ($result[\u0026#39;Messages\u0026#39;] ?? [] as $message) { $receiptHandle = $message[\u0026#39;ReceiptHandle\u0026#39;]; try { $body = json_decode($message[\u0026#39;Body\u0026#39;], true, flags: JSON_THROW_ON_ERROR); $validator-\u0026gt;assertSupportedSchema($body); $validator-\u0026gt;assertValidOfflineOrder($body); $idempotencyKey = $body[\u0026#39;idempotencyKey\u0026#39;]; if ($idempotencyStore-\u0026gt;hasProcessed($idempotencyKey)) { $sqsClient-\u0026gt;deleteMessage([ \u0026#39;QueueUrl\u0026#39; =\u0026gt; $queueUrl, \u0026#39;ReceiptHandle\u0026#39; =\u0026gt; $receiptHandle, ]); continue; } $offlineOrderImporter-\u0026gt;import($body[\u0026#39;payload\u0026#39;]); $idempotencyStore-\u0026gt;markProcessed($idempotencyKey); $sqsClient-\u0026gt;deleteMessage([ \u0026#39;QueueUrl\u0026#39; =\u0026gt; $queueUrl, \u0026#39;ReceiptHandle\u0026#39; =\u0026gt; $receiptHandle, ]); } catch (InvalidPayloadException $exception) { $failureStore-\u0026gt;recordValidationFailure($message, $exception); // This is not retryable. Delete it from the hot queue. $sqsClient-\u0026gt;deleteMessage([ \u0026#39;QueueUrl\u0026#39; =\u0026gt; $queueUrl, \u0026#39;ReceiptHandle\u0026#39; =\u0026gt; $receiptHandle, ]); } catch (RetryableIntegrationException $exception) { $logger-\u0026gt;warning(\u0026#39;Retryable integration failure\u0026#39;, [ \u0026#39;error\u0026#39; =\u0026gt; $exception-\u0026gt;getMessage(), ]); // Do not delete the message. // SQS will make it visible again after the visibility timeout. } catch (Throwable $exception) { $logger-\u0026gt;error(\u0026#39;Unexpected queue consumer failure\u0026#39;, [ \u0026#39;error\u0026#39; =\u0026gt; $exception-\u0026gt;getMessage(), ]); // Usually do not delete here. // Let SQS retry and eventually move it to the DLQ. } } } The important part is not the exact PHP.\nThe important part is the behavior.\nBad payloads do not retry forever.\nSuccessful messages are deleted.\nDuplicate messages are safe.\nTemporary failures retry.\nUnexpected failures eventually end up in the DLQ.\nIdempotency Is Not Optional # Even with FIFO queues, idempotency is not optional.\nThis is one of the easiest mistakes to make when discussing FIFO.\nFIFO helps with ordering and deduplication at the queue layer. It does not guarantee the business operation on the other side did not already happen.\nFor SAP-to-Magento offline orders, the idempotency key might be:\n1 sap-offline-order-{sap_order_number}-created-v1 For Magento-to-SAP exports, it might be:\n1 magento-order-export-{magento_order_increment_id}-created-v1 If the same order can have multiple export events:\n1 magento-order-export-{magento_order_increment_id}-{event_type}-{version} The idempotency store can be a database table with a unique constraint.\n1 2 3 4 5 6 7 8 9 CREATE TABLE integration_idempotency ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, idempotency_key VARCHAR(191) NOT NULL, source_system VARCHAR(50) NOT NULL, destination_system VARCHAR(50) NOT NULL, event_type VARCHAR(100) NOT NULL, processed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uniq_idempotency_key (idempotency_key) ); Then the worker can safely say:\n1 2 3 Have I processed sap-offline-order-90012345-created-v1 before? -\u0026gt; Yes: delete message and move on. -\u0026gt; No: process it, then mark it processed. Without idempotency, retries can create duplicate side effects.\nThat is how you get duplicate exports, duplicate customer emails, duplicate order notes, or repeated calls into downstream systems.\nFIFO makes integrations more predictable.\nIdempotency makes them safe.\nDead-Letter Queues Are Not Trash Cans # A DLQ is not where messages go to be forgotten.\nA DLQ is an operational work queue.\nThat means it needs ownership.\nFor each DLQ, I would want:\nCloudWatch alarms when visible messages are greater than zero. A dashboard showing message count and oldest message age. Logging that includes correlation IDs and idempotency keys. A replay process. A way to inspect failure reasons. A documented owner for each integration path. For example, an alarm on the SAP-to-Magento DLQ could mean:\n1 2 An offline order from SAP failed Magento import after multiple attempts. It may be malformed, missing required fields, or blocked by a downstream Magento issue. An alarm on the Magento-to-SAP DLQ could mean:\n1 2 A Magento order export failed after multiple attempts. SAP may be unavailable, rejecting the payload, or receiving data that does not match its contract. Those are different operational problems.\nThey deserve different alerts, different dashboards, and potentially different escalation paths.\nObservability: What I Would Watch # Queues make system behavior visible if you actually watch the right metrics.\nAt minimum, I would monitor these for each queue:\nApproximate number of visible messages. Approximate number of in-flight messages. Approximate age of oldest message. DLQ visible message count. Consumer error rate. Consumer processing duration. Number of validation failures. Number of successful imports/exports. Number of idempotency skips. Downstream API latency and failure rate. The most important metric is often the age of the oldest message.\nQueue depth tells me how much work is waiting.\nOldest message age tells me whether the system is falling behind.\nA queue with 2,000 messages that drains quickly may be fine after a batch import.\nA queue with one message that is six hours old may be a serious business issue.\nFor business integrations, I also want application-level metrics:\n1 2 3 4 5 6 7 8 9 offline_orders_received_from_sap offline_orders_imported_to_magento offline_orders_rejected_validation offline_orders_sent_to_dlq magento_orders_queued_for_sap magento_orders_exported_to_sap magento_orders_failed_retryable magento_orders_sent_to_dlq Infrastructure metrics tell me whether the queue is healthy.\nBusiness metrics tell me whether the integration is doing its job.\nBoth matter.\nWhere AfterShip Fits # AfterShip should not be treated as an afterthought just because it sits behind the scenes.\nTracking and delivery visibility are part of the customer experience.\nIf a customer places a phone order or EDI order, Magento still needs to present that order in a way that feels consistent with a normal web order. If tracking exists, AfterShip can help provide shipment visibility and delivery estimates.\nBut that does not mean every system needs to directly call every other system in real time.\nA better model is to let each integration boundary do one job well.\nSAP-to-Magento offline order queue:\n1 Make Magento aware of valid offline orders. Magento-to-SAP order export queue:\n1 Make SAP aware of valid Magento-originated orders and inventory-impacting activity. AfterShip integration:\n1 Handle shipment tracking, delivery updates, and estimated delivery communication. Those flows may be related, but they should not all be one giant synchronous chain.\nBecause if one link in that chain fails, everything fails.\nQueues let each piece recover independently.\nHandling Invalid SAP Data # The missing country problem is a perfect example of why validation matters.\nA naive integration says:\n1 2 SAP sent an order. Put it in Magento. A better integration says:\n1 2 SAP sent an order. Does it meet Magento’s minimum customer-facing order contract? If the answer is no, then the system should create a clear failure record.\nFor example:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { \u0026#34;failureType\u0026#34;: \u0026#34;validation_error\u0026#34;, \u0026#34;sourceSystem\u0026#34;: \u0026#34;sap\u0026#34;, \u0026#34;destinationSystem\u0026#34;: \u0026#34;magento\u0026#34;, \u0026#34;eventType\u0026#34;: \u0026#34;offline_order.created\u0026#34;, \u0026#34;idempotencyKey\u0026#34;: \u0026#34;sap-offline-order-90012345-created-v1\u0026#34;, \u0026#34;correlationId\u0026#34;: \u0026#34;01J2RC8A7P8K9WQ4JZP6N3N2HY\u0026#34;, \u0026#34;errors\u0026#34;: [ { \u0026#34;field\u0026#34;: \u0026#34;shippingAddress.countryCode\u0026#34;, \u0026#34;message\u0026#34;: \u0026#34;Country code is required\u0026#34; } ], \u0026#34;receivedAt\u0026#34;: \u0026#34;2026-07-06T12:45:00Z\u0026#34; } That gives the team something actionable.\nInstead of “Magento keeps retrying thousands of rows,” the conversation becomes:\n1 2 3 4 5 SAP is sending offline orders without required address fields. Here are the affected order numbers. Here is the exact field missing. Here is when the invalid payload was received. Here is whether it has been corrected and replayed. That is a much healthier operational model.\nReplay Tooling Matters # Eventually, someone will fix the bad data.\nWhen that happens, the team needs a safe way to replay messages.\nThat replay process should be intentional. I do not want someone manually copying JSON out of AWS and pasting it into a random script on their laptop.\nA simple CLI command is enough to start:\n1 2 3 4 5 php bin/magento integration:replay \\ --source=sap \\ --destination=magento \\ --event-type=offline_order.created \\ --idempotency-key=sap-offline-order-90012345-created-v1 Or for a DLQ redrive:\n1 2 3 aws sqs start-message-move-task \\ --source-arn arn:aws:sqs:us-east-1:123456789012:sap-to-magento-offline-orders-dlq.fifo \\ --destination-arn arn:aws:sqs:us-east-1:123456789012:sap-to-magento-offline-orders.fifo The exact replay mechanism depends on how the system stores failures, but the rules should be clear:\nDo not replay until the root cause is fixed. Do not replay invalid messages blindly. Preserve correlation IDs. Preserve or intentionally regenerate idempotency keys. Preserve correct FIFO message group IDs. Preserve correct FIFO deduplication behavior. Log who replayed what and when. Replay in small batches first. Watch downstream metrics during replay. Replay is not just a technical feature.\nReplay is part of the operational contract.\nWhy Separate Queues Matter # It can be tempting to create one general-purpose queue called something like:\n1 ecommerce-integrations.fifo I would avoid that.\nSeparate queues make ownership and failure behavior much clearer.\nSAP-to-Magento offline orders and Magento-to-SAP order exports are different workflows.\nThey have different producers.\nThey have different consumers.\nThey have different payloads.\nThey have different retry behavior.\nThey have different business impact.\nThey should not share the same queue just because they both involve orders.\nA SAP offline order import failing means customers may not see an offline order in their account.\nA Magento order export failing means SAP may not receive an e-commerce order or inventory-impacting update correctly.\nThose are not the same problem.\nSeparate queues make that obvious.\nWhen I Would Still Use Standard Queues # This does not mean FIFO queues should be used everywhere.\nStandard queues still make sense when each message is independent and ordering does not matter.\nExamples:\n1 2 3 4 5 6 Recalculate product metadata. Send non-critical analytics events. Fan out independent background jobs. Refresh cache entries. Queue isolated records where latest-state validation handles ordering. Process events where duplicate delivery has no meaningful side effect. Standard queues are simple, scalable, and perfectly valid for many workloads.\nBut that is not the core case in this post.\nThis post is about SAP, Magento, offline orders, order export, customer-visible state, inventory-impacting activity, tracking updates, and business systems that can disagree about whether data is valid.\nFor that kind of workflow, I want per-order sequencing and queue-level deduplication.\nThat pushes me toward FIFO.\nCommon Gotchas # 1. Using One Global Message Group # FIFO queues preserve order within a message group.\nIf every message uses the same group, everything becomes serialized.\nBad:\n1 MessageGroupId = sap-to-magento Better:\n1 MessageGroupId = sap-order-90012345 That lets one order process in sequence without blocking every other order.\n2. Assuming FIFO Means Idempotency Is Solved # FIFO helps, but downstream side effects can still happen before a message is deleted.\nConsumers still need business-level idempotency.\n3. Retrying Bad Data Forever # If the payload is missing required fields, more retries will not fix it.\nThe worker needs to classify invalid payloads separately from retryable failures.\n4. Choosing the Wrong Deduplication ID # If the deduplication ID is too broad, different business events may collapse into one message.\nIf it is too narrow, obvious duplicates may not deduplicate.\nThe deduplication ID should represent the specific business event.\n5. Making the Visibility Timeout Too Short # If the visibility timeout is shorter than the processing time, another worker may receive the same message while the first worker is still processing it.\nSet the timeout based on realistic processing duration.\nIf some jobs take longer, extend visibility during processing or split the work into smaller messages.\n6. Making the Message Too Large # SQS has message size limits.\nDo not shove giant order documents into the queue if they do not need to be there.\nFor larger payloads, store the full document in S3 and send a pointer in the message.\n7. Treating the DLQ as the End of the Story # A DLQ without alerts is just a quieter failure.\nA DLQ needs alarms, dashboards, ownership, and replay tooling.\n8. Ignoring Schema Versioning # Message contracts change.\nAdd schemaVersion from the beginning.\nConsumers should reject unsupported versions clearly instead of failing mysteriously.\n9. Mixing Business Failures With Infrastructure Failures # A missing country code is not the same as a SAP timeout.\nOne needs data correction.\nThe other needs retry.\nDo not send both through the same mental model.\nA Practical Migration Path # I would not cut this over recklessly, especially around a major WMS move or any other high-risk operational window.\nThe safest path is incremental.\nPhase 1: Define the Message Contracts # Before touching infrastructure, define the contracts.\nFor SAP-to-Magento:\n1 What does Magento require to present an offline order safely? For Magento-to-SAP:\n1 What does SAP require to accept an e-commerce order export safely? This includes required fields, optional fields, schema versions, idempotency keys, message group IDs, deduplication IDs, and validation rules.\nPhase 2: Add SQS FIFO Infrastructure # Create the two FIFO queues and their FIFO DLQs.\nDo not cut production traffic yet.\nJust establish the infrastructure, IAM permissions, alarms, and dashboards.\nPhase 3: Shadow Publish # Start publishing messages to SQS while the existing path still runs.\nDo not consume them for business impact yet.\nUse this phase to inspect real payloads and validate assumptions.\nThis is where the ugly data shows up.\nThat is good.\nI would rather find the missing country problem in shadow mode than during a hard cutover.\nPhase 4: Build Consumers # Build the Magento import consumer and SAP export worker.\nMake them idempotent from day one.\nAdd structured logs, metrics, and failure classification.\nPhase 5: Controlled Cutover # Move one flow first.\nI would probably start with the less risky direction or a limited subset of orders.\nWatch queue age, DLQ count, validation failures, processing time, deduplication behavior, and business-level reconciliation.\nPhase 6: Replay and Cleanup # Once the queue-based flow is stable, clean up the old database-backed retry path.\nThis is important.\nLeaving the old path around forever creates confusion.\nEventually, there should be one clear source of truth for each integration workflow.\nThe Bigger Lesson # The real lesson is not “use SQS.”\nThe real lesson is that integration architecture should respect failure boundaries.\nSAP, Magento, WMS, and AfterShip do not fail the same way.\nThey do not validate data the same way.\nThey do not recover the same way.\nThey do not have the same operational ownership.\nThey should not be glued together as if they are one perfectly reliable system.\nA queue gives each side room to breathe.\nSAP can produce an offline order event.\nMagento can consume it when ready.\nInvalid data can be rejected clearly.\nTemporary failures can retry.\nPoison messages can land in a DLQ.\nOperators can inspect, fix, and replay.\nFIFO queues add another important layer: order-aware processing.\nFor SAP-to-Magento and Magento-to-SAP, that matters. These are not disposable events. They are order lifecycle events. They deserve per-order sequencing, deduplication, and business-safe idempotency.\nThat is the difference between a fragile integration and an operationally sane one.\nFor e-commerce systems, that difference matters.\nBecause when order data is wrong, customers feel it.\nWhen tracking data is late, customers feel it.\nWhen inventory updates are delayed, customers feel it.\nWhen retries pile up silently for weeks, the business eventually feels it.\nQueues do not eliminate integration problems.\nThey make those problems visible, recoverable, ordered, and contained.\nThat is exactly why they belong between systems that do not fail the same way.\n","date":"6 July 2026","externalUrl":null,"permalink":"/posts/why-queues-belong-between-systems-that-dont-fail-the-same-way/","section":"Posts","summary":"When SAP, Magento, WMS, and AfterShip all participate in the same order lifecycle, direct integrations become fragile fast. This post explains why AWS SQS FIFO queues belong between systems that fail differently, using real e-commerce flows like offline orders, order export, tracking updates, retries, bad data, dead-letter queues, and idempotent consumers.","title":"Why Queues Belong Between Systems That Don’t Fail the Same Way","type":"posts"},{"content":"","date":"29 June 2026","externalUrl":null,"permalink":"/tags/arch-linux/","section":"Tags","summary":"","title":"Arch Linux","type":"tags"},{"content":"","date":"29 June 2026","externalUrl":null,"permalink":"/tags/bitwarden/","section":"Tags","summary":"","title":"Bitwarden","type":"tags"},{"content":"","date":"29 June 2026","externalUrl":null,"permalink":"/tags/chezmoi/","section":"Tags","summary":"","title":"Chezmoi","type":"tags"},{"content":"","date":"29 June 2026","externalUrl":null,"permalink":"/tags/dotfiles/","section":"Tags","summary":"","title":"Dotfiles","type":"tags"},{"content":"","date":"29 June 2026","externalUrl":null,"permalink":"/categories/linux/","section":"Categories","summary":"","title":"Linux","type":"categories"},{"content":"","date":"29 June 2026","externalUrl":null,"permalink":"/series/reproducible-linux-workstation/","section":"Series","summary":"","title":"Reproducible Linux Workstation","type":"series"},{"content":"","date":"29 June 2026","externalUrl":null,"permalink":"/tags/secrets-management/","section":"Tags","summary":"","title":"Secrets Management","type":"tags"},{"content":" Secrets Management for Dotfiles: Templates, Bitwarden, and Safe Automation # A reproducible desktop setup is only useful if it can rebuild the parts that actually matter.\nIt is easy to version control shell aliases, Hyprland configs, Waybar themes, Git settings, package lists, and service files. That is the comfortable part of dotfiles. The uncomfortable part is everything those configs eventually touch:\nSSH private keys API tokens Wi-Fi credentials Git signing material Cloud provider credentials Personal service tokens Local .env values Application-specific secrets that quietly accumulate over time That is where dotfiles can get dangerous.\nMy rule is simple:\nSecrets never go in the dotfiles repo. Ever.\nNot plaintext. Not “temporarily.” Not because the repo is private. Not because I plan to clean it up later. Not even encrypted unless I have a very specific reason and a very specific threat model.\nFor my workstation setup, Bitwarden is the source of truth for secrets. Chezmoi is responsible for rendering templates. Git is responsible for versioning the structure around those templates.\nThat separation matters.\nGit stores the shape of the machine.\nBitwarden stores the sensitive values.\nChezmoi joins them together at apply time.\nThe Problem With “Just Put It in Dotfiles” # Dotfiles start innocently.\nFirst it is a .zshrc.\nThen it is Git config.\nThen it is SSH config.\nThen it is application settings.\nThen it is a helper script.\nThen one day there is an API token inside a shell export because it was faster to paste it there than wire up proper secret management.\nThat is usually how secrets leak. Not because someone sat down and made a reckless architectural decision, but because convenience slowly beat discipline.\nThe risks are obvious:\nA private repo can become public. A committed secret lives forever in Git history unless aggressively purged. A backup, fork, clone, or CI job can preserve data you thought you removed. A debug log can print values during bootstrap. A local machine can become harder to audit because secrets are scattered everywhere. For a reproducible workstation, I want the opposite.\nI want my dotfiles repo to be boring. If someone sees it, the worst thing they should get is my preferred editor settings, Hyprland structure, shell aliases, and maybe some strong opinions about Linux.\nThey should not get credentials.\nThe Architecture: Git for Configuration, Bitwarden for Secrets # The architecture I want looks like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 ┌──────────────────────────┐ │ Dotfiles Git Repository │ │ │ │ - templates │ │ - scripts │ │ - package lists │ │ - service definitions │ │ - non-secret defaults │ └─────────────┬────────────┘ │ │ chezmoi apply │ ▼ ┌──────────────────────────┐ │ Chezmoi Template Engine │ │ │ │ - renders .tmpl files │ │ - calls Bitwarden CLI │ │ - writes local files │ └─────────────┬────────────┘ │ │ bw get / session unlock │ ▼ ┌──────────────────────────┐ │ Bitwarden Vault │ │ │ │ - SSH private keys │ │ - API tokens │ │ - Wi-Fi credentials │ │ - local credentials │ └─────────────┬────────────┘ │ ▼ ┌──────────────────────────┐ │ Local Workstation │ │ │ │ - ~/.ssh/id_ed25519 │ │ - ~/.config/... │ │ - NetworkManager config │ │ - app credentials │ └──────────────────────────┘ The important part is that the secret does not move through Git.\nThe repo only contains references like:\n1 {{ (bitwardenFields \u0026#34;item\u0026#34; \u0026#34;github-api-token\u0026#34;).token.value }} or:\n1 {{ bitwardenAttachmentByRef \u0026#34;id_ed25519\u0026#34; \u0026#34;item\u0026#34; \u0026#34;ssh-main-key\u0026#34; }} Those references are useless without an unlocked Bitwarden session on the local machine.\nThat gives me the automation I want without turning my dotfiles into a credential dump.\nWhy Bitwarden Fits This Workflow # I use Bitwarden here because it gives me a practical balance between security and usability.\nFor a workstation bootstrap, I do not want a complicated enterprise secrets platform just to restore my SSH key and a handful of local credentials. I also do not want secrets copied into random shell files.\nBitwarden gives me:\nA real encrypted vault A CLI interface through bw Cross-machine access Attachments for things like SSH private keys Custom fields for API tokens and structured values A workflow that pauses for human unlock when needed Chezmoi can call Bitwarden from templates, so I can keep my dotfiles declarative while still pulling sensitive values from the vault during chezmoi apply.\nThat means a fresh machine can be rebuilt with a flow like this:\n1 2 chezmoi init git@github.com:dcajio/dotfiles.git chezmoi apply At some point, the process should pause and require me to unlock Bitwarden.\nThat pause is a feature, not a bug.\nI do not want a fully unattended system silently pulling every secret I own onto a new machine. I want safe automation: automate the boring parts, but require explicit authentication before sensitive material is rendered locally.\nInstalling the Required Tools # On Arch, the packages I care about are:\n1 yay -S chezmoi bitwarden-cli jq Depending on the machine and package availability, bitwarden-cli may come from the AUR. The jq package is not strictly required for chezmoi templates, but it is useful when inspecting Bitwarden CLI output manually.\nI also want to confirm the CLI works before wiring it into automation:\n1 2 bw --version chezmoi --version Then I log in:\n1 bw login Or, if already logged in, unlock the vault:\n1 export BW_SESSION=\u0026#34;$(bw unlock --raw)\u0026#34; The important part is BW_SESSION.\nThat environment variable represents the active unlocked Bitwarden CLI session. If it is not set, bw may require an unlock before it can retrieve secrets.\nFor my workflow, I want this to be temporary. I do not want to permanently export BW_SESSION in .zshrc, commit it somewhere, or write it into a log.\nA session token is sensitive. Treat it like a secret.\nConfiguring Chezmoi to Use Bitwarden # Chezmoi can be configured to automatically unlock Bitwarden when needed.\nIn my chezmoi config:\n1 2 3 4 # ~/.config/chezmoi/chezmoi.toml [bitwarden] unlock = \u0026#34;auto\u0026#34; With this enabled, chezmoi can call bw unlock when BW_SESSION is missing.\nThat gives me the behavior I want during a fresh machine bootstrap:\nStart chezmoi apply. Chezmoi reaches a template that needs Bitwarden. Bitwarden requires unlock. I authenticate. Chezmoi renders the file locally. Secrets are written only to the destination machine. This keeps the process automated without pretending secrets should be unattended.\nVault Organization # The biggest mistake I see with password-manager-backed dotfiles is poor naming.\nIf the vault is messy, the templates become messy.\nI prefer names that describe infrastructure purpose, not just application names.\nFor example:\n1 2 3 4 5 6 dotfiles/ssh/main-ed25519 dotfiles/api/github-personal-token dotfiles/api/cloudflare-token dotfiles/wifi/home-network dotfiles/git/signing-key dotfiles/aws/personal-lab Inside Bitwarden, I would use a folder or collection like:\n1 Dotfiles Then each item should have predictable fields.\nFor API tokens, I like custom fields:\n1 2 3 4 5 Item name: dotfiles/api/github-personal-token Custom fields: token = ghp_xxxxxxxxxxxxxxxxxxxx note = Used for local GitHub CLI/dev workflows For Wi-Fi:\n1 2 3 4 5 Item name: dotfiles/wifi/home-network Custom fields: ssid = MyNetwork password = super-secret-password For SSH keys, I prefer attachments:\n1 2 3 4 5 Item name: dotfiles/ssh/main-ed25519 Attachments: id_ed25519 id_ed25519.pub You could store the SSH private key as a secure note field, but I prefer attachments because it maps cleanly to files.\nThe goal is consistency. Templates should be boring because the vault structure is predictable.\nRendering an SSH Key With Chezmoi # For SSH, I want the private key restored locally with strict permissions.\nIn the chezmoi source directory, the target file might look like this:\n1 ~/.local/share/chezmoi/private_dot_ssh/private_id_ed25519.tmpl The private_ prefix tells chezmoi to apply restrictive permissions.\nInside the template:\n1 {{ bitwardenAttachmentByRef \u0026#34;id_ed25519\u0026#34; \u0026#34;item\u0026#34; \u0026#34;dotfiles/ssh/main-ed25519\u0026#34; }} For the public key:\n1 ~/.local/share/chezmoi/private_dot_ssh/id_ed25519.pub.tmpl 1 {{ bitwardenAttachmentByRef \u0026#34;id_ed25519.pub\u0026#34; \u0026#34;item\u0026#34; \u0026#34;dotfiles/ssh/main-ed25519\u0026#34; }} After applying:\n1 chezmoi apply I still like to verify permissions:\n1 ls -la ~/.ssh Expected result:\n1 2 -rw------- id_ed25519 -rw-r--r-- id_ed25519.pub If needed:\n1 2 3 chmod 700 ~/.ssh chmod 600 ~/.ssh/id_ed25519 chmod 644 ~/.ssh/id_ed25519.pub I would usually enforce that with a script rather than trust myself to remember.\nExample chezmoi script:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # ~/.local/share/chezmoi/run_after_ensure-ssh-permissions.sh #!/usr/bin/env bash set -euo pipefail chmod 700 \u0026#34;$HOME/.ssh\u0026#34; if [[ -f \u0026#34;$HOME/.ssh/id_ed25519\u0026#34; ]]; then chmod 600 \u0026#34;$HOME/.ssh/id_ed25519\u0026#34; fi if [[ -f \u0026#34;$HOME/.ssh/id_ed25519.pub\u0026#34; ]]; then chmod 644 \u0026#34;$HOME/.ssh/id_ed25519.pub\u0026#34; fi The key detail: the private key itself never exists in the repository. Only the template reference does.\nRendering API Tokens # API tokens are a better fit for Bitwarden custom fields.\nExample Bitwarden item:\n1 2 3 4 Item name: dotfiles/api/github-personal-token Custom fields: token = ghp_xxxxxxxxxxxxxxxxxxxx A chezmoi-managed shell environment file might look like this:\n1 ~/.local/share/chezmoi/private_dot_config/shell/private_exports.tmpl Template:\n1 2 3 4 # This file is generated by chezmoi. # Do not edit directly. export GITHUB_TOKEN=\u0026#34;{{ (bitwardenFields \u0026#34;item\u0026#34; \u0026#34;dotfiles/api/github-personal-token\u0026#34;).token.value }}\u0026#34; That renders to:\n1 export GITHUB_TOKEN=\u0026#34;ghp_xxxxxxxxxxxxxxxxxxxx\u0026#34; Only on the local machine.\nThe source repo only contains:\n1 {{ (bitwardenFields \u0026#34;item\u0026#34; \u0026#34;dotfiles/api/github-personal-token\u0026#34;).token.value }} That is the pattern I want everywhere.\nAvoiding Secrets in Shell Startup Files # One thing I am still cautious about: exporting secrets globally from .zshrc.\nIt is convenient, but it also means every shell inherits those values. That can leak into child processes, logs, debugging tools, shell history accidents, and application environments.\nFor some tokens, that may be acceptable.\nFor others, I prefer a function that loads the secret only when needed.\nExample:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # ~/.config/zsh/functions/github-token load_github_token() { if [[ -z \u0026#34;${BW_SESSION:-}\u0026#34; ]]; then export BW_SESSION=\u0026#34;$(bw unlock --raw)\u0026#34; fi export GITHUB_TOKEN=\u0026#34;$( bw get item \u0026#34;dotfiles/api/github-personal-token\u0026#34; \\ | jq -r \u0026#39;.fields[] | select(.name == \u0026#34;token\u0026#34;) | .value\u0026#39; )\u0026#34; echo \u0026#34;GitHub token loaded into current shell session.\u0026#34; } That keeps the default shell cleaner.\nThe tradeoff is convenience. For commands I run constantly, I may allow a generated private exports file. For rarely used or higher-risk secrets, I would rather load on demand.\nThat is the kind of decision worth making per secret, not globally.\nWi-Fi Credentials # Wi-Fi credentials are another interesting case.\nOn Linux, NetworkManager can store Wi-Fi profiles under:\n1 /etc/NetworkManager/system-connections/ Those files require root ownership and strict permissions, so I do not want to casually render them as a normal user without thinking through the process.\nA possible template could look like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [connection] id={{ (bitwardenFields \u0026#34;item\u0026#34; \u0026#34;dotfiles/wifi/home-network\u0026#34;).ssid.value }} type=wifi interface-name=wlan0 [wifi] mode=infrastructure ssid={{ (bitwardenFields \u0026#34;item\u0026#34; \u0026#34;dotfiles/wifi/home-network\u0026#34;).ssid.value }} [wifi-security] key-mgmt=wpa-psk psk={{ (bitwardenFields \u0026#34;item\u0026#34; \u0026#34;dotfiles/wifi/home-network\u0026#34;).password.value }} [ipv4] method=auto [ipv6] method=auto But I would not blindly drop that into place as part of my normal user-level dotfiles apply.\nInstead, I would separate this into a machine bootstrap step:\n1 2 3 4 5 sudo install -m 600 -o root -g root \\ ./generated-home-wifi.nmconnection \\ /etc/NetworkManager/system-connections/home-wifi.nmconnection sudo systemctl restart NetworkManager For Wi-Fi, the important question is not “can chezmoi template it?”\nIt can.\nThe better question is “should this be part of my normal dotfiles apply, or part of a privileged machine bootstrap?”\nFor me, Wi-Fi belongs in the bootstrap category because it touches system-level networking.\nSafe Automation Rules # This is the part I care about most.\nThe goal is not just automation.\nThe goal is safe automation.\nMy rules:\nNever commit secrets to the repo. Never print secrets during bootstrap. Never run with set -x around secret commands. Never store BW_SESSION permanently. Use restrictive file permissions for rendered secrets. Keep privileged system secrets separate from normal user dotfiles. Make secret rendering explicit enough that I can audit it later. That means my scripts should look like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #!/usr/bin/env bash set -euo pipefail # Do not use set -x in scripts that touch secrets. if ! command -v bw \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;Bitwarden CLI is required before applying secrets.\u0026#34; exit 1 fi if ! command -v chezmoi \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;chezmoi is required.\u0026#34; exit 1 fi BW_STATUS=\u0026#34;$(bw status | jq -r \u0026#39;.status\u0026#39;)\u0026#34; case \u0026#34;$BW_STATUS\u0026#34; in \u0026#34;unauthenticated\u0026#34;) echo \u0026#34;Logging into Bitwarden...\u0026#34; bw login export BW_SESSION=\u0026#34;$(bw unlock --raw)\u0026#34; ;; \u0026#34;locked\u0026#34;) echo \u0026#34;Unlocking Bitwarden...\u0026#34; export BW_SESSION=\u0026#34;$(bw unlock --raw)\u0026#34; ;; \u0026#34;unlocked\u0026#34;) echo \u0026#34;Bitwarden is already unlocked.\u0026#34; ;; *) echo \u0026#34;Unknown Bitwarden status: $BW_STATUS\u0026#34; exit 1 ;; esac echo \u0026#34;Applying chezmoi configuration...\u0026#34; chezmoi apply echo \u0026#34;Locking Bitwarden CLI session...\u0026#34; bw lock \u0026gt;/dev/null || true unset BW_SESSION echo \u0026#34;Dotfiles applied.\u0026#34; Notice what this does not do.\nIt does not echo secret values.\nIt does not store the session token.\nIt does not run debug tracing.\nIt does not redirect sensitive command output into logs.\nIt keeps the workflow human-approved while still automated.\nPreventing Accidental Secret Commits # Even with this setup, I still want guardrails.\nThe first one is a .gitignore inside the chezmoi source repo.\nExample:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # Never commit generated secret material *.secret *.key *.pem *.p12 *.pfx *.env .env .env.* id_rsa id_ed25519 *.nmconnection # Local scratch files scratch/ tmp/ That is not enough by itself, but it prevents obvious mistakes.\nI also like the idea of using a pre-commit secret scanner.\nFor example:\n1 yay -S gitleaks Then:\n1 gitleaks detect --source ~/.local/share/chezmoi For a repo that is supposed to never contain secrets, a scanner should be boring. If it finds something, I want to know before that commit leaves my machine.\nI would eventually wire that into a pre-commit hook:\n1 2 3 4 #!/usr/bin/env bash set -euo pipefail gitleaks detect --source . --redact Again, the goal is not to rely on one perfect tool.\nThe goal is layers:\nDiscipline Repository structure .gitignore Chezmoi templates Bitwarden source of truth Secret scanning Careful bootstrap scripts What Else Should Live in Bitwarden? # SSH keys and API tokens are obvious.\nWi-Fi credentials are also reasonable.\nBut once I started thinking about my dotfiles this way, I realized there are probably more values that belong in Bitwarden than I originally considered.\nCandidates include:\nGitHub personal access tokens Cloudflare API tokens AWS access keys for personal lab accounts Tailscale auth keys SMTP credentials for local tools Grafana or Datadog API keys Package registry tokens Docker registry credentials Private GPG keys or signing material Recovery codes for developer services License keys for paid software Local-only .env values for side projects The test is simple:\nWould I be uncomfortable if this value appeared in a public GitHub repo?\nIf yes, it belongs in Bitwarden, not dotfiles.\nWhat I Would Not Automate Blindly # There are some things I do not want a dotfiles apply to handle without a deliberate step.\nFor example:\nReplacing system NetworkManager profiles Installing private SSH keys onto a machine I have not verified Pulling every cloud credential automatically Writing production credentials onto a personal workstation Rendering secrets into globally sourced shell files without thinking about scope Automation should reduce mistakes, not remove judgment.\nMy ideal bootstrap asks me to unlock Bitwarden, renders only what the machine needs, sets permissions correctly, and then gets out of the way.\nA Practical Bootstrap Flow # The full new-machine flow should eventually look something like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #!/usr/bin/env bash set -euo pipefail echo \u0026#34;Installing base packages...\u0026#34; yay -S --needed chezmoi bitwarden-cli jq gitleaks echo \u0026#34;Initializing dotfiles...\u0026#34; chezmoi init git@github.com:dcajio/dotfiles.git echo \u0026#34;Authenticating with Bitwarden...\u0026#34; if [[ \u0026#34;$(bw status | jq -r \u0026#39;.status\u0026#39;)\u0026#34; == \u0026#34;unauthenticated\u0026#34; ]]; then bw login fi export BW_SESSION=\u0026#34;$(bw unlock --raw)\u0026#34; echo \u0026#34;Applying dotfiles...\u0026#34; chezmoi apply echo \u0026#34;Validating secret hygiene...\u0026#34; gitleaks detect --source \u0026#34;$(chezmoi source-path)\u0026#34; --redact echo \u0026#34;Cleaning up Bitwarden session...\u0026#34; bw lock \u0026gt;/dev/null || true unset BW_SESSION echo \u0026#34;Bootstrap complete.\u0026#34; This is the direction I want my workstation automation to go.\nOne command should be able to rebuild the machine, but not at the expense of exposing secrets.\nTroubleshooting and Gotchas # Chezmoi Cannot Find bw # If chezmoi fails because it cannot find the Bitwarden CLI, confirm it is installed and on the path:\n1 2 command -v bw bw --version If bw exists in an unusual location, fix the path before running chezmoi apply.\nBitwarden Is Logged In but Locked # Being logged in is not the same as being unlocked.\nCheck status:\n1 bw status | jq If locked:\n1 export BW_SESSION=\u0026#34;$(bw unlock --raw)\u0026#34; Then re-run:\n1 chezmoi apply The Template Works Manually but Fails in Chezmoi # This is usually an environment issue.\nThe current shell may have BW_SESSION, but the process running chezmoi may not.\nCheck:\n1 echo \u0026#34;${BW_SESSION:+BW_SESSION is set}\u0026#34; If needed, run:\n1 2 export BW_SESSION=\u0026#34;$(bw unlock --raw)\u0026#34; chezmoi apply Secrets Show Up in Diffs # Be careful with:\n1 chezmoi diff If templates render secrets, diffs may display generated values depending on what changed.\nFor sensitive files, I avoid casually pasting diffs into tickets, chats, logs, or blog posts. I also avoid running verbose debug output around secret rendering.\nSSH Rejects the Key # SSH is strict about permissions.\nFix them:\n1 2 3 chmod 700 ~/.ssh chmod 600 ~/.ssh/id_ed25519 chmod 644 ~/.ssh/id_ed25519.pub Then test:\n1 ssh -T git@github.com Secret Names Changed in Bitwarden # Templates depend on stable item names or IDs.\nIf I rename Bitwarden items freely, templates can break.\nFor long-term stability, item IDs are more reliable. For readability, names are nicer. I generally prefer readable names while the system is still evolving, then switch critical templates to IDs once the vault structure settles.\nThe Security Boundary I Want # This setup is not trying to make my local machine magically immune to compromise.\nIf my workstation is compromised while Bitwarden is unlocked, there is risk. That is true of any local secret workflow.\nThe boundary I care about here is narrower and practical:\nGit should never contain secrets. A cloned dotfiles repo should be safe to inspect. A new machine should require explicit Bitwarden authentication. Secrets should only render locally. Generated secret files should have correct permissions. Automation should not print sensitive values. Sessions should not be stored permanently. That is a realistic boundary for workstation automation.\nIt is not security theater. It is reducing the most likely mistakes.\nFinal Thoughts # Dotfiles are infrastructure.\nMaybe not production infrastructure. Maybe not customer-facing infrastructure. But they are still the automation layer for the machine I use every day to write code, manage systems, access cloud environments, and do real work.\nThat means they deserve the same discipline I would apply anywhere else:\nSeparate config from secrets. Keep sensitive values out of Git. Make automation repeatable. Make failure modes obvious. Avoid cleverness where boring security works better. For my setup, Bitwarden and chezmoi hit the right balance.\nChezmoi gives me reproducible configuration.\nBitwarden gives me a safe place for secrets.\nThe repo describes what the machine should look like.\nThe vault provides the values that should never be in the repo.\nThat is exactly the split I want.\n","date":"29 June 2026","externalUrl":null,"permalink":"/posts/secrets-management-dotfiles-bitwarden-chezmoi/","section":"Posts","summary":"Learn how I use Bitwarden as the source of truth for dotfiles secrets while chezmoi renders sensitive files locally during workstation automation.","title":"Secrets Management for Dotfiles: Bitwarden, Chezmoi Templates, and Safe Automation","type":"posts"},{"content":"","date":"29 June 2026","externalUrl":null,"permalink":"/tags/security/","section":"Tags","summary":"","title":"Security","type":"tags"},{"content":"","date":"18 June 2026","externalUrl":null,"permalink":"/tags/hyprland/","section":"Tags","summary":"","title":"Hyprland","type":"tags"},{"content":"","date":"18 June 2026","externalUrl":null,"permalink":"/tags/infrastructure-as-code/","section":"Tags","summary":"","title":"Infrastructure as Code","type":"tags"},{"content":"I do not treat my Linux desktop like a toy anymore.\nThat does not mean I do not care how it looks. I absolutely do. I want my desktop to look sharp, modern, dark, readable, and intentional. I want my terminal, status bar, launcher, lock screen, and editor to feel like they belong to the same system.\nBut the real goal is bigger than aesthetics.\nMy Arch + Hyprland setup is built around one idea:\nMy desktop should be reproducible infrastructure.\nIf I wipe my laptop tomorrow, I should not have to spend a weekend trying to remember which packages I installed, which config files I edited, which environment variables fixed screen sharing, or which script made my monitors behave correctly.\nI should be able to redeploy my workstation.\nThat mindset changes everything.\nInstead of a random pile of dotfiles, my desktop becomes something closer to a small platform:\nversion-controlled configuration modular Hyprland files wallpaper-driven theming repeatable package installation machine-specific monitor templates shared keybindings across systems explicit handling for Wayland quirks scripts for the small things I do constantly This is the same way I think about infrastructure at work. If a process matters and I will repeat it more than once, it should eventually become documented, automated, or reproducible.\nMy desktop is no different.\nLeft vertical monitor Right horizontal monitor The Goals Behind the Setup # There are plenty of ways to run Linux.\nI did not choose Arch and Hyprland because it is the easiest path. I chose it because I wanted control without dragging around a full desktop environment that makes assumptions for me.\nThe main goals were:\nFull rebuildability Keeping multiple machines in sync Readable wallpaper-based theming Aesthetic consistency Low bloat Wayland-native tooling A workflow that feels fast all day The rebuildability piece is the most important.\nI use more than one machine. My desktop and laptop need to feel like the same environment. I do not want one machine to have a different launcher, different keybinds, different fonts, missing scripts, or a slightly broken terminal theme.\nThat kind of drift is annoying in cloud infrastructure.\nIt is just as annoying on a workstation.\nThe second big goal was theming. I like changing wallpapers, but I do not want to manually retheme my entire desktop every time I do it. The color system should follow the wallpaper automatically while still keeping text readable.\nThat is where tools like pywal, swww, and matugen fit into the workflow.\nThe wallpaper is not just decoration. It becomes an input into the theme pipeline.\nCurrent Machine Context # The machine shown in this setup is my System76 Gazelle laptop running Arch Linux and Hyprland.\nThe current environment looks roughly like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 OS: Arch Linux Compositor: Hyprland Session: Wayland Shell: Zsh Terminal: Ghostty / Kitty Status bar: Waybar Prompt: Starship Editor: Neovim / Vim Notes: Obsidian Browser: Firefox / Chrome Audio: PipeWire + WirePlumber Locking: Hyprlock Idle: Hypridle Launcher: Rofi / Wofi Screenshots: grim + slurp Clipboard: wl-clipboard The hardware is also important because it influences some of the design decisions.\nThis laptop has hybrid graphics:\n1 2 Integrated GPU: Intel Iris Xe Dedicated GPU: NVIDIA RTX 3050 Mobile That matters because Wayland behavior can vary depending on whether I am on Intel, AMD, or NVIDIA hardware. Some of my machines are smoother than others. Some need different environment variables. Some behave differently with external monitors. Some make screen sharing more painful.\nSo the config cannot assume every machine is identical.\nThat is one of the biggest reasons I separate shared configuration from machine-specific configuration.\nThe Architecture of the Desktop # The biggest improvement I made was thinking about the desktop as a set of layers.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ┌───────────────────────────────────────┐ │ Applications │ │ Firefox, Chrome, Obsidian, Teams │ ├───────────────────────────────────────┤ │ User Experience Layer │ │ Waybar, Rofi, Dunst, Hyprlock │ ├───────────────────────────────────────┤ │ Hyprland Configuration │ │ Keybinds, windows, input, monitors │ ├───────────────────────────────────────┤ │ Theme System │ │ Wallpaper, pywal, matugen, CSS │ ├───────────────────────────────────────┤ │ System Services │ │ PipeWire, WirePlumber, portals │ ├───────────────────────────────────────┤ │ Hardware-Specific Layer │ │ Monitors, GPU quirks, laptop behavior │ └───────────────────────────────────────┘ That structure helps keep the config clean.\nWhen something breaks, I want to know where it belongs.\nIf the issue is screen sharing, I am looking at PipeWire, portals, the browser, and Hyprland.\nIf the issue is colors, I am looking at the theme pipeline.\nIf the issue is an external monitor, I am looking at monitor templates and enforcement scripts.\nIf the issue is a floating window, I am looking at window rules.\nThis is much easier to troubleshoot than one giant hyprland.conf file with everything thrown into it.\nCleaning Up the Hyprland Config Structure # My Hyprland config is modular, but the naming matters.\nIt is very easy for a Linux config directory to slowly turn into a junk drawer:\n1 2 3 4 5 6 UserAnimations.conf WaybarStyles.sh WaybarLayout.sh KillActiveProcess.sh looknfeel.conf windowrules.conf That works, but it does not read cleanly.\nFor a reproducible setup, I want names that are boring, predictable, lowercase, and obvious.\nThe cleaned-up structure I prefer is:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ~/.config/hypr/ ├── hyprland.conf ├── modules/ │ ├── autostart.conf │ ├── environment.conf │ ├── input.conf │ ├── keybinds.conf │ ├── monitors.conf │ ├── theme.conf │ ├── window-rules.conf │ └── animations.conf ├── scripts/ │ ├── apply-wallpaper │ ├── enforce-monitors │ ├── lock-session │ ├── restart-waybar │ ├── screenshot-region │ └── volume-control └── state/ └── current-wallpaper That structure gives every file a clear job.\nThe main hyprland.conf becomes the entrypoint:\n1 2 3 4 5 6 7 8 9 10 11 12 13 # ~/.config/hypr/hyprland.conf # # Main Hyprland entrypoint. # Keep this file boring. Real configuration lives in modules/. source = ~/.config/hypr/modules/environment.conf source = ~/.config/hypr/modules/theme.conf source = ~/.config/hypr/modules/monitors.conf source = ~/.config/hypr/modules/input.conf source = ~/.config/hypr/modules/keybinds.conf source = ~/.config/hypr/modules/window-rules.conf source = ~/.config/hypr/modules/animations.conf source = ~/.config/hypr/modules/autostart.conf That is the whole point.\nThe root config should explain the system at a glance. It should not contain every detail.\nModule 1: Environment # Wayland, Electron apps, browsers, NVIDIA, and portals all care about environment.\nI keep those settings isolated because they are one of the easiest places to create machine-specific problems.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # ~/.config/hypr/modules/environment.conf # # Session-level environment variables. # Keep hardware-specific values templated through chezmoi where possible. env = XDG_CURRENT_DESKTOP,Hyprland env = XDG_SESSION_TYPE,wayland env = XDG_SESSION_DESKTOP,Hyprland # Toolkit behavior env = QT_QPA_PLATFORM,wayland;xcb env = QT_WAYLAND_DISABLE_WINDOWDECORATION,1 env = GDK_BACKEND,wayland,x11 env = SDL_VIDEODRIVER,wayland env = CLUTTER_BACKEND,wayland # Firefox / Mozilla Wayland support env = MOZ_ENABLE_WAYLAND,1 # Electron / Chromium-based apps env = ELECTRON_OZONE_PLATFORM_HINT,auto For NVIDIA-specific machines, I would keep the values separate or generated through a template:\n1 2 3 4 5 6 # NVIDIA-specific values. # Do not apply globally to every machine unless every machine is NVIDIA. env = LIBVA_DRIVER_NAME,nvidia env = __GLX_VENDOR_LIBRARY_NAME,nvidia env = NVD_BACKEND,direct I avoid dumping NVIDIA settings into the shared config unless they are genuinely safe for every machine.\nThat is the broader rule:\nShared config should be boring. Machine-specific config should be explicit.\nModule 2: Input # Input settings deserve their own file because laptops and desktops are different.\nA laptop has a touchpad. A desktop may not. Mouse sensitivity may differ. Natural scrolling may make sense on one machine and not another.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # ~/.config/hypr/modules/input.conf input { kb_layout = us follow_mouse = 1 sensitivity = 0 touchpad { natural_scroll = true tap-to-click = true disable_while_typing = true } } gestures { workspace_swipe = true workspace_swipe_fingers = 3 } I like keeping this simple.\nMost of the time, input problems are felt immediately. If this file is clean, it is easy to adjust after a rebuild.\nModule 3: Keybinds # Keybinds are where consistency matters the most.\nOnce muscle memory develops, the machine should disappear. I should not have to think about whether I am on my desktop or laptop.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 # ~/.config/hypr/modules/keybinds.conf $mod = SUPER $terminal = ghostty $fallbackTerminal = kitty $fileManager = dolphin $browser = firefox $launcher = rofi -show drun # Applications bind = $mod, RETURN, exec, $terminal bind = $mod SHIFT, RETURN, exec, $fallbackTerminal bind = $mod, E, exec, $fileManager bind = $mod, B, exec, $browser bind = $mod, SPACE, exec, $launcher # Window management bind = $mod, Q, killactive bind = $mod, F, fullscreen bind = $mod SHIFT, F, togglefloating bind = $mod, P, pseudo bind = $mod, J, togglesplit # Session controls bind = $mod, L, exec, ~/.config/hypr/scripts/lock-session bind = $mod SHIFT, R, exec, hyprctl reload bind = $mod SHIFT, W, exec, ~/.config/hypr/scripts/restart-waybar # Screenshots bind = , Print, exec, ~/.config/hypr/scripts/screenshot-region # Clipboard history bind = $mod, V, exec, cliphist list | rofi -dmenu | cliphist decode | wl-copy The names matter here too.\nI would rather have:\n1 2 3 restart-waybar screenshot-region lock-session than:\n1 2 3 wbrestart.sh screenshot.sh hyprlock.sh The cleaner names read like commands.\nThat makes the whole setup easier to understand later.\nModule 4: Monitors # Monitors are where Linux desktop configs often get ugly.\nMy laptop setup includes:\n1 2 3 4 5 6 7 8 9 10 11 12 13 Internal display: eDP-1 1920x1080 @ 144Hz scale 1.50 External vertical display: HDMI-A-1 1920x1080 @ 60Hz rotated vertically External main display: HDMI-A-2 3440x1440 @ 60Hz A direct Hyprland config for that machine might look like this:\n1 2 3 4 5 6 7 # ~/.config/hypr/modules/monitors.conf # # Generated or selected per-machine through chezmoi. monitor = eDP-1,1920x1080@144,4520x0,1.5 monitor = HDMI-A-1,1920x1080@60,0x0,1,transform,3 monitor = HDMI-A-2,3440x1440@60,1080x0,1 This works, but it should not be globally shared across every system.\nThe better pattern is to treat monitor layout as machine-specific infrastructure.\nIn the dotfiles repo, I would structure it like this:\n1 2 3 4 5 6 7 8 9 10 11 dot_config/hypr/ ├── hyprland.conf.tmpl ├── modules/ │ ├── input.conf │ ├── keybinds.conf │ ├── theme.conf │ └── window-rules.conf └── monitors/ ├── galactica.conf ├── desktop.conf └── fallback.conf Then hyprland.conf.tmpl can source the right monitor file:\n1 2 3 4 5 6 7 {{- if eq .chezmoi.hostname \u0026#34;galactica\u0026#34; }} source = ~/.config/hypr/monitors/galactica.conf {{- else if eq .chezmoi.hostname \u0026#34;desktop\u0026#34; }} source = ~/.config/hypr/monitors/desktop.conf {{- else }} source = ~/.config/hypr/monitors/fallback.conf {{- end }} This is the difference between dotfiles and workstation infrastructure.\nDotfiles are copied.\nInfrastructure adapts.\nMonitor Enforcement # External monitor behavior is one of those things that can work perfectly for weeks and then randomly come up wrong after a reboot, dock change, driver update, or sleep/wake cycle.\nSo I like having an explicit monitor enforcement script.\n1 2 3 4 5 6 7 8 9 10 #!/usr/bin/env bash # ~/.config/hypr/scripts/enforce-monitors set -euo pipefail hyprctl keyword monitor \u0026#34;eDP-1,1920x1080@144,4520x0,1.5\u0026#34; hyprctl keyword monitor \u0026#34;HDMI-A-1,1920x1080@60,0x0,1,transform,3\u0026#34; hyprctl keyword monitor \u0026#34;HDMI-A-2,3440x1440@60,1080x0,1\u0026#34; notify-send \u0026#34;Hyprland\u0026#34; \u0026#34;Monitor layout enforced\u0026#34; Then bind it:\n1 bind = $mod SHIFT, M, exec, ~/.config/hypr/scripts/enforce-monitors This is not fancy.\nIt is practical.\nAnd practical is what makes a desktop usable every day.\nModule 5: Theme # The theme file is generated from the wallpaper pipeline.\nThe goal is not just matching colors. The goal is readable matching colors.\nA wallpaper can generate a beautiful palette that is terrible for text. I care more about contrast than novelty.\nA clean generated Hyprland theme file might look like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 # ~/.config/hypr/modules/theme.conf # # Generated by wallpaper/theme pipeline. $background = rgb(11111b) $surface = rgb(181825) $overlay = rgb(313244) $foreground = rgb(cdd6f4) $muted = rgb(6c7086) $primary = rgb(89b4fa) $secondary = rgb(cba6f7) $accent = rgb(f9e2af) $success = rgb(a6e3a1) $warning = rgb(fab387) $error = rgb(f38ba8) general { gaps_in = 5 gaps_out = 10 border_size = 2 col.active_border = $primary $accent 45deg col.inactive_border = $overlay } decoration { rounding = 10 blur { enabled = true size = 6 passes = 2 vibrancy = 0.16 } shadow { enabled = true range = 12 render_power = 3 color = rgba(00000066) } } Waybar can consume the same palette through CSS:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /* ~/.config/waybar/colors.css */ /* Generated by wallpaper/theme pipeline. */ @define-color background #11111b; @define-color surface #181825; @define-color overlay #313244; @define-color foreground #cdd6f4; @define-color muted #6c7086; @define-color primary #89b4fa; @define-color secondary #cba6f7; @define-color accent #f9e2af; @define-color success #a6e3a1; @define-color warning #fab387; @define-color error #f38ba8; The important thing is that Hyprland, Waybar, Rofi, Kitty, Ghostty, and other tools should not each invent their own colors.\nThe theme should have a source of truth.\nWallpaper as a Theme Source # Changing wallpapers should update the desktop, not create more manual work.\nA cleaned-up wallpaper script might look like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #!/usr/bin/env bash # ~/.config/hypr/scripts/apply-wallpaper set -euo pipefail wallpaper=\u0026#34;${1:-}\u0026#34; if [[ -z \u0026#34;$wallpaper\u0026#34; ]]; then echo \u0026#34;Usage: apply-wallpaper /path/to/wallpaper\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi if [[ ! -f \u0026#34;$wallpaper\u0026#34; ]]; then echo \u0026#34;Wallpaper does not exist: $wallpaper\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi state_dir=\u0026#34;$HOME/.config/hypr/state\u0026#34; mkdir -p \u0026#34;$state_dir\u0026#34; # Apply wallpaper. swww img \u0026#34;$wallpaper\u0026#34; \\ --transition-type grow \\ --transition-duration 0.8 \\ --transition-fps 60 # Generate color palette. wal -i \u0026#34;$wallpaper\u0026#34; -n # Store current wallpaper reference. ln -sfn \u0026#34;$wallpaper\u0026#34; \u0026#34;$state_dir/current-wallpaper\u0026#34; # Regenerate app-specific theme files. \u0026#34;$HOME/.config/hypr/scripts/generate-theme\u0026#34; # Reload UI pieces that consume the theme. hyprctl reload \u0026#34;$HOME/.config/hypr/scripts/restart-waybar\u0026#34; notify-send \u0026#34;Theme updated\u0026#34; \u0026#34;$(basename \u0026#34;$wallpaper\u0026#34;)\u0026#34; Then generate-theme can be responsible for writing the generated files:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #!/usr/bin/env bash # ~/.config/hypr/scripts/generate-theme set -euo pipefail wal_cache=\u0026#34;$HOME/.cache/wal\u0026#34; hypr_theme=\u0026#34;$HOME/.config/hypr/modules/theme.conf\u0026#34; waybar_colors=\u0026#34;$HOME/.config/waybar/colors.css\u0026#34; # This is intentionally simplified. # In my real setup, I would generate these values from pywal/matugen output. background=\u0026#34;$(sed -n \u0026#39;1p\u0026#39; \u0026#34;$wal_cache/colors\u0026#34; | tr -d \u0026#39;#\u0026#39;)\u0026#34; foreground=\u0026#34;$(sed -n \u0026#39;8p\u0026#39; \u0026#34;$wal_cache/colors\u0026#34; | tr -d \u0026#39;#\u0026#39;)\u0026#34; primary=\u0026#34;$(sed -n \u0026#39;5p\u0026#39; \u0026#34;$wal_cache/colors\u0026#34; | tr -d \u0026#39;#\u0026#39;)\u0026#34; accent=\u0026#34;$(sed -n \u0026#39;4p\u0026#39; \u0026#34;$wal_cache/colors\u0026#34; | tr -d \u0026#39;#\u0026#39;)\u0026#34; cat \u0026gt; \u0026#34;$hypr_theme\u0026#34; \u0026lt;\u0026lt;EOF # Generated file. Do not edit manually. \\$background = rgb($background) \\$foreground = rgb($foreground) \\$primary = rgb($primary) \\$accent = rgb($accent) general { gaps_in = 5 gaps_out = 10 border_size = 2 col.active_border = \\$primary \\$accent 45deg col.inactive_border = rgba(${background}aa) } decoration { rounding = 10 blur { enabled = true size = 6 passes = 2 vibrancy = 0.16 } } EOF cat \u0026gt; \u0026#34;$waybar_colors\u0026#34; \u0026lt;\u0026lt;EOF /* Generated file. Do not edit manually. */ @define-color background #$background; @define-color foreground #$foreground; @define-color primary #$primary; @define-color accent #$accent; EOF I like this pattern because it creates a clean boundary:\nscripts generate the theme Hyprland consumes the theme Waybar consumes the theme the wallpaper remains the input No manual color chasing.\nModule 6: Window Rules # Window rules are what make Hyprland feel like a real daily-driver environment instead of just a compositor.\nSome windows should tile.\nSome should float.\nSome should center.\nSome should pin.\nSome should open on specific workspaces.\nA cleaned-up window-rules.conf might look like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # ~/.config/hypr/modules/window-rules.conf # Authentication prompts windowrulev2 = float,class:^(org.kde.polkit-kde-authentication-agent-1)$ windowrulev2 = center,class:^(org.kde.polkit-kde-authentication-agent-1)$ # Audio controls windowrulev2 = float,class:^(pavucontrol)$ windowrulev2 = size 900 600,class:^(pavucontrol)$ windowrulev2 = center,class:^(pavucontrol)$ # Picture-in-picture windowrulev2 = float,title:^(Picture-in-Picture)$ windowrulev2 = pin,title:^(Picture-in-Picture)$ windowrulev2 = keepaspectratio,title:^(Picture-in-Picture)$ # File picker / dialogs windowrulev2 = float,title:^(Open File)$ windowrulev2 = float,title:^(Save File)$ # Obsidian windowrulev2 = workspace 1,class:^(obsidian)$ # Browser windowrulev2 = workspace 2,class:^(firefox)$ windowrulev2 = workspace 2,class:^(google-chrome)$ # Communication windowrulev2 = workspace 3,class:^(teams-for-linux)$ windowrulev2 = workspace 3,class:^(Mailspring)$ I do not want every app to require manual placement.\nSome windows have obvious homes. The config should know that.\nModule 7: Animations # Animations are easy to overdo.\nI want the desktop to feel polished, not theatrical.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # ~/.config/hypr/modules/animations.conf animations { enabled = true bezier = smoothIn, 0.25, 1, 0.5, 1 bezier = smoothOut, 0.36, 0, 0.66, -0.56 bezier = workspaceCurve, 0.22, 1, 0.36, 1 animation = windows, 1, 4, smoothIn animation = windowsOut, 1, 4, smoothOut animation = border, 1, 8, default animation = fade, 1, 4, default animation = workspaces, 1, 4, workspaceCurve } The goal is simple:\nfast enough to feel responsive smooth enough to feel modern subtle enough to stay out of my way A desktop used for actual work should not feel like it is constantly showing off.\nModule 8: Autostart # Autostart should be boring and explicit.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # ~/.config/hypr/modules/autostart.conf exec-once = waybar exec-once = dunst exec-once = hypridle exec-once = /usr/lib/polkit-kde-authentication-agent-1 # Clipboard history exec-once = wl-paste --type text --watch cliphist store exec-once = wl-paste --type image --watch cliphist store # Wallpaper daemon exec-once = swww-daemon # Network / removable media helpers exec-once = udiskie --tray I do not like mystery startup behavior.\nIf something starts with my session, I want to know where it starts and why.\nWaybar: The System Dashboard # Waybar is not just decoration.\nIt is the dashboard for the desktop.\nI want to see enough information to understand the state of the machine without opening extra apps:\nworkspaces focused window network audio battery date/time tray notifications maybe CPU and memory A clean Waybar structure looks like this:\n1 2 3 4 ~/.config/waybar/ ├── config.jsonc ├── style.css └── colors.css colors.css is generated by the wallpaper/theme pipeline.\nstyle.css defines the actual appearance.\nconfig.jsonc defines the modules.\nExample:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 { \u0026#34;layer\u0026#34;: \u0026#34;top\u0026#34;, \u0026#34;position\u0026#34;: \u0026#34;top\u0026#34;, \u0026#34;height\u0026#34;: 36, \u0026#34;spacing\u0026#34;: 8, \u0026#34;modules-left\u0026#34;: [ \u0026#34;hyprland/workspaces\u0026#34;, \u0026#34;hyprland/window\u0026#34; ], \u0026#34;modules-center\u0026#34;: [ \u0026#34;clock\u0026#34; ], \u0026#34;modules-right\u0026#34;: [ \u0026#34;pulseaudio\u0026#34;, \u0026#34;network\u0026#34;, \u0026#34;battery\u0026#34;, \u0026#34;tray\u0026#34; ], \u0026#34;hyprland/workspaces\u0026#34;: { \u0026#34;format\u0026#34;: \u0026#34;{icon}\u0026#34;, \u0026#34;format-icons\u0026#34;: { \u0026#34;1\u0026#34;: \u0026#34;󰲠\u0026#34;, \u0026#34;2\u0026#34;: \u0026#34;󰲢\u0026#34;, \u0026#34;3\u0026#34;: \u0026#34;󰲤\u0026#34;, \u0026#34;4\u0026#34;: \u0026#34;󰲦\u0026#34;, \u0026#34;default\u0026#34;: \u0026#34;󰧞\u0026#34; } }, \u0026#34;clock\u0026#34;: { \u0026#34;format\u0026#34;: \u0026#34; {:%I:%M  %a, %b %d}\u0026#34;, \u0026#34;tooltip-format\u0026#34;: \u0026#34;{:%Y-%m-%d}\u0026#34; }, \u0026#34;pulseaudio\u0026#34;: { \u0026#34;format\u0026#34;: \u0026#34; {volume}%\u0026#34;, \u0026#34;format-muted\u0026#34;: \u0026#34;󰝟 muted\u0026#34; }, \u0026#34;battery\u0026#34;: { \u0026#34;format\u0026#34;: \u0026#34;{icon} {capacity}%\u0026#34;, \u0026#34;format-icons\u0026#34;: [ \u0026#34;󰂎\u0026#34;, \u0026#34;󰁺\u0026#34;, \u0026#34;󰁼\u0026#34;, \u0026#34;󰁾\u0026#34;, \u0026#34;󰂁\u0026#34;, \u0026#34;󰁹\u0026#34; ] } } And the style:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @import url(\u0026#34;./colors.css\u0026#34;); * { font-family: \u0026#34;Atkinson Hyperlegible Mono Nerd Font\u0026#34;; font-size: 13px; border: none; border-radius: 0; min-height: 0; } window#waybar { background: alpha(@background, 0.88); color: @foreground; } #workspaces button { padding: 0 8px; color: @muted; } #workspaces button.active { color: @accent; border-bottom: 2px solid @primary; } #clock, #battery, #network, #pulseaudio, #tray { padding: 0 10px; background: alpha(@surface, 0.72); border-radius: 10px; } The bar should be useful first and pretty second.\nThe best status bar is the one that gives me context without demanding attention.\nTerminal Workflow: Ghostty, Kitty, Zsh, and Starship # Most of my real work happens in the terminal, so the terminal setup matters.\nI use Zsh with Starship, and I currently have both Ghostty and Kitty available. Ghostty is my primary terminal right now, but I like having Kitty installed as a fallback.\nThe terminal needs to be:\nfast readable consistent with the desktop theme good with Nerd Fonts predictable across machines Starship is useful because it gives me context without a giant prompt.\nFor DevOps work, I care about things like:\ncurrent directory Git branch Git status AWS profile Docker context command duration Example:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # ~/.config/starship.toml format = \u0026#34;\u0026#34;\u0026#34; $username\\ $hostname\\ $directory\\ $git_branch\\ $git_status\\ $aws\\ $docker_context\\ $cmd_duration\\ $line_break\\ $character \u0026#34;\u0026#34;\u0026#34; [directory] truncation_length = 3 truncate_to_repo = true [aws] symbol = \u0026#34; \u0026#34; format = \u0026#39;on [$symbol($profile )(\\($region\\) )]($style)\u0026#39; [docker_context] symbol = \u0026#34; \u0026#34; format = \u0026#39;via [$symbol$context]($style) \u0026#39; [cmd_duration] min_time = 750 format = \u0026#34;took [$duration]($style) \u0026#34; The AWS profile piece is especially important.\nI do not want to guess which AWS account or profile I am pointed at. That is how mistakes happen.\nThe prompt should make important context visible.\nScreen Sharing on Wayland # Screen sharing is one of the biggest practical issues with a Hyprland workstation.\nOn Wayland, applications do not just get unrestricted access to the screen. That is good for security, but it means screen sharing depends on the correct portal and PipeWire stack.\nThe chain looks like this:\n1 2 3 4 5 6 7 8 9 Application ↓ xdg-desktop-portal ↓ xdg-desktop-portal-hyprland ↓ PipeWire ↓ Hyprland The packages that matter are:\n1 2 3 4 5 sudo pacman -S \\ pipewire \\ pipewire-pulse \\ wireplumber \\ xdg-desktop-portal-hyprland When screen sharing breaks, I start here:\n1 2 3 4 systemctl --user status pipewire systemctl --user status wireplumber systemctl --user status xdg-desktop-portal systemctl --user status xdg-desktop-portal-hyprland If the services are running but screen sharing is still acting weird:\n1 2 systemctl --user restart xdg-desktop-portal systemctl --user restart xdg-desktop-portal-hyprland Then I restart the browser or Teams.\nThe annoying part is that Teams, Firefox, Chrome, and Electron apps can behave differently. A setup can work perfectly in one app and fail in another.\nThat is why screen sharing is part of my rebuild checklist.\nI do not consider a workstation rebuild complete until screen sharing works.\nClipboard Issues on Wayland # Clipboard behavior is another area where Wayland can feel different from X11.\nI use wl-clipboard:\n1 sudo pacman -S wl-clipboard Basic test:\n1 2 printf \u0026#34;clipboard test\u0026#34; | wl-copy wl-paste The basic tools usually work fine.\nThe pain tends to show up between browsers, Teams, Electron apps, and native Wayland applications.\nA better setup includes clipboard history:\n1 sudo pacman -S cliphist Then autostart:\n1 2 exec-once = wl-paste --type text --watch cliphist store exec-once = wl-paste --type image --watch cliphist store And bind a picker:\n1 bind = $mod, V, exec, cliphist list | rofi -dmenu | cliphist decode | wl-copy That gives me a way to inspect and recover clipboard history instead of treating the clipboard like invisible magic.\nFor a work machine, that matters.\nScreenshots with grim and slurp # Screenshots are part of my daily workflow.\nI use them for documentation, bug reports, notes, blog posts, and quick visual references.\nThe Wayland-native stack is simple:\n1 sudo pacman -S grim slurp A cleaned-up screenshot script:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #!/usr/bin/env bash # ~/.config/hypr/scripts/screenshot-region set -euo pipefail screenshot_dir=\u0026#34;$HOME/Pictures/Screenshots\u0026#34; mkdir -p \u0026#34;$screenshot_dir\u0026#34; file=\u0026#34;$screenshot_dir/screenshot-$(date +%Y-%m-%d-%H%M%S).png\u0026#34; geometry=\u0026#34;$(slurp)\u0026#34; grim -g \u0026#34;$geometry\u0026#34; \u0026#34;$file\u0026#34; wl-copy \u0026lt; \u0026#34;$file\u0026#34; notify-send \u0026#34;Screenshot captured\u0026#34; \u0026#34;$file\u0026#34; Keybind:\n1 bind = , Print, exec, ~/.config/hypr/scripts/screenshot-region Simple, reliable, and easy to remember.\nLocking and Idle Management # I use Hyprlock and Hypridle for locking and idle behavior.\nThe goal is:\nlock the session after inactivity turn off displays after a longer idle period restore displays cleanly avoid weird resume behavior Example:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # ~/.config/hypr/hypridle.conf general { lock_cmd = hyprlock before_sleep_cmd = loginctl lock-session after_sleep_cmd = hyprctl dispatch dpms on } listener { timeout = 300 on-timeout = hyprlock } listener { timeout = 600 on-timeout = hyprctl dispatch dpms off on-resume = hyprctl dispatch dpms on } The lock script stays boring:\n1 2 3 4 5 6 #!/usr/bin/env bash # ~/.config/hypr/scripts/lock-session set -euo pipefail hyprlock I do not want my lock behavior buried in a giant config file. It is important enough to be obvious.\nHandling Node Version Managers and GUI Apps # One of the more annoying issues I have run into is environment mismatch between terminal apps and GUI apps.\nIn a terminal, my shell loads Zsh config, initializes tools like fnm or mise, and everything works.\nBut GUI apps launched from Hyprland do not always inherit the same environment as an interactive shell.\nThat can create weird issues with Node-based workflows, editor integrations, or tools that expect a specific runtime path.\nMy shell config might include:\n1 2 3 4 # ~/.zshrc eval \u0026#34;$(fnm env --use-on-cd)\u0026#34; eval \u0026#34;$(mise activate zsh)\u0026#34; That works for interactive shells.\nBut for session-level environment, I prefer to make important values explicit through Hyprland environment config or systemd user environment imports.\nExample:\n1 2 3 4 # ~/.config/hypr/modules/environment.conf exec-once = dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP exec-once = systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP PATH This is not the most glamorous part of the setup, but it is important.\nA desktop environment is not just windows and wallpapers. It is also the environment your tools inherit.\nPackage Bootstrapping # A reproducible desktop needs an explicit package list.\nFor the core desktop, I would start with:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 sudo pacman -S \\ hyprland \\ hypridle \\ hyprlock \\ waybar \\ ghostty \\ kitty \\ zsh \\ starship \\ rofi \\ wofi \\ dunst \\ swaync \\ python-pywal16 \\ grim \\ slurp \\ wl-clipboard \\ pipewire \\ pipewire-pulse \\ wireplumber \\ pavucontrol \\ xdg-desktop-portal-hyprland \\ dolphin \\ firefox \\ neovim \\ obsidian \\ polkit-kde-agent \\ udiskie \\ qt5-wayland \\ qt6-wayland For AUR packages:\n1 2 3 4 5 6 yay -S \\ google-chrome \\ teams-for-linux-bin \\ mailspring-bin \\ morgen-bin \\ aws-session-manager-plugin I do not think everyone should copy my exact package list.\nThe more important point is that the package list exists.\nA rebuild should not depend on memory.\nWhere Chezmoi Fits # Chezmoi is what turns the whole setup into something I can safely sync across machines.\nThe dotfiles repo should separate shared files, generated files, templates, and machine-specific logic.\nA clean repo structure might look like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 chezmoi-repo/ ├── dot_config/ │ ├── hypr/ │ │ ├── hyprland.conf.tmpl │ │ ├── modules/ │ │ └── monitors/ │ ├── waybar/ │ ├── ghostty/ │ ├── kitty/ │ ├── rofi/ │ └── starship.toml ├── private_dot_ssh/ ├── run_once_install-packages.sh.tmpl └── scripts/ The monitor selection can be templated:\n1 2 3 4 5 6 7 {{- if eq .chezmoi.hostname \u0026#34;galactica\u0026#34; }} source = ~/.config/hypr/monitors/galactica.conf {{- else if eq .chezmoi.hostname \u0026#34;desktop\u0026#34; }} source = ~/.config/hypr/monitors/desktop.conf {{- else }} source = ~/.config/hypr/monitors/fallback.conf {{- end }} Package installation can also be templated:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/usr/bin/env bash set -euo pipefail {{- if eq .chezmoi.os \u0026#34;linux\u0026#34; }} sudo pacman -S --needed \\ hyprland \\ waybar \\ ghostty \\ zsh \\ starship \\ rofi \\ grim \\ slurp \\ wl-clipboard \\ pipewire \\ wireplumber {{- end }} The rule is simple:\nshared config goes in Git secrets stay private machine-specific values are templated generated files are either ignored or clearly marked rebuild steps are scripted That is how the desktop stays manageable.\nPerformance and UX Tuning # A pretty desktop is useless if it feels slow.\nMy priorities are:\nlow input latency fast launcher readable text reliable screen sharing predictable monitor behavior consistent keybinds no unnecessary background noise I like blur, animations, rounded corners, Nerd Fonts, and polished Waybar modules.\nBut I do not want the desktop to feel like it is performing a magic trick every time I open a terminal.\nThe best Hyprland setup is one that feels quiet.\nIt should look good, respond quickly, and stay out of the way.\nGotchas I Had to Work Around # This is the part that matters more than the screenshots.\nScreen Sharing Is Still the Big One # Wayland screen sharing depends on portals and PipeWire.\nIf Teams or a browser cannot share the screen, check the portal stack before wasting time randomly changing Hyprland settings.\n1 2 systemctl --user status pipewire wireplumber systemctl --user status xdg-desktop-portal xdg-desktop-portal-hyprland Restarting portals often fixes weird behavior:\n1 2 systemctl --user restart xdg-desktop-portal systemctl --user restart xdg-desktop-portal-hyprland NVIDIA Needs Its Own Lane # NVIDIA can work well, but it still needs special care.\nI avoid mixing NVIDIA-specific assumptions into the shared config. If a setting only applies to NVIDIA, it belongs in a machine-specific file or template.\nThis matters even more when syncing dotfiles between machines with different GPUs.\nClipboard Behavior Can Be Weird # Clipboard issues show up most often between Teams, Firefox, Chrome, and Electron apps.\nThe basic wl-copy and wl-paste tools are essential, but adding cliphist makes the clipboard easier to inspect and recover from.\nGUI Apps Do Not Always Inherit Your Shell # If Node, npm, pnpm, fnm, or mise work in the terminal but not in a GUI app, the issue may be environment inheritance.\nDo not assume .zshrc fixes everything.\nSession-level environment matters.\nExternal Monitors Need a Recovery Path # Docking, undocking, sleep, wake, and GPU behavior can all affect monitors.\nHaving an enforce-monitors script gives me a fast recovery path instead of manually fighting display layout every time something comes up wrong.\nWhat I Still Want to Improve # The setup is reproducible, but it is not finished.\nLinux desktops are never really finished.\nThe next things I want to improve are:\ncleaner machine-specific monitor templates better clipboard history integration stronger first-run bootstrap scripts more deterministic wallpaper theme generation better documentation for each helper script fewer overlapping tools where I currently have duplicates a rebuild checklist that validates screen sharing, audio, monitors, and clipboard behavior The goal is not perfection.\nThe goal is to make the setup easier to rebuild, easier to understand, and easier to trust.\nFinal Thoughts # Arch and Hyprland are not the easiest way to run a Linux desktop.\nThat is fine.\nI am not optimizing for easiest. I am optimizing for control, speed, reproducibility, and a desktop that feels like mine.\nThe difference between a fragile rice and a real workstation setup is discipline.\nA fragile rice looks good in screenshots.\nA real workstation survives rebuilds.\nThat is what I want from my desktop. I want the wallpaper, colors, terminal, status bar, keybinds, monitors, scripts, and applications to work together as one reproducible system.\nIf I wipe the machine, I should not feel like I lost my setup.\nI should feel like I am redeploying it.\nMy Linux desktop is not just a collection of dotfiles.\nIt is infrastructure.\n","date":"18 June 2026","externalUrl":null,"permalink":"/posts/arch-hyprland-reproducible-desktop-environment/","section":"Posts","summary":"How I built a reproducible Arch Linux and Hyprland desktop environment with modular config files, wallpaper-driven theming, Waybar, Ghostty, multi-monitor support, and machine-specific dotfile templating.","title":"Inside My Arch + Hyprland Setup: Building a Fully Reproducible Desktop Environment","type":"posts"},{"content":"","date":"18 June 2026","externalUrl":null,"permalink":"/tags/pacman/","section":"Tags","summary":"","title":"Pacman","type":"tags"},{"content":"","date":"18 June 2026","externalUrl":null,"permalink":"/tags/systemd/","section":"Tags","summary":"","title":"Systemd","type":"tags"},{"content":" Turning My Linux Desktop into Infrastructure as Code # At some point, dotfiles stop being enough.\nManaging my Hyprland config, Waybar setup, kitty theme, shell aliases, wallpaper-driven colors, and application preferences with chezmoi gets me a long way. It means my environment feels familiar across machines. It means my config is versioned. It means I can roll back bad ideas instead of trying to remember what file I edited at 1 AM.\nBut dotfiles only solve one layer of the problem.\nThey do not install the packages those configs depend on. They do not enable the services my desktop expects to be running. They do not decide whether this machine needs NVIDIA-specific packages, laptop power management, Bluetooth, Docker, development tooling, screenshot utilities, clipboard helpers, or the right AUR packages.\nThat is where the rebuild story starts to get messy.\nMy goal for this part of the series is simple:\nI want to be able to take a fresh Arch machine and turn it into my working Linux desktop in roughly 15 minutes.\nNot because I reinstall constantly. Not because shaving a few minutes off setup time is life-changing. The real value is confidence.\nIf my laptop dies, I can rebuild. If my desktop gets weird, I can start clean. If I buy a new machine, I am not spending an entire weekend remembering every package, service, and tweak I rely on.\nThis post is about expanding my dotfiles into something closer to Infrastructure as Code for my workstation.\nThe Problem: Dotfiles Are Not the Whole System # In the previous parts of this series, I focused heavily on reproducibility:\nHyprland config structure modular config files chezmoi for dotfile management wallpaper-driven theming keeping multiple machines in sync avoiding a bloated desktop setup That solved the “how do I keep my config clean?” problem.\nBut a Linux desktop is more than config files.\nMy Hyprland setup depends on a collection of packages and services. Some are obvious. Some are easy to forget until they are missing.\nFor example, a Wayland desktop might need tools for:\ndisplay management authentication agents screenshots screen recording clipboard history notifications portals PipeWire audio Bluetooth fonts themes terminal tools development tools Docker browsers editor tooling AUR packages hardware-specific drivers The problem is not installing these one time. The problem is remembering the full shape of the system six months later.\nThat is where I want my setup to become more intentional.\nI do not want my desktop to be a pile of manually installed packages and undocumented assumptions. I want it to look more like the infrastructure I manage professionally:\ndeclarative where practical version-controlled repeatable split by concern safe to run more than once aware of machine-specific differences In other words, I want my workstation to be treated like infrastructure.\nThe Goal: A New Machine in 15 Minutes # The target state is not a fully automated bare-metal Arch installer.\nAt least not yet.\nFor now, I am assuming a base Arch install already exists with:\nnetworking working a user account created sudo configured Git installed internet access available disk partitioning already handled the system booted and usable from a TTY or minimal environment That is a practical starting point.\nFrom there, I want to clone my dotfiles repository, run one bootstrap command, and let the machine converge into my preferred environment.\nSomething like this:\n1 2 3 git clone git@github.com:dcajio/dotfiles.git ~/.local/share/chezmoi cd ~/.local/share/chezmoi ./bootstrap.sh The bootstrap process should handle:\nInstalling required pacman packages Installing yay if it is not already present Installing AUR packages Applying my chezmoi dotfiles Enabling system services Enabling user services Handling machine-specific differences Leaving me with a usable Hyprland workstation That is the vision.\nThe important part is that this script should be safe to rerun. A rebuild script that only works once is not automation. It is a fragile install recipe.\nRepository Structure # Before writing the script, I want a clean structure.\nI already use chezmoi, so the dotfiles repository is the obvious home for this. But I do not want one giant script filled with every package, service, and conditional branch.\nI want the repo to be readable.\nA structure like this makes sense:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ~/.local/share/chezmoi/ ├── bootstrap.sh ├── packages/ │ ├── pacman-core.txt │ ├── pacman-desktop.txt │ ├── pacman-dev.txt │ ├── pacman-hyprland.txt │ ├── pacman-laptop.txt │ ├── aur-core.txt │ ├── aur-desktop.txt │ └── aur-dev.txt ├── services/ │ ├── system.txt │ └── user.txt ├── scripts/ │ ├── install-yay.sh │ ├── install-pacman-packages.sh │ ├── install-aur-packages.sh │ ├── enable-system-services.sh │ ├── enable-user-services.sh │ └── detect-machine.sh └── home/ └── dot_config/ └── ... This keeps the system split into logical pieces:\npackage lists live in packages/ service lists live in services/ reusable scripts live in scripts/ chezmoi still owns the actual home directory config This is the same principle I use when organizing infrastructure or application code: separate intent from implementation.\nThe package files describe what I want installed. The scripts describe how to install them. The dotfiles describe how I want the environment configured.\nPackage Bootstrapping with pacman # The first layer is official Arch packages.\nI want these package lists to be boring text files. One package per line. Comments allowed. Blank lines ignored.\nFor example:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # packages/pacman-core.txt base-devel git curl wget unzip rsync jq fzf ripgrep fd bat eza zoxide man-db man-pages Then a desktop-specific list:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # packages/pacman-desktop.txt hyprland waybar kitty wofi dunst swww grim slurp wl-clipboard xdg-desktop-portal xdg-desktop-portal-hyprland pipewire pipewire-pulse wireplumber pavucontrol networkmanager blueman brightnessctl playerctl polkit-gnome noto-fonts noto-fonts-emoji ttf-jetbrains-mono-nerd And a development list:\n1 2 3 4 5 6 7 8 9 10 11 12 13 # packages/pacman-dev.txt docker docker-compose nodejs npm python python-pip go rust neovim github-cli openssh The install script can read all of these files and pass the packages into pacman.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #!/usr/bin/env bash set -euo pipefail ROOT_DIR=\u0026#34;$(cd \u0026#34;$(dirname \u0026#34;${BASH_SOURCE[0]}\u0026#34;)/..\u0026#34; \u0026amp;\u0026amp; pwd)\u0026#34; install_pacman_file() { local file=\u0026#34;$1\u0026#34; if [[ ! -f \u0026#34;$file\u0026#34; ]]; then echo \u0026#34;Package file not found: $file\u0026#34; return 0 fi mapfile -t packages \u0026lt; \u0026lt;( grep -vE \u0026#39;^\\s*#\u0026#39; \u0026#34;$file\u0026#34; | grep -vE \u0026#39;^\\s*$\u0026#39; ) if [[ \u0026#34;${#packages[@]}\u0026#34; -eq 0 ]]; then echo \u0026#34;No packages found in $file\u0026#34; return 0 fi echo \u0026#34;Installing pacman packages from $file\u0026#34; sudo pacman -S --needed --noconfirm \u0026#34;${packages[@]}\u0026#34; } install_pacman_file \u0026#34;$ROOT_DIR/packages/pacman-core.txt\u0026#34; install_pacman_file \u0026#34;$ROOT_DIR/packages/pacman-desktop.txt\u0026#34; install_pacman_file \u0026#34;$ROOT_DIR/packages/pacman-dev.txt\u0026#34; The key flag here is:\n1 --needed That makes the script rerunnable. If a package is already installed, pacman does not reinstall it unnecessarily.\nThis is one of the most important habits in workstation automation: make every step idempotent where possible.\nAUR Bootstrapping with yay # I use yay for AUR packages.\nThat means the bootstrap process needs to account for two cases:\nyay is already installed yay is missing and needs to be built I do not want to assume the machine already has everything.\nHere is a simple install-yay.sh:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/usr/bin/env bash set -euo pipefail if command -v yay \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;yay is already installed\u0026#34; exit 0 fi echo \u0026#34;Installing yay\u0026#34; sudo pacman -S --needed --noconfirm git base-devel tmpdir=\u0026#34;$(mktemp -d)\u0026#34; trap \u0026#39;rm -rf \u0026#34;$tmpdir\u0026#34;\u0026#39; EXIT git clone https://aur.archlinux.org/yay.git \u0026#34;$tmpdir/yay\u0026#34; cd \u0026#34;$tmpdir/yay\u0026#34; makepkg -si --noconfirm Then I can manage AUR packages the same way as official packages:\n1 2 3 4 # packages/aur-core.txt visual-studio-code-bin google-chrome Hyprland and desktop niceties may also end up here depending on what I choose to install from the AUR:\n1 2 3 4 # packages/aur-desktop.txt wlogout hyprpicker And the AUR install script:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #!/usr/bin/env bash set -euo pipefail ROOT_DIR=\u0026#34;$(cd \u0026#34;$(dirname \u0026#34;${BASH_SOURCE[0]}\u0026#34;)/..\u0026#34; \u0026amp;\u0026amp; pwd)\u0026#34; install_aur_file() { local file=\u0026#34;$1\u0026#34; if [[ ! -f \u0026#34;$file\u0026#34; ]]; then echo \u0026#34;AUR package file not found: $file\u0026#34; return 0 fi mapfile -t packages \u0026lt; \u0026lt;( grep -vE \u0026#39;^\\s*#\u0026#39; \u0026#34;$file\u0026#34; | grep -vE \u0026#39;^\\s*$\u0026#39; ) if [[ \u0026#34;${#packages[@]}\u0026#34; -eq 0 ]]; then echo \u0026#34;No AUR packages found in $file\u0026#34; return 0 fi echo \u0026#34;Installing AUR packages from $file\u0026#34; yay -S --needed --noconfirm \u0026#34;${packages[@]}\u0026#34; } install_aur_file \u0026#34;$ROOT_DIR/packages/aur-core.txt\u0026#34; install_aur_file \u0026#34;$ROOT_DIR/packages/aur-desktop.txt\u0026#34; install_aur_file \u0026#34;$ROOT_DIR/packages/aur-dev.txt\u0026#34; This gives me a clean split:\npacman for official repo packages yay for AUR packages text files for package intent scripts for installation behavior It is simple, but that is the point.\nThe Main Bootstrap Script # The top-level bootstrap.sh should be boring and readable.\nI want to be able to open it a year from now and immediately understand the provisioning flow.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #!/usr/bin/env bash set -euo pipefail ROOT_DIR=\u0026#34;$(cd \u0026#34;$(dirname \u0026#34;${BASH_SOURCE[0]}\u0026#34;)\u0026#34; \u0026amp;\u0026amp; pwd)\u0026#34; echo \u0026#34;Starting workstation bootstrap\u0026#34; echo \u0026#34;Updating system packages\u0026#34; sudo pacman -Syu --noconfirm echo \u0026#34;Installing pacman packages\u0026#34; \u0026#34;$ROOT_DIR/scripts/install-pacman-packages.sh\u0026#34; echo \u0026#34;Ensuring yay is installed\u0026#34; \u0026#34;$ROOT_DIR/scripts/install-yay.sh\u0026#34; echo \u0026#34;Installing AUR packages\u0026#34; \u0026#34;$ROOT_DIR/scripts/install-aur-packages.sh\u0026#34; echo \u0026#34;Applying chezmoi dotfiles\u0026#34; chezmoi apply echo \u0026#34;Enabling system services\u0026#34; \u0026#34;$ROOT_DIR/scripts/enable-system-services.sh\u0026#34; echo \u0026#34;Enabling user services\u0026#34; \u0026#34;$ROOT_DIR/scripts/enable-user-services.sh\u0026#34; echo \u0026#34;Bootstrap complete\u0026#34; echo \u0026#34;Reboot recommended\u0026#34; This is intentionally plain.\nThe orchestration script should not contain every detail. It should describe the order of operations.\nThe scripts underneath it can handle the details.\nSystem Services # Packages are only part of the workstation. The next layer is services.\nOn a typical Arch desktop, there are system-level services I expect to be available.\nExamples:\n1 2 3 4 5 # services/system.txt NetworkManager.service bluetooth.service docker.service Then the enable script:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #!/usr/bin/env bash set -euo pipefail ROOT_DIR=\u0026#34;$(cd \u0026#34;$(dirname \u0026#34;${BASH_SOURCE[0]}\u0026#34;)/..\u0026#34; \u0026amp;\u0026amp; pwd)\u0026#34; SERVICE_FILE=\u0026#34;$ROOT_DIR/services/system.txt\u0026#34; if [[ ! -f \u0026#34;$SERVICE_FILE\u0026#34; ]]; then echo \u0026#34;No system service file found\u0026#34; exit 0 fi while read -r service; do [[ -z \u0026#34;$service\u0026#34; || \u0026#34;$service\u0026#34; =~ ^# ]] \u0026amp;\u0026amp; continue echo \u0026#34;Enabling system service: $service\u0026#34; sudo systemctl enable --now \u0026#34;$service\u0026#34; done \u0026lt; \u0026#34;$SERVICE_FILE\u0026#34; This gives me a simple place to add or remove system services without editing shell logic.\nIf I decide Docker should not run on a certain machine, I can handle that later through host-specific package and service lists.\nUser Services # User services are just as important on a Wayland desktop.\nThis could include things like:\nuser-level sync tools notification helpers theme watchers wallpaper daemons custom scripts systemd --user timers anything that should run as my user instead of root A user service list may look like this:\n1 2 3 4 5 # services/user.txt pipewire.service pipewire-pulse.service wireplumber.service And the script:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/usr/bin/env bash set -euo pipefail ROOT_DIR=\u0026#34;$(cd \u0026#34;$(dirname \u0026#34;${BASH_SOURCE[0]}\u0026#34;)/..\u0026#34; \u0026amp;\u0026amp; pwd)\u0026#34; SERVICE_FILE=\u0026#34;$ROOT_DIR/services/user.txt\u0026#34; if [[ ! -f \u0026#34;$SERVICE_FILE\u0026#34; ]]; then echo \u0026#34;No user service file found\u0026#34; exit 0 fi systemctl --user daemon-reload while read -r service; do [[ -z \u0026#34;$service\u0026#34; || \u0026#34;$service\u0026#34; =~ ^# ]] \u0026amp;\u0026amp; continue echo \u0026#34;Enabling user service: $service\u0026#34; systemctl --user enable --now \u0026#34;$service\u0026#34; done \u0026lt; \u0026#34;$SERVICE_FILE\u0026#34; One gotcha here is that user services require a working user session. Depending on where the bootstrap script is run from, some user-level services may not behave exactly the same from a TTY as they do inside a graphical session.\nThat is why this process needs to be practical, not overly magical. I am fine with the script getting the machine 95% of the way there and telling me when a reboot or login cycle is required.\nApplying chezmoi # Once the required packages exist, chezmoi can safely apply my dotfiles.\nThis order matters.\nIf my dotfiles reference binaries that are not installed yet, I do not want the first run to be noisy or broken. So the flow should be:\ninstall packages install AUR packages apply dotfiles enable services reboot or log out/in The chezmoi step itself is simple:\n1 chezmoi apply But this is where the workstation starts to feel like mine again.\nThis brings back:\nHyprland config Waybar config kitty config shell config Git config theme files scripts application preferences machine templates The dotfiles are still the core of the setup. The bootstrap script just makes sure the machine is ready for them.\nHandling Machine-Specific Differences # This is where things get interesting.\nMy desktop and laptop are not identical. Different machines may need different packages, services, and config values.\nThe obvious examples are:\nNVIDIA vs AMD graphics desktop monitor layout vs laptop display work machine vs personal machine laptop battery tools Bluetooth requirements hostnames input devices docking station behavior machine-specific Hyprland monitor rules secrets and SSH keys work-only packages This is exactly where chezmoi templates start to make sense.\nI do not want to maintain completely separate dotfile repos for every machine. That defeats the purpose. I want one repo with conditional behavior.\nA simple first step is to have chezmoi prompt for machine type during initialization.\nFor example, a .chezmoi.toml.tmpl:\n1 2 3 [data] machine_type = \u0026#34;{{ promptStringOnce . \u0026#34;machine_type\u0026#34; \u0026#34;Machine type? desktop/laptop/work\u0026#34; }}\u0026#34; gpu = \u0026#34;{{ promptStringOnce . \u0026#34;gpu\u0026#34; \u0026#34;GPU type? amd/nvidia/intel\u0026#34; }}\u0026#34; Then inside a Hyprland template, I can branch:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 {{- if eq .gpu \u0026#34;nvidia\u0026#34; }} # NVIDIA-specific environment variables env = LIBVA_DRIVER_NAME,nvidia env = __GLX_VENDOR_LIBRARY_NAME,nvidia env = NVD_BACKEND,direct {{- end }} {{- if eq .machine_type \u0026#34;laptop\u0026#34; }} # Laptop display monitor = eDP-1,preferred,auto,1 {{- else }} # Desktop monitors monitor = DP-1,2560x1440@144,0x0,1 monitor = DP-2,2560x1440@144,2560x0,1 {{- end }} That is the kind of difference I want chezmoi to own.\nFor package and service differences, I can add machine-specific package lists.\n1 2 3 4 5 6 7 packages/ ├── pacman-core.txt ├── pacman-desktop.txt ├── pacman-dev.txt ├── pacman-laptop.txt ├── pacman-nvidia.txt └── pacman-work.txt Then the bootstrap script can decide what to install based on detected or configured machine data.\nA simple version could read the hostname:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 hostname=\u0026#34;$(hostnamectl --static)\u0026#34; case \u0026#34;$hostname\u0026#34; in daves-desktop) MACHINE_TYPE=\u0026#34;desktop\u0026#34; GPU_TYPE=\u0026#34;amd\u0026#34; ;; daves-laptop) MACHINE_TYPE=\u0026#34;laptop\u0026#34; GPU_TYPE=\u0026#34;nvidia\u0026#34; ;; *) MACHINE_TYPE=\u0026#34;generic\u0026#34; GPU_TYPE=\u0026#34;unknown\u0026#34; ;; esac echo \u0026#34;Machine type: $MACHINE_TYPE\u0026#34; echo \u0026#34;GPU type: $GPU_TYPE\u0026#34; That is not perfect, but it is easy to understand.\nLong-term, I may prefer chezmoi data for this instead of duplicating machine metadata in Bash. The important part is keeping the machine-specific logic explicit. I do not want hidden assumptions.\nExample Machine Detection Script # Here is a starting point:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #!/usr/bin/env bash set -euo pipefail hostname=\u0026#34;$(hostnamectl --static)\u0026#34; gpu_type=\u0026#34;unknown\u0026#34; if lspci | grep -qi nvidia; then gpu_type=\u0026#34;nvidia\u0026#34; elif lspci | grep -qi amd; then gpu_type=\u0026#34;amd\u0026#34; elif lspci | grep -qi intel; then gpu_type=\u0026#34;intel\u0026#34; fi machine_type=\u0026#34;generic\u0026#34; if [[ -d /sys/class/power_supply/BAT0 || -d /sys/class/power_supply/BAT1 ]]; then machine_type=\u0026#34;laptop\u0026#34; else machine_type=\u0026#34;desktop\u0026#34; fi cat \u0026lt;\u0026lt;EOF HOSTNAME=$hostname GPU_TYPE=$gpu_type MACHINE_TYPE=$machine_type EOF That script could be sourced by the bootstrap process:\n1 2 3 4 5 eval \u0026#34;$(\u0026#34;$ROOT_DIR/scripts/detect-machine.sh\u0026#34;)\u0026#34; echo \u0026#34;Detected hostname: $HOSTNAME\u0026#34; echo \u0026#34;Detected GPU: $GPU_TYPE\u0026#34; echo \u0026#34;Detected machine type: $MACHINE_TYPE\u0026#34; Then I can conditionally install packages:\n1 2 3 4 5 6 7 if [[ \u0026#34;$MACHINE_TYPE\u0026#34; == \u0026#34;laptop\u0026#34; ]]; then install_pacman_file \u0026#34;$ROOT_DIR/packages/pacman-laptop.txt\u0026#34; fi if [[ \u0026#34;$GPU_TYPE\u0026#34; == \u0026#34;nvidia\u0026#34; ]]; then install_pacman_file \u0026#34;$ROOT_DIR/packages/pacman-nvidia.txt\u0026#34; fi A laptop package file may include:\n1 2 3 4 5 # packages/pacman-laptop.txt tlp upower acpi An NVIDIA file may include:\n1 2 3 4 5 # packages/pacman-nvidia.txt nvidia nvidia-utils nvidia-settings I would rather start simple and improve this over time than try to build the perfect abstraction on day one.\nSecrets Do Not Belong in Dotfiles # One thing I am not trying to do is store secrets directly in this repo.\nSSH keys, API tokens, work credentials, private environment files, and anything sensitive need a separate strategy.\nFor now, the bootstrap process can create the expected directories and leave reminders.\n1 2 3 4 mkdir -p \u0026#34;$HOME/.ssh\u0026#34; chmod 700 \u0026#34;$HOME/.ssh\u0026#34; echo \u0026#34;Remember to restore SSH keys separately.\u0026#34; That is not fully automated, but it is safe.\nEventually, this could integrate with a proper secret manager. But I would rather have one manual step than accidentally commit something sensitive into a dotfiles repo.\nThis is one of those places where infrastructure discipline matters. Reproducibility is good. Accidentally syncing secrets everywhere is not.\nMaking It Safe to Rerun # This is probably the most important part of the whole setup.\nA good bootstrap script should not be a one-time install bomb. It should be safe to run again after I edit a package list, add a new service, or change part of the environment.\nThat means:\nuse pacman -S --needed use yay -S --needed avoid destructive commands do not overwrite secrets do not blindly delete files keep package lists additive make service enablement repeatable let chezmoi handle dotfile diffs print what the script is doing The script should be boring.\nBoring automation is good automation.\nThe First Full Bootstrap Draft # Putting the pieces together, here is what an early version of the main script could look like:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #!/usr/bin/env bash set -euo pipefail ROOT_DIR=\u0026#34;$(cd \u0026#34;$(dirname \u0026#34;${BASH_SOURCE[0]}\u0026#34;)\u0026#34; \u0026amp;\u0026amp; pwd)\u0026#34; log() { echo echo \u0026#34;==\u0026gt; $1\u0026#34; } log \u0026#34;Starting workstation bootstrap\u0026#34; log \u0026#34;Detecting machine\u0026#34; eval \u0026#34;$(\u0026#34;$ROOT_DIR/scripts/detect-machine.sh\u0026#34;)\u0026#34; echo \u0026#34;Hostname: $HOSTNAME\u0026#34; echo \u0026#34;Machine type: $MACHINE_TYPE\u0026#34; echo \u0026#34;GPU type: $GPU_TYPE\u0026#34; log \u0026#34;Updating system\u0026#34; sudo pacman -Syu --noconfirm log \u0026#34;Installing base pacman packages\u0026#34; \u0026#34;$ROOT_DIR/scripts/install-pacman-packages.sh\u0026#34; if [[ \u0026#34;$MACHINE_TYPE\u0026#34; == \u0026#34;laptop\u0026#34; \u0026amp;\u0026amp; -f \u0026#34;$ROOT_DIR/packages/pacman-laptop.txt\u0026#34; ]]; then log \u0026#34;Installing laptop-specific packages\u0026#34; \u0026#34;$ROOT_DIR/scripts/install-pacman-file.sh\u0026#34; \u0026#34;$ROOT_DIR/packages/pacman-laptop.txt\u0026#34; fi if [[ \u0026#34;$GPU_TYPE\u0026#34; == \u0026#34;nvidia\u0026#34; \u0026amp;\u0026amp; -f \u0026#34;$ROOT_DIR/packages/pacman-nvidia.txt\u0026#34; ]]; then log \u0026#34;Installing NVIDIA-specific packages\u0026#34; \u0026#34;$ROOT_DIR/scripts/install-pacman-file.sh\u0026#34; \u0026#34;$ROOT_DIR/packages/pacman-nvidia.txt\u0026#34; fi log \u0026#34;Ensuring yay is installed\u0026#34; \u0026#34;$ROOT_DIR/scripts/install-yay.sh\u0026#34; log \u0026#34;Installing AUR packages\u0026#34; \u0026#34;$ROOT_DIR/scripts/install-aur-packages.sh\u0026#34; log \u0026#34;Applying chezmoi dotfiles\u0026#34; chezmoi apply log \u0026#34;Enabling system services\u0026#34; \u0026#34;$ROOT_DIR/scripts/enable-system-services.sh\u0026#34; log \u0026#34;Enabling user services\u0026#34; \u0026#34;$ROOT_DIR/scripts/enable-user-services.sh\u0026#34; log \u0026#34;Creating expected local directories\u0026#34; mkdir -p \u0026#34;$HOME/.ssh\u0026#34; chmod 700 \u0026#34;$HOME/.ssh\u0026#34; log \u0026#34;Bootstrap complete\u0026#34; echo \u0026#34;A reboot or logout/login is recommended before judging the final desktop state.\u0026#34; This is not the final form forever. That is fine.\nThe point is to establish the pattern:\ndefine packages in files define services in files keep scripts small detect machine differences apply dotfiles after dependencies exist make the system rebuildable Gotchas and Edge Cases # This kind of setup sounds clean, but there are always rough edges.\nAUR Packages Can Break # AUR packages are not the same as official repo packages. Builds can fail. Maintainers can change things. Dependencies can shift.\nThat is why I do not want the entire system to depend on a fragile chain of AUR packages. Anything critical should come from the official repos when possible.\nUser Services May Need a Real Session # Some systemd --user services behave differently depending on whether I run the script from a TTY, SSH session, or inside the actual graphical environment.\nFor first bootstrapping, I am okay with enabling what I can and rebooting.\nNVIDIA Is Always Its Own Chapter # NVIDIA on Wayland can still be weird depending on driver versions, kernel, display manager, and Hyprland behavior.\nI do not want NVIDIA-specific environment variables sprinkled randomly through my config. They should be isolated behind templates or dedicated config sections.\nMonitor Names Are Not Universal # Hyprland monitor configs can break when display names change.\nA desktop with multiple monitors needs different rules than a laptop. A docked laptop may need another set entirely.\nThis is a perfect use case for chezmoi templates or imported host-specific Hyprland config files.\nSome Things Should Stay Manual # Not everything needs to be automated immediately.\nExamples:\nrestoring SSH private keys logging into browsers authenticating password managers enrolling work accounts pairing Bluetooth devices setting up proprietary applications The goal is not zero manual steps. The goal is eliminating the dumb, repetitive, error-prone setup work.\nFinal Thoughts # This is the point where my Linux desktop starts becoming more than a custom setup.\nIt becomes a system.\nThe dotfiles are still important, but they are only one part of the rebuild story. The real win is being able to describe the entire workstation in code:\nwhat packages it needs what services should run what configs should apply what differs between machines what should stay manual what can be safely repeated That is the same mindset I use when working with cloud infrastructure.\nA server should not be a mystery. A deployment should not depend on tribal knowledge. A workstation should not either.\nMy end goal is a Linux desktop that I can rebuild quickly, understand completely, and carry across machines without dragging years of accidental cruft along with it.\nThat is what “new machine in 15 minutes” really means.\nNot perfection.\nConfidence.\n","date":"18 June 2026","externalUrl":null,"permalink":"/posts/turning-linux-desktop-into-infrastructure-as-code/","section":"Posts","summary":"Turning My Linux Desktop into Infrastructure as Code # At some point, dotfiles stop being enough.\nManaging my Hyprland config, Waybar setup, kitty theme, shell aliases, wallpaper-driven colors, and application preferences with chezmoi gets me a long way. It means my environment feels familiar across machines. It means my config is versioned. It means I can roll back bad ideas instead of trying to remember what file I edited at 1 AM.\n","title":"Turning My Linux Desktop into Infrastructure as Code","type":"posts"},{"content":"","date":"18 June 2026","externalUrl":null,"permalink":"/tags/waybar/","section":"Tags","summary":"","title":"Waybar","type":"tags"},{"content":"","date":"18 June 2026","externalUrl":null,"permalink":"/tags/wayland/","section":"Tags","summary":"","title":"Wayland","type":"tags"},{"content":"I’ve landed on a fairly opinionated conclusion over the years: dotfiles management only works when it disappears into your workflow. The moment I have to “think about syncing configs,” I stop maintaining it.\nThat’s why I eventually standardized on chezmoi backed by a plain Git repository, with a fully automated apply model across my Arch + Hyprland environment.\nThis isn’t a “nice dotfiles setup.” It’s a repeatable machine bootstrap system.\nWhy I Moved Away From Traditional Dotfile Management # Before chezmoi, I went through the usual phases:\nBare Git repo in $HOME GNU Stow symlink farms Manual rsync scripts between machines Half-baked Ansible attempts They all fail for the same reason:\nThey assume your dotfiles are static configuration, not environment-specific infrastructure.\nIn reality, my setup is very dynamic:\nArch Linux desktop with Hyprland + Wayland-specific configs Frequent UI/WM tweaks (Hyprland binds, animations, monitors) Secrets that must never leak but still need templating (AWS creds, tokens, SSH config fragments) Rapid iteration between “experiment” and “stable” configs What I needed was:\nA single source of truth Machine-aware configuration Secret handling built-in Zero manual syncing That combination is where chezmoi actually fits properly.\nThe Core Architecture I Use # At a high level:\n1 2 3 ~/.local/share/chezmoi → Git repo (source of truth) ~/.config/chezmoi → runtime config ~/.zshrc, ~/.config/* → generated outputs But the important part isn’t structure—it’s flow:\nMy workflow model # I edit files inside the chezmoi source directory I commit to Git Any machine pulls automatically chezmoi applies changes idempotently Templates resolve per-machine differences There is no “sync step.” There is only “state convergence.”\nInitial Setup (How I Bootstrap a Machine) # On a fresh Arch install:\n1 2 3 sh -c \u0026#34;$(curl -fsLS get.chezmoi.io)\u0026#34; chezmoi init git@github.com:david/dotfiles.git chezmoi apply -v From that point on, the machine is “owned” by the dotfile system.\nI don’t manually configure:\nshell Hyprland git SSH tooling aliases environment variables Everything is declarative.\nFile Layout Strategy # I structure my repo like this:\n1 2 3 4 5 6 7 8 9 10 11 dotfiles/ dot_config/ hypr/ waybar/ kitty/ dot_zshrc dot_gitconfig private_dot_ssh/ private_dot_aws/ run_once_after_bootstrap.sh.tmpl .chezmoi.toml Key idea:\nIf it exists on disk, it exists in chezmoi.\nEven “messy” configs like Hyprland benefit from this. I don’t treat them as sacred—I treat them as reproducible artifacts.\nThe Real Power: Machine-Aware Templates # This is where chezmoi becomes more than a dotfile manager.\nI heavily use templating for environment differences.\nExample: .zshrc.tmpl\n1 2 3 4 5 6 7 8 9 10 11 12 # Common aliases alias ll=\u0026#34;eza -lah\u0026#34; alias gs=\u0026#34;git status\u0026#34; {{ if eq .chezmoi.hostname \u0026#34;arch-main\u0026#34; }} export MONITOR_LAYOUT=\u0026#34;ultrawide\u0026#34; export EDITOR=\u0026#34;nvim\u0026#34; {{ end }} {{ if eq .chezmoi.os \u0026#34;linux\u0026#34; }} export PATH=\u0026#34;$HOME/.local/bin:$PATH\u0026#34; {{ end }} This allows me to treat each machine like a role, not a special snowflake.\nSecrets Handling (The Part Most People Get Wrong) # I do not store plaintext secrets in Git.\nInstead:\nprivate_* files are encrypted templates inject them at render time secrets never exist in final repo state Example:\n1 private_dot_aws/credentials.tmpl 1 2 3 [default] aws_access_key_id = {{ (bitwarden \u0026#34;aws_access_key\u0026#34;) }} aws_secret_access_key = {{ (bitwarden \u0026#34;aws_secret_secret\u0026#34;) }} This is a bad example as we are all using aws login right? right? RIGHT!?\nI prefer template-driven injection rather than static encrypted blobs because:\nrotation is easier I don’t need to “re-encrypt repo” secrets stay external to state management You can swap the backend (GPG/age/Vault/Bitwarden CLI). The key idea is:\nchezmoi manages shape, not secret lifecycle.\nFully Automated Mode (My Real Setup) # This is where most dotfile systems stop—but mine doesn’t.\nI run chezmoi in an automated reconciliation loop.\nSystemd user timer (Arch) # 1 2 3 4 5 6 7 8 9 10 # ~/.config/systemd/user/chezmoi.timer [Unit] Description=Run chezmoi apply periodically [Timer] OnBootSec=2min OnUnitActiveSec=10min [Install] WantedBy=timers.target Service:\n1 2 3 4 5 6 [Unit] Description=Apply chezmoi dotfiles [Service] Type=oneshot ExecStart=/usr/bin/chezmoi apply -v Enable:\n1 systemctl --user enable --now chezmoi.timer Why this matters # Instead of:\nremembering to sync configs manually pulling changes drift accumulating silently I get:\ncontinuous reconciliation immediate config propagation zero friction iteration loop Hyprland-Specific Wins # Hyprland configs change a lot—bindings, animations, monitor layouts.\nExample:\n1 2 3 4 5 6 7 8 # monitors.conf.tmpl {{ if eq .chezmoi.hostname \u0026#34;arch-main\u0026#34; }} monitor=DP-1,3440x1440@165,0x0,1 monitor=HDMI-A-1,1920x1080@60,3440x0,1 {{ else }} monitor=DP-1,preferred,auto,1 {{ end }} This alone eliminates 90% of my “why is my layout broken on this machine?” problems.\nGit Workflow Model # My repo is intentionally boring:\nmain = production state feature branches = experiments no untracked local divergence allowed Workflow:\n1 2 3 chezmoi edit ~/.config/hypr/hyprland.conf git commit -am \u0026#34;Tweak animations\u0026#34; git push Then every machine converges automatically.\nGotchas and Edge Cases # 1. Bootstrapping chicken-and-egg problem # SSH keys + Git access must exist before init.\nSolution:\ninitial bootstrap uses HTTPS SSH keys injected after first apply 2. Drift during experimentation # If I manually tweak configs outside chezmoi, automation overwrites them.\nThat’s intentional—but dangerous if you forget.\nMitigation:\n1 chezmoi diff Becomes part of my debugging loop.\n3. Template complexity creep # It’s easy to over-template everything.\nRule I follow:\nIf a template has more than 2 conditions, it’s probably wrong.\n4. Hyprland reload behavior # Some changes require manual reload:\n1 hyprctl reload I don’t automate this because it can interrupt sessions unexpectedly.\nWhy This Works So Well for My Setup # The real reason chezmoi works in my environment:\nI run a single main workstation (Arch + Hyprland) I frequently rebuild or test configs I care about reproducibility more than portability I want zero manual “sync thinking” This turns dotfiles into:\na declarative system configuration layer for my desktop environment\nNot a backup system.\nNot a sync tool.\nA state engine.\nIf I Were Starting Today # I would do exactly this again:\nInitialize chezmoi early Treat $HOME as managed infrastructure Automate apply via systemd timer immediately Use templates aggressively—but not excessively Keep secrets external and injected ","date":"17 June 2026","externalUrl":null,"permalink":"/posts/chezmoi-dotfiles-arch-linux-hyprland-automation/","section":"Posts","summary":"I’ve landed on a fairly opinionated conclusion over the years: dotfiles management only works when it disappears into your workflow. The moment I have to “think about syncing configs,” I stop maintaining it.\n","title":"Managing Dotfiles with ChezMoi: A Fully Automated Infrastructure-as-Code Desktop for Arch Linux and Hyprland","type":"posts"},{"content":"","date":"12 June 2026","externalUrl":null,"permalink":"/tags/artificial-intelligence/","section":"Tags","summary":"","title":"Artificial Intelligence","type":"tags"},{"content":"","date":"12 June 2026","externalUrl":null,"permalink":"/tags/aws-certification/","section":"Tags","summary":"","title":"AWS Certification","type":"tags"},{"content":"When I started seriously preparing for AWS certifications, I hit a familiar problem for anyone working in infrastructure: the volume of information isn’t the hard part — retention and structure are.\nYou can watch hours of training, build labs, and take pages of notes, but unless that information is continuously refined, it decays quickly. A few weeks later, the details blur: IAM evaluation logic, VPC routing behavior, the subtle differences between endpoint types — all of it starts to fade.\nWhat I needed wasn’t more notes.\nI needed a system that continuously refines knowledge as it is captured.\nThat’s what led me to combine Obsidian with an AI-assisted workflow using the Copilot plugin — turning my vault into a structured, evolving second brain.\nThe Core Idea: Notes Are Not Storage — They Are a Processing Pipeline # Most people treat notes as static storage.\nMine are not.\nMy Obsidian vault is a knowledge pipeline:\nCapture raw information while learning Immediately refine and structure it using AI (inside Obsidian) Store it in a structured knowledge system Reinforce it using active recall (flashcards) The key shift is this:\nNotes are not the output of learning. They are the intermediate state of understanding.\nMy Obsidian Vault Structure # I keep my structure intentionally simple. Complexity kills consistency.\nInbox (Raw Capture Layer) # This is where everything starts.\nWhile watching AWS courses or working through labs, I dump raw bullet-point notes here. They are intentionally unstructured:\nIAM explicit deny overrides allow SCP applies at account level Gateway endpoints free Interface endpoints cost money Route tables auto-create local route These notes are messy by design. The goal is speed, not clarity.\nLab Notebooks (Processed Knowledge Layer) # This is where raw notes get transformed.\nUsing the Copilot plugin inside Obsidian, I refine and restructure notes directly in-place — without ever copying or pasting content elsewhere.\nThis is important: I’m not exporting notes to an AI tool.\nI’m editing them in context inside the vault.\nThe result is structured, explainable knowledge:\nClear sections Expanded explanations Corrected inaccuracies AWS-specific behavior clarified Exam-relevant details surfaced Reference (Evergreen Knowledge Layer) # This is my long-term knowledge base.\nIt contains canonical concepts that get reused across notes:\nIAM VPC fundamentals Route tables Security groups Endpoint types S3 behavior Each reference note is designed to be linked everywhere else in the vault.\nOver time, this becomes a personal technical encyclopedia.\nArchitecture (Real-World Systems Layer) # This is where theory meets implementation.\nI document:\nAWS architectures I’ve built or studied Infrastructure patterns Deployment flows Network topology decisions Tradeoffs and design reasoning This layer forces me to move beyond memorization into systems thinking.\nWhere AI Actually Fits in the Workflow # The Copilot plugin inside Obsidian changes the workflow in a critical way:\nI don’t leave my notes to use AI.\nInstead, AI operates inside the note itself.\nThat means I can:\nHighlight a section of messy notes and ask it to restructure them Ask it to expand a concept in-place Fix unclear explanations without losing context Identify missing AWS concepts relevant to the current note Improve clarity without breaking the structure of the vault This creates a tight feedback loop between thinking and refinement.\nA raw section like:\nexplicit deny wins SCP overrides IAM not sure why Becomes:\nIAM Evaluation Precedence # In AWS IAM policy evaluation, an explicit Deny always overrides any Allow, regardless of where the permission is defined.\nThis applies across:\nIdentity-based policies (IAM users/roles) Resource-based policies (S3, SQS, etc.) AWS Organizations SCPs Even if an SCP or IAM policy allows an action, an explicit deny at any level will block it.\nThis is critical for troubleshooting access issues in real environments, where overlapping policies can create unexpected denials.\nAI as a Knowledge Expander, Not a Knowledge Generator # One of the most valuable uses of AI in this workflow is identifying missing context.\nMy raw notes often capture facts without explanation.\nFor example:\nGateway endpoint free Interface endpoint costs money These are correct, but incomplete.\nInside Obsidian, I can ask Copilot to expand this context, and it will surface:\nArchitecture differences (Gateway vs Interface endpoints) Cost models (per-hour vs no charge) Service coverage limitations Routing behavior differences When each should be used in real architectures The important distinction is that AI is not replacing learning — it is exposing gaps in understanding.\nFlashcards: Converting Notes into Active Recall # At the end of each study session, I generate flashcards directly from my refined notes using Copilot.\nThis step is critical because it converts passive understanding into retrieval practice.\nI use multiple formats:\nFill-in-the-Blank # Gateway Endpoints provide private access to ______ and DynamoDB.\nSingle Choice # Which service evaluates SCPs?\nA. IAM B. AWS Organizations C. CloudTrail D. Route 53\nMulti-Select # Which statements about Interface Endpoints are true? (Select all that apply)\nBecause the flashcards are generated from already-refined notes, they remain tightly aligned with what I actually studied that day.\nThis closes the loop:\nLearn → Capture → Refine → Connect → Test\nThe Hard Part: AI Isn’t Trusted, It’s Verified # There is a critical constraint in this system:\nAI is not authoritative.\nTwo issues show up consistently:\n1. Hallucinated Detail Expansion # When asked to expand concepts, AI can introduce details that were never in the original material.\nThis is especially dangerous in certification study contexts, where precision matters.\nTo mitigate this, I treat AI output as:\nStructured drafts, not truth sources\nEverything is validated against AWS documentation or lab behavior.\n2. Partial or Incomplete Refactoring # When working with larger notes, AI sometimes fails to fully rewrite or restructure content cleanly.\nThis can lead to:\nMissing bullet points Skipped sections Inconsistent structure Because of that, I always compare the original and refined versions before committing changes into my Lab Notebooks.\nWhy This System Works # The real value isn’t Obsidian.\nIt isn’t AI.\nIt isn’t flashcards.\nIt’s the combination of all three into a continuous refinement loop.\nMost learning systems stop at capture.\nThis system continues through:\nStructural refinement (AI-assisted editing inside context) Knowledge linking (Obsidian graph structure) Active recall (flashcards) Over time, this builds something more valuable than notes:\nA searchable, interconnected model of how you think about systems.\nFinal Thought # AWS certifications are not difficult because of complexity alone.\nThey’re difficult because of volume and interdependency.\nA system like this doesn’t reduce the complexity.\nIt organizes it into something that can be repeatedly processed, reinforced, and retrieved.\nYears from now, I won’t remember every IAM edge case or networking nuance I studied.\nBut I won’t need to.\nMy second brain will already have done that work.\n","date":"12 June 2026","externalUrl":null,"permalink":"/posts/aws-second-brain-obsidian-ai-workflow/","section":"Posts","summary":"When I started seriously preparing for AWS certifications, I hit a familiar problem for anyone working in infrastructure: the volume of information isn’t the hard part — retention and structure are.\n","title":"Building a Second Brain for AWS Certification with Obsidian and AI","type":"posts"},{"content":"","date":"12 June 2026","externalUrl":null,"permalink":"/tags/learning-systems/","section":"Tags","summary":"","title":"Learning Systems","type":"tags"},{"content":"","date":"12 June 2026","externalUrl":null,"permalink":"/tags/obsidian/","section":"Tags","summary":"","title":"Obsidian","type":"tags"},{"content":"","date":"12 June 2026","externalUrl":null,"permalink":"/categories/personal-knowledge-management/","section":"Categories","summary":"","title":"Personal Knowledge Management","type":"categories"},{"content":"","date":"5 June 2026","externalUrl":null,"permalink":"/tags/engineer-lifestyle/","section":"Tags","summary":"","title":"Engineer Lifestyle","type":"tags"},{"content":"","date":"5 June 2026","externalUrl":null,"permalink":"/categories/fitness/","section":"Categories","summary":"","title":"Fitness","type":"categories"},{"content":"","date":"5 June 2026","externalUrl":null,"permalink":"/tags/habit-design/","section":"Tags","summary":"","title":"Habit Design","type":"tags"},{"content":"","date":"5 June 2026","externalUrl":null,"permalink":"/tags/morning-routine/","section":"Tags","summary":"","title":"Morning Routine","type":"tags"},{"content":"","date":"5 June 2026","externalUrl":null,"permalink":"/tags/productivity/","section":"Tags","summary":"","title":"Productivity","type":"tags"},{"content":"","date":"5 June 2026","externalUrl":null,"permalink":"/tags/strength-training/","section":"Tags","summary":"","title":"Strength Training","type":"tags"},{"content":"","date":"5 June 2026","externalUrl":null,"permalink":"/tags/systems-thinking/","section":"Tags","summary":"","title":"Systems Thinking","type":"tags"},{"content":"For years, I trained whenever I could fit it into the day.\nSometimes that meant lunch. Sometimes after work. Sometimes not at all.\nThe problem wasn\u0026rsquo;t motivation. The problem was that life is unpredictable.\nAs a Senior DevOps Engineer and Lead Developer, my day can go sideways at any moment. A production issue, emergency deployment, infrastructure outage, urgent code review, or an executive meeting can instantly destroy any plans I had for the afternoon.\nEventually I realized something:\nThe later I scheduled my workout, the more opportunities existed for it to be cancelled.\nThat\u0026rsquo;s what led me to a 5 AM training schedule.\nNot because it\u0026rsquo;s trendy.\nNot because some productivity guru told me to wake up before sunrise.\nBecause it was the only time of day that truly belonged to me.\nMy Current Schedule # My weekdays look something like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 5:00 AM Wake Up 5:05 AM Wash face, brush teeth, get dressed 5:30 AM Leave for gym 5:45 AM - 7:00 AM Weight Training 7:15 AM Home 7:15 AM - 7:30 AM Overnight oats for protein and carbs 7:30 AM - 8:30 AM Review emails Review pull requests Handle urgent issues 8:30 AM - 9:30 AM Continuing education Work Day Lunch Break Cardio 5:00 PM - 6:00 PM Additional continuing education 6:00 PM - 6:15 PM Active review of what I learned 6:15 PM Shower and prepare for dinner Evening Dinner Relaxation Quality time Bed Most people notice two things immediately:\nI train before work. I don\u0026rsquo;t shower immediately afterward. The second one usually gets more questions than the first.\nThe reason is simple.\nMy mornings are about stacking productive activities together while preserving as much time as possible for learning.\nBy delaying my shower until the evening, I avoid an extra transition period and can move directly into work and study mode.\nMy Training Split # I\u0026rsquo;m currently running a:\n1 2 3 4 5 Push Pull Lower Upper Lower Or:\n1 PPLUL This gives me:\nHigh training frequency Two lower-body sessions per week Twice-weekly stimulation for most muscle groups Enough flexibility to adjust volume when recovery becomes an issue As I\u0026rsquo;ve gotten older, I\u0026rsquo;ve found that consistency beats perfection.\nI no longer care about finding the mathematically optimal split.\nI care about finding the split I\u0026rsquo;ll still be running six months from now.\nPPLUL checks that box.\nWhy 5 AM Works So Well # The Gym Is Empty # This alone is almost enough reason.\nAt 5:45 AM:\nNo waiting for equipment No crowds No social distractions No groups occupying half the gym I can walk in with a plan and execute it.\nA workout that might take 90 minutes after work often takes 60–75 minutes in the morning.\nThe Day Can\u0026rsquo;t Take It Away # One of my favorite benefits is psychological.\nBy 7 AM I\u0026rsquo;ve already:\nWoken up Driven to the gym Completed a workout Returned home Started nutrition Before most people have had their first cup of coffee.\nIf the rest of the day turns into chaos, that\u0026rsquo;s fine.\nThe workout is already done.\nNo amount of meetings can take it away.\nNo production outage can cancel it.\nNo emergency deployment can derail it.\nThe biggest task of the day is already complete.\nIt Creates More Time for Learning # This was one of the unexpected benefits.\nMany people assume waking up earlier means less energy.\nWhat I found was that it created structure.\nInstead of trying to figure out when I was going to study, I built education directly into my day.\nEvery day includes:\nMorning Learning # One hour dedicated to professional growth.\nRecently that\u0026rsquo;s included:\nAWS certifications Architecture design Infrastructure automation Cloud security Platform engineering concepts Evening Learning # Another focused hour.\nActive Recall # Fifteen minutes reviewing what I learned.\nThis final piece is probably the most important.\nReading isn\u0026rsquo;t learning.\nWatching videos isn\u0026rsquo;t learning.\nRetention comes from recall.\nThe daily review dramatically increases how much information sticks.\nThe Hardest Part: Fasted Training # The biggest challenge wasn\u0026rsquo;t waking up.\nIt was training without food.\nSome mornings feel fantastic.\nOthers don\u0026rsquo;t.\nEspecially during:\nHeavy squat sessions Deadlift days Poor sleep Recovery deficits There are days where I can feel that glycogen levels simply aren\u0026rsquo;t where I\u0026rsquo;d like them to be.\nFor anyone considering morning training, understand that performance may initially suffer.\nThat\u0026rsquo;s normal.\nYour body needs time to adapt.\nThe Other Challenge: Sleep # Everyone wants to wake up at 5 AM.\nVery few people want to go to bed at 9 PM.\nThose two things are inseparable.\nThe first few weeks required significant adjustments:\nEarlier dinners Less late-night screen time More consistency on weekends Better sleep hygiene The workout schedule only works because the sleep schedule supports it.\nYou cannot continuously trade sleep for productivity.\nEventually the bill comes due.\nWhat I\u0026rsquo;ve Learned # The biggest lesson wasn\u0026rsquo;t fitness related.\nIt was operational.\nAs engineers, we\u0026rsquo;re constantly thinking about reliability.\nWe build redundant systems.\nWe automate repetitive tasks.\nWe remove single points of failure.\nEventually I realized my fitness routine needed the same treatment.\nAfternoon workouts depended on too many external factors.\nMeetings.\nDeployments.\nIncidents.\nUnexpected work.\nMorning training removed those dependencies.\nThe result wasn\u0026rsquo;t a better workout.\nIt was a more reliable system.\nAnd just like in infrastructure engineering, reliability beats theoretical peak performance every single time.\nA perfect training plan that gets skipped twice a week is worse than a good training plan executed consistently for years.\nThe 5 AM schedule isn\u0026rsquo;t magic.\nIt\u0026rsquo;s simply the most reliable way I\u0026rsquo;ve found to ensure that no matter what happens during the day, I\u0026rsquo;ve already invested in my health, my education, and my long-term goals before most people have even started work.\n","date":"5 June 2026","externalUrl":null,"permalink":"/posts/5-am-training-split-devops-fitness-system/","section":"Posts","summary":"For years, I trained whenever I could fit it into the day.\nSometimes that meant lunch. Sometimes after work. Sometimes not at all.\nThe problem wasn’t motivation. The problem was that life is unpredictable.\n","title":"The 5 AM Training Split: Building a Reliable Fitness System for Engineers","type":"posts"},{"content":"","date":"1 June 2026","externalUrl":null,"permalink":"/tags/amazon-ecs/","section":"Tags","summary":"","title":"Amazon ECS","type":"tags"},{"content":"","date":"1 June 2026","externalUrl":null,"permalink":"/tags/amazon-rds/","section":"Tags","summary":"","title":"Amazon RDS","type":"tags"},{"content":"","date":"1 June 2026","externalUrl":null,"permalink":"/tags/cloudflare/","section":"Tags","summary":"","title":"Cloudflare","type":"tags"},{"content":"","date":"1 June 2026","externalUrl":null,"permalink":"/tags/multi-region/","section":"Tags","summary":"","title":"Multi-Region","type":"tags"},{"content":"When most people talk about multi-region architectures, they immediately jump to diagrams full of globally distributed databases, active-active replication, service meshes, and enough complexity to require a dedicated platform team.\nThat\u0026rsquo;s not the reality for most e-commerce companies.\nMy responsibility is managing the infrastructure that powers our online storefronts while also leading development efforts, reviewing code, managing deployments, and supporting multiple business initiatives. Every architectural decision has to balance reliability, performance, operational complexity, and cost.\nAs we expanded our presence beyond the United States and continued investing in our Canadian storefront, I started evaluating what a practical multi-region architecture should actually look like for an e-commerce platform.\nThe goal wasn\u0026rsquo;t to build the most impressive AWS diagram possible.\nThe goal was to improve:\nAvailability Fault tolerance Time-to-first-byte (TTFB) Peak traffic handling Geographic proximity for customers While keeping the system maintainable by a relatively small engineering team.\nThis article walks through the architectural decisions, tradeoffs, and implementation approach I recommend for most mid-sized e-commerce organizations.\nThe Problem With Traditional Single-Region E-Commerce # Most AWS-hosted Magento environments follow a familiar pattern:\n1 2 3 4 5 6 7 8 9 Internet │ Cloudflare │ Application Load Balancer │ Magento Application Servers │ RDS Database This works well until you begin encountering issues such as:\nRegional Failures # Even highly available AWS services occasionally experience disruptions.\nA full regional outage is rare, but partial service failures happen more often than many teams realize.\nIf everything lives in a single AWS region, your blast radius becomes:\n1 Region Failure = Store Offline Not ideal when every minute of downtime directly impacts revenue.\nInternational Customers # As we expanded our Canadian presence, latency became a bigger concern.\nEven if Cloudflare is caching most static content, dynamic requests still need to travel back to the origin infrastructure.\nThis becomes especially noticeable for:\nProduct searches Cart operations Checkout workflows Customer account pages Personalized content Reducing geographic distance between customers and compute resources can have a measurable impact on perceived performance.\nTraffic Spikes # E-commerce traffic is rarely consistent.\nTraffic surges can come from:\nMarketing campaigns New product launches Seasonal promotions Email campaigns Viral social media exposure A single region can scale significantly, but distributing traffic across regions creates additional headroom while reducing localized bottlenecks.\nWhy We Didn\u0026rsquo;t Pursue Active-Active Everything # Whenever multi-region discussions begin, someone inevitably proposes:\nLet\u0026rsquo;s make everything active-active.\nIn theory, this sounds fantastic.\nIn practice, it introduces significant challenges:\nDatabase conflict resolution Inventory synchronization Order consistency Session replication Increased operational complexity More difficult troubleshooting For Magento specifically, active-active databases can quickly become a nightmare.\nInventory counts, orders, customer accounts, and payment transactions all require strong consistency.\nThe complexity often outweighs the benefits.\nFor most e-commerce companies, active-active is solving a problem they don\u0026rsquo;t actually have.\nThe Architecture I Prefer # Instead of active-active databases, I favor a hybrid approach:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Cloudflare │ ┌──────────┴──────────┐ │ │ US Region CA Region │ │ ALB ALB │ │ Application Tier Application Tier │ │ └──────────┬──────────┘ │ Primary RDS │ Cross-Region Read Replica This provides:\nMulti-region compute Regional failover capability Improved customer proximity Simpler data consistency model Lower operational burden Without introducing distributed database complexity.\nFrontend Architecture # One of the biggest improvements we made was separating frontend concerns from Magento.\nOur storefront utilizes Next.js rather than relying solely on Magento rendering.\nThis provides several advantages:\nFaster page loads Better caching opportunities Reduced Magento workload Improved scalability The frontend runs in ECS Fargate behind an Application Load Balancer.\n1 2 3 4 5 6 7 Cloudflare │ Regional ALB │ ECS Fargate │ Custom APIs Fargate allows horizontal scaling without worrying about underlying server management.\nDuring traffic spikes:\n1 2 Tasks → 10 Tasks → 20 Tasks AWS handles provisioning automatically.\nFor stateless frontend services, this is an excellent fit.\nBackend Architecture # Magento presents a different challenge.\nUnlike the frontend, Magento often requires:\nCustom extensions Local filesystem dependencies Third-party integrations Legacy application assumptions For this reason, I maintain Magento on EC2 using pre-baked AMIs.\n1 2 3 4 5 ALB │ Auto Scaling Group │ Magento EC2 Instances Every AMI contains:\nApplication code Required packages PHP configuration Monitoring agents Deployment dependencies This approach provides:\nFaster scaling events Consistent deployments Reduced configuration drift Instead of provisioning servers during scale-out events, new instances launch already configured.\nBreaking Magento Dependencies # One of the most important architectural decisions wasn\u0026rsquo;t infrastructure related at all.\nIt was reducing dependence on Magento itself.\nHistorically, many storefront requests flowed directly through Magento GraphQL and REST endpoints.\nAs traffic grows, this becomes problematic.\nMagento is extremely capable, but it\u0026rsquo;s not always the most efficient system for every request.\nWe began extracting high-volume functionality into dedicated services:\n1 2 3 4 5 6 Next.js │ ├── Product API ├── Search API ├── Pricing API └── Magento Benefits include:\nReduced Magento load Better cacheability Independent scaling Improved response times Easier regional deployment This becomes even more valuable in a multi-region architecture because stateless services are significantly easier to replicate globally than a monolithic application.\nDatabase Strategy # The database is where many multi-region architectures become unnecessarily complicated.\nMy preference is straightforward:\nPrimary Region # 1 RDS Primary Handles:\nOrders Checkout Inventory updates Customer accounts Administrative operations Secondary Region # 1 Cross-Region Read Replica Handles:\nRead-heavy workloads Reporting Regional failover preparation Benefits include:\nReduced read pressure Geographic redundancy Faster recovery options Lower complexity This preserves a single source of truth while still gaining many multi-region advantages.\nTraffic Routing # Cloudflare becomes increasingly valuable in a multi-region design.\nInstead of exposing AWS directly:\n1 2 3 4 5 Customer │ Cloudflare │ Nearest Healthy Region Cloudflare provides:\nGlobal Anycast routing Edge caching DDoS protection WAF controls Health-based failover In many cases, Cloudflare can improve user experience more dramatically than adding additional AWS regions.\nI often recommend maximizing edge caching opportunities before introducing complex multi-region database architectures.\nHandling Regional Failures # The objective isn\u0026rsquo;t necessarily zero downtime.\nThe objective is minimizing recovery time while maintaining operational sanity.\nIf the primary region experiences issues:\n1 2 3 4 5 6 7 Region A Failure │ Cloudflare Detects Failure │ Traffic Redirected │ Region B Activated Recovery procedures become predictable.\nBecause infrastructure already exists in the secondary region, failover becomes an operational event rather than a disaster recovery project.\nThe Cost Reality # Multi-region infrastructure is not free.\nYou\u0026rsquo;ll pay for:\nAdditional compute Cross-region data transfer Read replicas Load balancers Monitoring Storage replication The question isn\u0026rsquo;t:\nCan we build it?\nThe question is:\nDoes the business benefit justify the cost?\nFor most e-commerce organizations, the answer is yes once revenue reaches a point where a regional outage becomes more expensive than maintaining standby infrastructure.\nLessons Learned # After spending years scaling e-commerce infrastructure, I\u0026rsquo;ve found that the most successful architectures are rarely the most complicated.\nA practical multi-region strategy should:\nImprove availability Improve customer experience Scale predictably Remain understandable Be operable by your existing team The biggest gains often come from:\nSeparating frontend and backend workloads Reducing dependence on Magento Leveraging Cloudflare aggressively Using auto-scaling everywhere possible Maintaining a single authoritative database Adding regional redundancy where it provides measurable value The temptation is always to chase the most advanced architecture.\nIn reality, the best architecture is the one your team can confidently operate at 2 AM during an outage.\nFor most e-commerce companies, that means building a resilient multi-region platform—not a distributed systems research project.\n","date":"1 June 2026","externalUrl":null,"permalink":"/posts/aws-multi-region-ecommerce-architecture-practical-guide/","section":"Posts","summary":"When most people talk about multi-region architectures, they immediately jump to diagrams full of globally distributed databases, active-active replication, service meshes, and enough complexity to require a dedicated platform team.\n","title":"Practical Multi-Region E-Commerce Architecture on AWS (Without Overengineering It)","type":"posts"},{"content":" About Me # I’m a Lead DevOps / Platform / Infrastructure Engineer, where I design and operate the systems that power a high-traffic e-commerce platform.\nMy work sits at the intersection of cloud infrastructure, software engineering, and operational reliability—primarily across AWS, Kubernetes-adjacent patterns, containerized deployments, and large-scale e-commerce architecture.\nBut I don’t think of myself as just an infrastructure engineer.\nI think in systems.\nHow I Think About Engineering # Most infrastructure problems aren’t actually technical problems—they’re complexity management problems.\nMy default approach is:\nReduce moving parts Eliminate unnecessary state Automate anything that is repeated more than once Prefer predictable failure modes over “clever” systems Optimize for operability at 2 AM, not theoretical elegance If a system is hard to reason about under pressure, it’s not done yet.\nThat mindset influences everything I build.\nWhat I Work On # My responsibilities span across:\nAWS infrastructure design and operations E-commerce platform reliability (Magento + modern frontend systems) CI/CD pipelines and deployment automation Docker-based scaling architectures Cloudflare edge configuration and traffic management Cross-region and high-availability planning Developer experience and internal tooling Code review, architecture guidance, and technical leadership A large part of my role is not just building systems—but making sure they remain operable, debuggable, and resilient as they scale.\nArchitecture Philosophy # I don’t believe in over-engineered systems.\nI believe in right-sized systems that evolve intentionally.\nThat usually means:\nStart simple Add complexity only when there is measurable pressure forcing it Prefer single sources of truth over distributed ambiguity Use managed services where it reduces operational burden Push logic to the edge (CDNs, frontend layers) where possible Avoid premature multi-region or distributed database complexity unless absolutely required In practice, that’s led to architectures like:\nHybrid Magento + Next.js storefronts ECS/Fargate-based frontend scaling layers EC2-based application tiers where control matters RDS with controlled replication strategies instead of full active-active complexity Cloudflare-first routing and caching strategies Beyond Infrastructure # Outside of work, I apply the same systems thinking to how I operate day-to-day.\nI maintain a highly structured workflow built around:\nKnowledge Systems # I use Obsidian as a personal knowledge system, combined with AI-assisted refinement to continuously improve and restructure technical understanding—especially while studying AWS certifications.\nRaw notes are treated as input data, not final output. They are refined into structured knowledge, then reinforced through active recall systems.\nPhysical Training System # I follow a structured 5 AM training schedule with a PPLUL split.\nThe goal isn’t optimization—it’s consistency under real-world constraints.\nI treat fitness the same way I treat infrastructure:\nIf it depends on perfect conditions, it will eventually fail.\nLinux + Desktop Environment # My primary development environment runs Arch Linux with Hyprland.\nI manage my entire configuration using a fully declarative dotfile system (chezmoi), treating my desktop like infrastructure-as-code rather than a manually configured environment.\nThat includes:\nTemplated configurations Machine-aware settings Automated reconciliation Encrypted secret injection Fully reproducible system bootstrap Writing \u0026amp; Learning # I write about systems, infrastructure, and workflows as a way of refining how I think.\nMost of what I publish comes from real implementation—not theory.\nTopics I focus on include:\nAWS architecture decisions and tradeoffs E-commerce scaling patterns Developer infrastructure and CI/CD design Linux desktop automation and reproducibility Knowledge management systems Personal productivity systems built like engineering problems Current Focus # Right now, my primary focus is continued professional development through AWS certification at the Professional level.\nI’m actively working toward completing both AWS Professional certifications, with a focus on deepening practical understanding of large-scale cloud architecture, distributed systems design, and operational excellence in production environments.\nThis isn’t just exam preparation—it’s structured reinforcement of the systems I work with daily.\nMy study approach is intentionally hands-on and implementation-driven:\nBuilding and validating real AWS architecture patterns in labs Reproducing production-like failure scenarios to understand behavior under stress Using Obsidian as a structured knowledge system to refine and retain concepts over time Leveraging active recall and spaced repetition rather than passive reading Connecting certification material directly to real e-commerce infrastructure challenges The goal is not simply to pass exams, but to strengthen architectural intuition in areas like:\nMulti-region system design and tradeoffs High availability and disaster recovery strategies Network architecture and edge routing (Cloudflare + AWS) Cost-aware scaling patterns in real production systems Operational resilience under incident conditions This continues to feed directly into my work on e-commerce infrastructure at scale, where these patterns are not theoretical—they are applied daily in production systems that need to stay reliable under real traffic and real constraints.\nClosing Thought # Good systems don’t require constant attention.\nThey absorb complexity, handle failure gracefully, and stay understandable under stress.\nThat’s what I try to build at work—and what I try to replicate in everything else I do.\n","externalUrl":null,"permalink":"/about/","section":"David Cajio | AWS, DevOps \u0026 Platform Reliability","summary":"About Me # I’m a Lead DevOps / Platform / Infrastructure Engineer, where I design and operate the systems that power a high-traffic e-commerce platform.\n","title":"About","type":"page"}]